A trigger runs a function automatically whenever a row is inserted, updated, or deleted — no application code has to remember to call it. The database enforces the behavior itself, which is exactly what you want for things like "always stamp updated_at" or "log every change".
The seed is a small documents table plus an empty document_audit log. Have a look:
SELECT id, title, updated_at FROM documents ORDER BY id;
A trigger needs a function first
You can't attach arbitrary SQL to a trigger — you attach a function that returns the special type trigger. Inside it, PL/pgSQL hands you two implicit rows: NEW (the row as it will be, for INSERT/UPDATE) and OLD (the row as it was, for UPDATE/DELETE). Write the function, then wire it up.
Here's one that forces updated_at to the current time on every change. It edits NEW and returns it — a BEFORE trigger uses the returned row as the row to actually write:
CREATE FUNCTION set_updated_at() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$;
The function on its own does nothing yet — it's just defined. CREATE TRIGGER is what binds it to a table and an event:
CREATE TRIGGER documents_set_updated_at
BEFORE UPDATE ON documents
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
Now change a row and read updated_at back — you never set it, but it moved:
UPDATE documents SET body = 'Getting started, revised.' WHERE title = 'Welcome';
SELECT title, updated_at FROM documents WHERE title = 'Welcome';
BEFORE ... FOR EACH ROW is the key pairing here: BEFORE means the trigger fires before the write, so anything it does to NEW lands in the stored row. FOR EACH ROW means it fires once per affected row (a FOR EACH STATEMENT trigger fires once per statement, regardless of row count — useful for coarse "something changed" signals, but it gets no /).