| title | Sync layout & data model |
|---|---|
| description | What the sync dir is, the folder-per-workflow layout — placeholders, code/, the structure snapshot, .decanter.json. |
| order | 1 |
The sync dir is the directory holding decanter.config.json. That is the
whole definition: every verb finds it by searching upward from wherever
you run it (and, for bundling, upward from the node file), the same way npm
finds a package.json. It is also the boundary the import rules measure
against: a relative import in a node file should resolve inside the sync
dir — one that escapes it
warns without blocking.
It is explicitly not "your git root": decanter never consults git to find it, and in a monorepo it can sit anywhere below the repo root. It doesn't have to be a repository at all — the git-dependent conveniences then stand down (auto-commit warns and skips, watch refuses its startup pull, and the guard's live mirror skips its snapshot refreshes) but syncing itself works.
The caveat is about agents, not about syncing. "Anywhere below the repo
root" holds for every verb: they search upward, and --dir /
N8N_DECANTER_DIR move where that search starts when you address the sync dir
from above it (configuration).
What placement does decide is the agent wiring init
scaffolds next to your config: an agent loads .mcp.json and
.claude/settings.json by where the agent was started, not by where the
sync dir is — see
Working with coding agents.
(init does look for a .git or a parent package.json to notice it
scaffolded into a nested project. That is not a reversal of the rule above: it
identifies the agent's project root, a different question from locating the
sync dir, which is still decided by decanter.config.json alone.)
The decanter-owned core of a scaffolded sync dir (init also copies agent
configs, package.json, and editor tooling — elided here):
my-n8n-code/ # ← the sync dir: the directory holding decanter.config.json
decanter.config.json # workflow list, root, bundleDependencies — defines this dir
.env # host + credentials (gitignored)
tsconfig.json # covers the whole sync dir (editor + typecheck)
n8n-globals.d.ts # the typed n8n globals surface
shared/ # scaffolded helper-code folder — a convention, not a rule:
# helpers may live in ANY folder(s) inside the sync dir
workflows/ # the configured root; one folder per synced workflow
.decanter-auth.json # MCP OAuth credentials (gitignored, machine-owned)
.decanter-template.json# template baseline for modification-aware re-init
… # plus AGENTS.md, .mcp.json & friends, package.json,
# decanter-ts-plugin/, editor configs — see init
Each synced workflow is one folder under the configured root:
workflows/
order-sync/ # kebab-case slug of the workflow name (a stable local pick)
workflow.json # read-only structure snapshot; code replaced by placeholders
.decanter.json # sync state — commit it, never edit it
code/
parse-order.js # one file per Code node, kebab-case-named
amazon-feed.ts
scenarios/
happy-path.json # committed pin-data set (see below)
The split of responsibilities (since the MCP-native sync): Code-node source lives here, in git, as the files decanter syncs. Workflow structure lives in n8n — you change it in the editor or over n8n's MCP tools (reached through decanter's guarded proxy), and decanter mirrors it into the read-only snapshot on every pull.
A new workflow's folder is the kebab-case slug of its name
("Order Sync" → order-sync/). If that slug is already taken by a different
workflow, it falls back to <slug>-<id8> (the same collision suffix node files
use) and warns.
Folders are a stable local pick: an existing folder is never renamed, no
matter who renames the workflow (the n8n UI or an agent over MCP). The
always-current display name lives in .decanter.json (name) instead, so the
picker, list, and ref-resolution stay accurate while your
working directory and git history never churn. Any folder name still resolves as
a ref, so a hand-rename works too. (Folders synced before this change keep their
original names and keep working.)
The workflow's structure, pretty-printed with a stable key order — except each
Code node's jsCode, whose entire value is a placeholder pointing at the
node's source file:
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "//@file:code/parse-order.js"
}workflow.json never contains code, and nothing pushes it: pull refreshes
it (reading the workflow tip — the draft when one exists), review diffs and
the offline tooling (preflight --offline,
node run, the local-engine replay
preflight --offline --simulate) read it, and local edits to it change nothing
in n8n. When the structure changed remotely, preflight's snapshot check warns
that the snapshot is out of date and pull refreshes the file.
The one meaningful local edit: re-pointing a //@file: placeholder (for a
.js ↔ .ts conversion) — the placeholders are the human-visible file map,
and push honors a re-point.
Viewer-relative and derived fields are stripped on pull (shared, scopes,
canExecute, the published-version copy activeVersion, and the
published-version pointer activeVersionId — state that churns on each
publish, with no local reader; preflight's lifecycle
check reads activeVersionId off the live workflow). The draft versionId is
kept, since the executions stale-capture warning
compares against it.
Node sources, named in kebab-case after their node (Parse Order →
code/parse-order.js). .js files are lossless (byte-identical round-trip);
.ts files are one-way — see
TypeScript nodes. Layouts from older
versions (files at the folder root) migrate automatically on the next pull.
Committed, full-workflow pin-data sets — scenarios/<slug>.json, each a
self-contained, execution-shaped file captured from a real run or scaffolded
from the workflow's schemas. test and
preflight --simulate replay one with --scenario <slug> and diff each node against it. Unlike the gitignored executions/
sibling (temporary capture data), scenarios/ is tracked in git, so a
scenario-based replay is reproducible for teammates and CI. See
scenario for how they're created, filled, and validated.
Per-folder machine state: the node-id → file map (with per-node cached names),
the per-node sync hashes used by the
drift guard, and the cached workflow name
(the display name, refreshed on every pull — it's why a kebab folder still reads
as the workflow, and why list/the picker keep working even if workflow.json
is missing or corrupt). Node ids are the identity anchor — they survive
renames made anywhere (the n8n UI, or any agent over MCP), so a rename just
moves the local file on the next pull. Commit it; never edit it by hand or
"fix" a hash.
Not per-workflow — the MCP OAuth credentials init minted
(client id + refresh token, rotated automatically). Gitignored, machine-owned;
delete it and re-run init to re-consent.
Not per-workflow — one file at the sync-dir root recording the hash of every
template file as init copied it. It's the baseline that
makes re-running init modification-aware (refresh untouched files, leave your
edits, report drift). Commit it; never edit it by hand.
After every successful push and pull, the workflow's folder is
git-committed automatically (scoped to that folder; outside a git repo it
just warns). "commitOnPush": false / "commitOnPull": false in the
config turn it off.