Deploy n8n Workflows via CLI, Not SQLite

Editing workflow JSON directly in SQLite leaves a stale activeVersionId and produces the 'all-green, nothing happens' ghost. The CLI import + publish path is the correct deploy.

There’s a specific failure mode in n8n that looks like success: the workflow shows active: true, every execution finishes green, and nothing happens. No error, no output, just silence.

The root cause: versioned workflows

n8n 2.x does not run from workflow_entity.nodes the way older versions did. The scheduler executes whatever version workflow_history[activeVersionId] points at. When you hand-edit the workflow JSON directly in SQLite, you update the draft row but not the activeVersionId pointer — so the scheduler keeps running the old snapshot while the UI shows your new nodes.

The classic signature is versionId != activeVersionId: your edits land in the draft (versionId), but the active version (activeVersionId) still points at the previous snapshot. Everything looks updated, and the scheduler ignores all of it.

The correct deploy path

  1. Stop n8n.
  2. docker compose run --rm --no-deps -T n8n import:workflow --input=/home/node/.n8n/x.json
  3. docker compose run --rm --no-deps -T n8n publish:workflow --id=X
  4. Start n8n.

The import step writes the workflow, and publish:workflow flips the active-version pointer correctly. That ordering is what makes the change actually take effect for the scheduler.

The lesson

When state lives in two places (a draft and an active snapshot), editing one and not the other is a silent no-op. If you’re tempted to fix a workflow by editing SQLite, stop — the CLI import + publish path exists precisely so you don’t have to know the version-pointer internals. And when a workflow is “active but does nothing”, check whether versionId and activeVersionId match before you debug the workflow logic.

Related

Filed under: Ops & Automation, Automation, n8n, Self-Hosting, SQLite