Three ways to capture changes
Query-based. A job polls each table for rows whose updated_at moved since the last run. It is easy and needs no database settings, but it only sees the latest state of a row, misses rows deleted in between, and depends on every writer setting updated_at.
Trigger-based. A trigger on each table copies the old and new row into an audit table, inside the same transaction as the change. It sees every row and works on any Postgres. It also adds work to every write, needs a trigger per table, and can be dropped or disabled by a role that owns the table, which may be the role the agent uses.
Log-based. Postgres already writes every change to its write-ahead log before it commits. Logical decoding is “the process of extracting all persistent changes to a database’s tables into a coherent, easy to understand format”.1 A replication slot keeps the reader’s place, and an output plugin formats the rows. It adds no trigger to your tables, sees changes in commit order, and can’t be skipped by the writer: a row that committed is in the log. It needs wal_level = logical and a slot, which has its own care and feeding; see replication slot.
Why it matters for AI agents
An agent’s tool reports what its statement touched. CDC reports what the database committed. When those two disagree, the database is right: a one-row UPDATE that fires a trigger shows up in the stream as every row the trigger changed, in the same transaction. That makes the change stream the natural thing to check an agent against, and the natural source for undo, because it holds each changed row’s values before and after.
What CDC doesn’t tell you
- Schema changes. Logical replication doesn’t carry the database schema or DDL commands,2 so a
DROP TABLEorALTER TABLEisn’t in the row stream. Block those with a rule instead. - Sequences and large objects. Sequence data and large objects aren’t replicated either.2
- Anything outside the database. An email the agent sent or a file it wrote leaves no trace in Postgres’s log.
- Who caused it. The stream says which transaction changed which rows, not which agent or request. Attribution takes something written in the same transaction, such as a row in a ledger table or a transactional message from
pg_logical_emit_message, which becomes part of the current transaction.3 - Old values, by default. What an update or delete carries about the old row depends on the table’s replica identity; see replica identity.