Self-hosted n8n audit trail: who changed what, when, and what ran

Short answer: treat audit as three append-only streams — workflow version snapshots, credential/user events, and production execution summaries — written to one Postgres table you control. Self-hosted n8n does not give you a polished compliance console out of the box; you build a thin logger that answers "who changed this workflow?" and "what did production actually send?" without guessing from chat history.

This came up on r/n8n on 6 November 2026 ("How is a audit trail implemented in a self hosted setup", 11 comments): operators need a paper trail for edits and runs, not another dashboard toy.

What "audit trail" means here

Stream Question it answers Source
Workflow versions Who edited which workflow, and what did it look like before? Export / webhook on save, or periodic snapshot
Access events Who logged in, who touched credentials, who activated a workflow? n8n DB tables + reverse proxy auth logs
Execution summaries What ran in production, with how many items, success or fail? n8n execution data or a heartbeat node

You do not need to store full input payloads of every customer email. Store identifiers, counts, hashes, and status. Keep PII out of the audit table unless you have a documented reason and retention policy.

One table that stays readable

CREATE TABLE IF NOT EXISTS n8n_audit_events ( id BIGSERIAL PRIMARY KEY, seen_at TIMESTAMPTZ NOT NULL DEFAULT now(), event_type TEXT NOT NULL, -- workflow_version | access | execution actor TEXT, -- user email / system / 'cron' workflow_id TEXT, workflow_name TEXT, resource TEXT, -- credential id, path, etc. summary TEXT NOT NULL, -- one-line human readable content_hash TEXT, -- sha256 of workflow JSON or payload fingerprint meta JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS n8n_audit_seen_at_idx ON n8n_audit_events (seen_at DESC); CREATE INDEX IF NOT EXISTS n8n_audit_workflow_idx ON n8n_audit_events (workflow_id, seen_at DESC); 

Append only. No UPDATE of old rows. If something was wrong, write a compensating event.

Stream 1: workflow versions on every meaningful save

Options that work on self-hosted setups:

  1. Scheduled export. Every N minutes, pull workflow JSON from the n8n API, hash it, and insert a workflow_version row only when the hash changed.
  2. Git backup. Many teams already dump workflows to a private repo. Treat each commit as the audit record; still mirror a one-line summary into Postgres so non-git users can query it.
  3. Manual "publish" workflow. Editors click a button that posts the current workflow JSON to an audit webhook before activating.

The important part is the hash: two saves with the same hash are noise; a new hash is a real change.

summary example: workflow_version | alice@agency.com | lead-sync | hash=ab12… | nodes=14→16 

Stream 2: access and credential events

Self-hosted n8n stores users and credentials in its own database. For a first version:

  • Log activations / deactivations of workflows (who turned production on).
  • Log credential create/update events if you can read them from the DB or admin API — never log the secret itself, only the credential id and name.
  • Put SSO or reverse-proxy auth in front of the editor UI and ship those login logs to the same table as access events.

If you cannot hook the DB yet, start with activations + versions. That already answers most "who broke prod?" questions.

Stream 3: production execution summaries

Reuse the same idea as a silent-failure watchdog: at the end of each critical workflow, POST a small heartbeat:

{ "event_type": "execution", "workflow_id": "lead-sync", "workflow_name": "Lead sync", "actor": "system", "summary": "execution ok items=42", "content_hash": "exec:2026-11-06T01:00:00Z:42:ok", "meta": { "items": 42, "status": "ok", "execution_id": "12345" } } 

Do not dump the full items array into meta. Keep counts, ids, and status.

How to query it when something goes wrong

-- Last 20 changes to one workflow SELECT seen_at, actor, event_type, summary FROM n8n_audit_events WHERE workflow_id = 'lead-sync' ORDER BY seen_at DESC LIMIT 20; -- Everything Alice touched today SELECT seen_at, event_type, workflow_name, summary FROM n8n_audit_events WHERE actor = 'alice@agency.com' AND seen_at >= date_trunc('day', now()) ORDER BY seen_at; 

Common mistakes

  • Logging secrets. Credential values, API keys, and raw email bodies do not belong in audit storage.
  • Updating instead of appending. An audit row is a fact about the past; correct it with a new row.
  • Only logging errors. Successful production runs matter when a customer asks "did you send that on Tuesday?"
  • No retention policy. Keep enough history for your contracts (often 90–180 days), then archive or delete on a schedule.

n8n or Make?

If you already self-host n8n, keep the audit writer as a small n8n workflow (webhook + Postgres). If a teammate prefers a visual router for the alert/digest side — daily email of "workflows changed yesterday" — Make can subscribe to the same webhook and format the digest: Make free plan.

What this does not replace

SOC2 evidence packs, legal holds, and full SIEM correlation. This pattern is the minimum useful trail for a small team running self-hosted automation: enough to reconstruct edits and production runs without pretending you bought an enterprise compliance suite.

Disclosure: Some links are affiliate links — I may earn a commission at no extra cost to you.