Skip to main content

Wired Hooks

n8n-sync only fires on a deliberately small set of n8n external hooks. Each maps to one SyncEvent on the wire.

Source hookEventEntity
credentials.create / credentials.updatecredentials.upsertcredentials
credentials.deletecredentials.deletecredentials
workflow.afterCreate / workflow.afterUpdateworkflow.upsertworkflows
workflow.activateworkflow.activateworkflows
workflow.afterDeleteworkflow.deleteworkflows
workflow.afterArchive / workflow.afterUnarchiveworkflow.archiveworkflows
workflow.postExecute ★execution.upsertexecutions ★

★ workflow.postExecute is opt-in — see the SYNC_ENTITIES setting below. executions requires workflows.

Hook selection rationale​

The goal is to mirror lifecycle state without duplicating traffic. The publisher selects hooks with these rules:

  • after* over pre* — pre-hooks like workflow.create / workflow.update / workflow.delete fire before commit, before the new state is durable. The subscriber would have nothing better than a guess about what actually got written. The corresponding after* hooks fire once the DB write has succeeded, and the payload reflects the committed row.
  • postExecute only when executions is in SYNC_ENTITIES — workflow.preExecute fires for every single run with no execution-summary counterpart on the subscriber, so wiring it would create a fan-out storm with nothing useful to apply. workflow.postExecute fires once the run reaches a terminal state and carries the scalar lifecycle columns, making it the right hook for execution sync.
  • No workflow.activate deactivate path — n8n emits an internal-only event on deactivation and no external hook. There is nothing to sync from. The target converges state on the next update or activate event. See Limitations for the full set of n8n-platform quirks.

Publisher never throws​

Every hook handler is wrapped so the publisher never throws. A rejecting hook propagates up to n8n — for example it can cancel a workflow activation. Sync failures are logged and swallowed; enqueue failures are logged and dropped.

SYNC_ENTITIES gating​

When an entity is not in SYNC_ENTITIES, the corresponding hook handler is not wired at all — the key is absent from the returned hook map, so n8n pays zero fan-out overhead.

SYNC_ENTITIES valueWired publisher hooks
workflows,credentials (default)All workflow hooks except postExecute, plus all credentials hooks.
workflows,credentials,executionsAbove, plus workflow.postExecute. The subscriber resolves ExecutionRepository.
workflowsWorkflow hooks only; credentials hooks are absent.
credentialsCredentials hooks only; workflow hooks are entirely absent.
executionsInvalid. Startup fails because execution sync requires workflow sync.
workflows,executionsWorkflow hooks plus workflow.postExecute; credentials hooks are absent.

Explicit invalid names fail startup instead of being ignored. An absent or blank SYNC_ENTITIES falls back to the default (workflows,credentials), but an explicit comma-only empty value is rejected after parsing.

On the subscriber, a valid event for a disabled family returns non-retryable 422 with SYNC_ENTITY_DISABLED before repository access, ordering inspection, or execution identity access.

Credential hook contract​

Credential sync only publishes when the hook payload or resolved repository row includes both a stable credential id and encrypted string data.

  • credentials.create and credentials.update drop object-form credential payloads instead of relying on repository-side encryption.
  • When credentials.create includes an id but the row is not yet queryable, the publisher briefly retries dbCollections.Credentials.findOne({ where: { id } }) and emits only that exact row.
  • Payloads without a stable credential id are logged and dropped. The publisher never guesses by mutable fields like { name, type }.
  • Known platform gap: n8n fires credentials.create pre-commit with id: null, so creates never sync on their own. See credential backfill below.

Credential backfill on workflow sync​

Because creates don't sync, a synced workflow can reference credentials the target has never seen. To close that gap without fetching every credential on every update:

  1. workflow.upsert / workflow.activate carry an id-only credentialIds list collected from the workflow's nodes.
  2. The subscriber checks those ids with a single query and reports the missing ones back inside the 200 delivery response ({ ok: true, missingCredentialIds }).
  3. The publisher resolves each reported id by stable id and emits a credentials.upsert backfill for exactly those blobs.

The round-trip stays inside the existing publisher→subscriber channel (response body) — no separate subscriber→publisher path. Backfill lookups are id-anchored; credential upserts carry no references, so the loop terminates. Applies only when the credentials entity is enabled on both sides.

Filtering by tag​

The publisher can also restrict propagation by inspecting n8n workflow tags. See Tag-based Filtering — the subscriber side is tag-agnostic.

Reference​