Wired Hooks
n8n-sync only fires on a deliberately small set of n8n external hooks. Each maps to one SyncEvent on the wire.
| Source hook | Event | Entity |
|---|---|---|
credentials.create / credentials.update | credentials.upsert | credentials |
credentials.delete | credentials.delete | credentials |
workflow.afterCreate / workflow.afterUpdate | workflow.upsert | workflows |
workflow.activate | workflow.activate | workflows |
workflow.afterDelete | workflow.delete | workflows |
workflow.afterArchive / workflow.afterUnarchive | workflow.archive | workflows |
workflow.postExecute ★ | execution.upsert | executions ★ |
★ 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*overpre*— pre-hooks likeworkflow.create/workflow.update/workflow.deletefire before commit, before the new state is durable. The subscriber would have nothing better than a guess about what actually got written. The correspondingafter*hooks fire once the DB write has succeeded, and the payload reflects the committed row.postExecuteonly whenexecutionsis inSYNC_ENTITIES—workflow.preExecutefires 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.postExecutefires once the run reaches a terminal state and carries the scalar lifecycle columns, making it the right hook for execution sync.- No
workflow.activatedeactivate 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 value | Wired publisher hooks |
|---|---|
workflows,credentials (default) | All workflow hooks except postExecute, plus all credentials hooks. |
workflows,credentials,executions | Above, plus workflow.postExecute. The subscriber resolves ExecutionRepository. |
workflows | Workflow hooks only; credentials hooks are absent. |
credentials | Credentials hooks only; workflow hooks are entirely absent. |
executions | Invalid. Startup fails because execution sync requires workflow sync. |
workflows,executions | Workflow 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.createandcredentials.updatedrop object-form credential payloads instead of relying on repository-side encryption.- When
credentials.createincludes anidbut the row is not yet queryable, the publisher briefly retriesdbCollections.Credentials.findOne({ where: { id } })and emits only that exact row. - Payloads without a stable credential
idare logged and dropped. The publisher never guesses by mutable fields like{ name, type }. - Known platform gap: n8n fires
credentials.createpre-commit withid: 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:
workflow.upsert/workflow.activatecarry an id-onlycredentialIdslist collected from the workflow's nodes.- The subscriber checks those ids with a single query and reports the missing ones back inside the
200delivery response ({ ok: true, missingCredentialIds }). - The publisher resolves each reported id by stable id and emits a
credentials.upsertbackfill 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
- Environment variables — every
SYNC_*setting, includingSYNC_ENTITIES. - Architecture — the publisher / subscriber pipeline that consumes these hooks.