A Trigger Runs SQL Automatically
A trigger is a stored block of SQL that fires whenever a specific event happens on a specific table. You write it once. SQLite handles the "when" for you.
The shape:
We never wrote an explicit INSERT into price_history. The trigger did it. Every future price update will be logged the same way, whether it comes from the CLI, a script, or an app.
The Anatomy of CREATE TRIGGER
Read the syntax piece by piece:
CREATE TRIGGER trigger_name
{ BEFORE | AFTER | INSTEAD OF } { INSERT | UPDATE [ OF column_list ] | DELETE }
ON table_name
[ FOR EACH ROW ]
[ WHEN condition ]
BEGIN
-- one or more statements
END;
- Timing —
BEFOREruns before the change,AFTERruns after,INSTEAD OFreplaces it (views only). - Event — which operation triggers it.
UPDATE OF col1, col2narrows updates to specific columns. - Table — the table being watched.
FOR EACH ROW— SQLite only supports row-level triggers, so this is implicit. You can write it for clarity; it changes nothing.WHEN— an optional condition. The trigger body only runs if it's true.- Body — one or more statements between
BEGINandEND. Each must end with a semicolon.
That's the whole grammar. Most real triggers are five to ten lines.
OLD and NEW: The Row Being Changed
Inside the body, two pseudo-rows let you see the data:
NEW— the incoming row. Available inINSERTandUPDATEtriggers.OLD— the existing row. Available inUPDATEandDELETEtriggers.
A DELETE trigger only has OLD. An INSERT trigger only has NEW. An UPDATE trigger has both.
The deleted row is gone from accounts, but its data was captured in deletions before it disappeared.
BEFORE: Validate or Adjust the Row
BEFORE triggers run before the row change hits disk. They're handy for raising an error or normalising data:
The second INSERT aborts before any row is written. RAISE(ABORT, '...') cancels the current statement and rolls back to the start of it — RAISE(FAIL, ...), RAISE(ROLLBACK, ...), and RAISE(IGNORE) give you finer control over what happens.
For pure data validation, prefer CHECK constraints — they're declarative and the optimizer knows about them. Reach for a BEFORE trigger when the rule needs to look at other tables or do something a CHECK can't express.
WHEN: Conditional Triggers
A WHEN clause filters which row changes actually fire the body. It's evaluated per row, after OLD and NEW are bound:
The first order doesn't make the cut. The other two do. Without the WHEN clause, every insert would have written to big_orders and you'd be filtering on the read side instead.
INSTEAD OF: Making a View Writable
Views are read-only by default. An INSTEAD OF trigger intercepts a write against a view and runs your SQL in its place — usually translating it into writes against the underlying table(s):
The application talks to the view as if it were a table. The trigger handles the split into first_name and last_name behind the scenes.
Listing and Dropping Triggers
Triggers live in sqlite_master alongside tables and indexes:
DROP TRIGGER IF EXISTS name; is the safe form. Dropping the table a trigger lives on drops the trigger automatically — you don't need to clean up first.
Pitfalls Worth Knowing
A few things bite people the first time:
- Triggers fire per row, not per statement. An
UPDATEthat touches 1,000 rows fires the trigger 1,000 times. If the body itself is expensive, that adds up fast. - Triggers run inside the surrounding transaction. If the outer statement rolls back, the trigger's writes roll back too. That's usually what you want, but it means a trigger isn't an escape hatch for "log this no matter what."
- Recursive triggers are off by default. A trigger that modifies the same table won't re-fire itself unless you set
PRAGMA recursive_triggers = ON;. Leave it off unless you have a specific reason. - Application-side writes can bypass them, but only by skipping the database. As long as every write goes through SQLite, the trigger will run. ORMs that batch via raw SQL still trigger them.
- Don't put business logic spread across many triggers. They're invisible from the call site — someone debugging "why did this row appear?" has to grep
sqlite_master. Use them for cross-cutting concerns (audit logs, derived columns, view writability) and keep the rest in application code.
A Realistic Audit-Log Example
Pulling the patterns together — track every change to a posts table:
One trigger keeps updated_at honest and writes an audit row in a single place. The application code doing the UPDATE doesn't need to know either of those things exist.
Next: JSON Support
Triggers handle automation around row events. The next piece of advanced SQLite is what you can store inside a row — JSON. SQLite has a full set of JSON functions for querying and updating structured data without leaving SQL, and that's the next page.
Frequently Asked Questions
What is a trigger in SQLite?
A trigger is a block of SQL that runs automatically when a specific event happens on a table — an INSERT, UPDATE, or DELETE. You define it once with CREATE TRIGGER and SQLite fires it for you whenever the event occurs. It's how you keep audit logs, maintain derived columns, or enforce rules without relying on the application to remember.
What's the difference between BEFORE, AFTER, and INSTEAD OF triggers?
BEFORE runs before the row change is applied — useful for validation or massaging the row. AFTER runs once the change has happened — useful for logging or syncing other tables. INSTEAD OF only works on views and replaces the would-be operation entirely, letting you make a view writable.
How do I reference the row being changed inside a trigger?
Use NEW.column for the incoming row on INSERT and UPDATE, and OLD.column for the existing row on UPDATE and DELETE. INSERT triggers only see NEW, DELETE triggers only see OLD, and UPDATE triggers see both. These references are scoped to the row currently being processed.
How do I list or remove triggers in SQLite?
Triggers live in sqlite_master — SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'; shows them all. To remove one, DROP TRIGGER trigger_name; or DROP TRIGGER IF EXISTS trigger_name; if you're not sure it exists. Dropping a table also drops its triggers.