Menu

SQLite Triggers: CREATE TRIGGER, BEFORE/AFTER, and OLD/NEW

How SQLite triggers work — BEFORE and AFTER, INSTEAD OF on views, the OLD and NEW row references, and when triggers are the right tool.

This page includes runnable editors — edit, run, and see output instantly.

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;
  • TimingBEFORE runs before the change, AFTER runs after, INSTEAD OF replaces it (views only).
  • Event — which operation triggers it. UPDATE OF col1, col2 narrows 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 BEGIN and END. 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 in INSERT and UPDATE triggers.
  • OLD — the existing row. Available in UPDATE and DELETE triggers.

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 UPDATE that 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_masterSELECT 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.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED