A PostgreSQL trigger binds a function to a table so that the function runs automatically when a defined event occurs — an INSERT, an UPDATE, a DELETE, or a TRUNCATE. Two separate pieces are required: the function itself, and the CREATE TRIGGER statement that attaches it to the table.
Setting up the table and the function
Because a trigger always belongs to a table, the example starts with one. The goal is to inspect incoming values and silently correct them: any value below zero becomes -1.
PgSQL
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
test=# CREATE TABLE t_temperature ( id serial, tstamp timestamptz, sensor_id int, value float4 ); CREATE TABLE test=# d t_temperature Table 'public.t_temperature' Column | Type | Collation | Nullable | Default -----------+--------------------------+-----------+----------+------------------------------------------- id | integer | | not null | nextval('t_temperature_id_seq'::regclass) tstamp | timestamp with time zone | | | sensor_id | integer | | | value | real | | | |
A function is then written to perform that correction. It reads NEW, the variable holding the row the trigger fired for; changing it changes what actually lands in the table.
PgSQL
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
CREATE OR REPLACE FUNCTION f_temp () RETURNS trigger AS $ DECLARE BEGIN RAISE NOTICE 'NEW: %', NEW; IF NEW.value < 0 THEN NEW.value := -1; RETURN NEW; END IF; RETURN NEW; END; $ LANGUAGE 'plpgsql'; |
NOTE: If the function returns
NEW, the row will be inserted as expected. However, if you returnNULL, the operation will be silently ignored. In case of a BEFORE trigger the row will not be inserted.
Attaching the trigger
With the function in place, the trigger itself is created and pointed at that function.
PgSQL
|
1 2 3 |
CREATE TRIGGER xtrig BEFORE INSERT ON t_temperature FOR EACH ROW EXECUTE PROCEDURE f_temp(); |
This trigger fires only on INSERT, shortly before it happens. PostgreSQL also distinguishes row-level from statement-level triggers; row-level triggers, which execute the function once per modified row, are the common choice.
The full syntax reference is at https://www.postgresql.org/docs/15/sql-createtrigger.html.
BEFORE vs. AFTER, and why naming matters
A trigger can be declared BEFORE or AFTER the operation. In the BEFORE case, the function runs before the row reaches the table, so it can still modify the row. In the AFTER case, the row is already inserted and the function sees it as stored.
The following image shows where to insert a trigger:

Any number of BEFORE and AFTER triggers may exist on one table. Order of execution has been deterministic since PostgreSQL 7.3: PostgreSQL runs all BEFORE triggers in alphabetical order, performs the operation, then runs all AFTER triggers in alphabetical order. That guarantee is what keeps trigger behaviour reproducible.
Testing it
Once deployed, the example can be exercised by inserting two rows — one is modified, the other passes through unchanged — and the trigger emits two log messages showing the contents of NEW.
PgSQL
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
test=# INSERT INTO t_temperature (tstamp, sensor_id, value) VALUES ('2021-05-04 13:23', 1, -196.4), ('2021-06-03 12:32', 1, 54.5) RETURNING *; NOTICE: NEW: (4,'2021-05-04 13:23:00+02',1,-196.4) NOTICE: NEW: (5,'2021-06-03 12:32:00+02',1,54.5) id | tstamp | sensor_id | value ----+------------------------+-----------+------- 4 | 2021-05-04 13:23:00+02 | 1 | -1 5 | 2021-06-03 12:32:00+02 | 1 | 54.5 (2 rows) INSERT 0 2 |
Available row variables
The INSERT example makes NEW available, but other operations differ:
- INSERT:
NEW - UPDATE:
NEWandOLD - DELETE:
OLD - TRUNCATE: none
An UPDATE trigger therefore has access to both the old and the new version of the row, while a DELETE trigger can inspect the row about to be removed.
Predefined TG_* variables
A trigger function also knows the context it was called in, exposed through a set of predefined variables:
PgSQL
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
CREATE OR REPLACE FUNCTION f_predefined () RETURNS trigger AS $ DECLARE BEGIN RAISE NOTICE 'NEW: %', NEW; RAISE NOTICE 'TG_RELID: %', TG_RELID; RAISE NOTICE 'TG_TABLE_SCHEMA: %', TG_TABLE_SCHEMA; RAISE NOTICE 'TG_TABLE_NAME: %', TG_TABLE_NAME; RAISE NOTICE 'TG_RELNAME: %', TG_RELNAME; RAISE NOTICE 'TG_OP: %', TG_OP; RAISE NOTICE 'TG_WHEN: %', TG_WHEN; RAISE NOTICE 'TG_LEVEL: %', TG_LEVEL; RAISE NOTICE 'TG_NARGS: %', TG_NARGS; RAISE NOTICE 'TG_ARGV: %', TG_ARGV; RAISE NOTICE ' TG_ARGV[0]: %', TG_ARGV[0]; RETURN NEW; END; $ LANGUAGE 'plpgsql'; CREATE TRIGGER trig_predefined BEFORE INSERT ON t_temperature FOR EACH ROW EXECUTE PROCEDURE f_predefined('hans'); INSERT INTO t_temperature (tstamp, sensor_id, value) VALUES ('2025-02-12 12:21', 2, 534.4); |
TG_RELID: the internal object id of the table being triggeredTG_RELNAME+TG_TABLE_SCHEMA+TG_TABLE_NAME: table and schema namesTG_OP: whether the trigger fired for INSERT, UPDATE, DELETE or TRUNCATETG_WHEN: whether the trigger fired BEFORE or AFTER the operationTG_LEVEL: statement or row level triggerTG_ARGS: the number of arguments passed to the triggerTG_ARGV: the list of arguments (index starts at 0)
Running the listing makes the trigger report exactly what is happening:
PgSQL
|
1 2 3 4 5 6 7 8 9 10 11 12 |
NOTICE: NEW: (8,'2025-02-12 12:21:00+01',2,534.4) NOTICE: TG_RELID: 98399 NOTICE: TG_TABLE_SCHEMA: public NOTICE: TG_TABLE_NAME: t_temperature NOTICE: TG_RELNAME: t_temperature NOTICE: TG_OP: INSERT NOTICE: TG_WHEN: BEFORE NOTICE: TG_LEVEL: ROW NOTICE: TG_NARGS: 1 NOTICE: TG_ARGV: [0:0]={hans} NOTICE: TG_ARGV[0]: hans NOTICE: NEW: (8,'2025-02-12 12:21:00+01',2,534.4) |
That context is what allows one function to be reused across more than one table instead of being written per table.
Looking further
Triggers support considerably more than the introduction covered here, and the subject rewards closer study. Earlier material on sophisticated temporary tables covers another corner of PostgreSQL.



