PostgreSQL Trigger Not Firing on Insert – 3 Fixes
You inserted rows but your trigger didn't run. Covers the three real reasons this happens and how to fix each, from quickest to deepest.
30-Second Fix: Check If the Trigger Is Enabled
The most common reason a trigger silently stops firing: it got disabled. Maybe you or another dev ran a DROP TRIGGER or ALTER TABLE ... DISABLE TRIGGER during a migration and forgot to re-enable it. Or the trigger was created but never active.
Run this to see the trigger's current state:
SELECT tgname, tgenabled
FROM pg_trigger
WHERE tgrelid = 'your_table'::regclass;
The tgenabled column tells you:
- O – enabled (fires on insert/update/delete depending on definition)
- D – disabled
- A – always (fires even in replication, rare)
- R – fires on replica only
If tgenabled is D, re-enable with:
ALTER TABLE your_table ENABLE TRIGGER your_trigger_name;
Why this works: tgenabled is a single-character flag in pg_trigger. When you disable a trigger, PostgreSQL just flips that flag from O to D. The trigger definition stays intact. Re-enabling flips it back. No need to recreate anything.
5-Minute Fix: Verify Trigger Function Exists and Returns Trigger
If the trigger is enabled but still doesn't fire, the next suspect is the trigger function. It's possible the function was dropped or renamed, or it doesn't return NEW or OLD properly.
Check the function exists:
SELECT proname, prosrc
FROM pg_proc
WHERE proname = 'your_function_name';
If it returns zero rows, the function is gone. You'll need to recreate it. If it exists, look at its return type. For a row-level trigger, the function must:
- Return
TRIGGER(notVOIDorINTEGER) - End with
RETURN NEW;(forAFTER INSERT) orRETURN NULL;if you want to skip the insert
Here's a minimal working example:
CREATE OR REPLACE FUNCTION log_insert()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_table(table_name, action, row_data)
VALUES (TG_TABLE_NAME, 'INSERT', row_to_json(NEW));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
And the trigger that calls it:
CREATE TRIGGER trg_log_insert
AFTER INSERT ON your_table
FOR EACH ROW
EXECUTE FUNCTION log_insert();
What's actually happening here: the trigger function is invoked by PostgreSQL's executor after the row is inserted. If the function doesn't return NEW, the executor sees a null result and silently skips any further operations (like logging). That's why your audit entry never appears.
15+ Minute Fix: Schema Search Path and Trigger Conditions
If both of the above check out, the problem is more subtle. Two common culprits:
1. Schema Search Path Mismatch
You might have the trigger and table in one schema, but the function references objects (like audit_table) that exist in a different schema. When PostgreSQL runs the trigger, it uses the search path set by the session or user that performed the insert. If that path doesn't include the schema where audit_table lives, the function fails silently.
To test, run this in the same session that does the insert:
SHOW search_path;
Then check if the schema containing audit_table is in that list. If not, fix by either:
- Schema-qualifying all references inside the function:
INSERT INTO my_schema.audit_table(...) - Setting a default search path for the trigger function using
SET search_path TO my_schema, public;at the top of the function body
The reason the second approach works: SET inside a function overrides the session's search path only for the duration of that function call. So even if the user inserts from a different schema, the trigger sees the correct path.
2. Trigger Condition (WHEN Clause) Blocking Execution
If your trigger has a WHEN clause, it might be evaluating to false for all your inserts. For example:
CREATE TRIGGER trg_log_insert
AFTER INSERT ON your_table
FOR EACH ROW
WHEN (NEW.status = 'active')
EXECUTE FUNCTION log_insert();
If you're inserting rows with status set to 'pending', the WHEN condition fails, and the function never runs. This is by design – PostgreSQL evaluates the condition before calling the function. Check the condition against your insert data:
SELECT pg_get_triggerdef(oid)
FROM pg_trigger
WHERE tgname = 'trg_log_insert';
This gives you the full CREATE TRIGGER statement, including the WHEN clause if any. Adjust either the condition or your insert logic to match.
One More Thing: Triggers Are Transactional
If your trigger inserts into audit_table, but you rolled back the main transaction, the trigger's insert also rolls back. You won't see the audit row. To test in isolation, commit after your insert:
BEGIN;
INSERT INTO your_table (...) VALUES (...);
COMMIT;
-- now check audit_table
This trips up a lot of people during debugging. The trigger did fire – you just didn't commit the evidence.
Was this solution helpful?