Compile n8n Code nodes mechanically; hash every node's parameters

Generate Worker wrappers from the n8n export and pin a SHA-256 of the file plus a per-node parameter hash; contract:check fails the build on any drift.

Migrating from n8n

· Chapter

13

·

3

min read

The answer. When porting an n8n workflow to Cloudflare Workflows, don't re-type the Code nodes by hand — you will "fix" things while copying and lose the ability to prove parity. Treat the exported workflow JSON as the source of truth and generate from it: a wrapper per Code node, plus a contract file holding the SHA-256 of the export bytes and a SHA-256 of each node's stable-serialised parameters (prompts, models, tool budgets, parser schemas). Then make contract:check part of verify: regenerate into memory, compare with what's committed, fail if anything is stale. Drift in either direction now shows up as a red build, not a silent divergence.

The pattern.

// scripts/generate-contract.mjs — plain run writes; `--check` verifies
import { createHash } from "node:crypto";
const sha256 = (s) => createHash("sha256").update(s).digest("hex");
const stableJson = (v) => JSON.stringify(sortKeysDeep(v));

const source = await readFile("workflow-export.json", "utf8");
const { nodes } = JSON.parse(source);
const contract = {
  export: { sha256: sha256(source), nodeCount: nodes.length },
  nodeParameterHashes: Object.fromEntries(nodes.map((n) => [n.name, sha256(stableJson(n.parameters ?? {}))])),
};
const files = [
  ["src/generated/contract.ts", `// Generated. Do not edit.\nexport const CONTRACT = ${JSON.stringify(contract, null, 2)} as const;`],
  ["src/generated/code-nodes.ts", renderWrappers(nodes.filter((n) => n.type === "n8n-nodes-base.code"))],
];
if (process.argv.includes("--check")) {
  const stale = files.filter(([p, body]) => readFileSync(p, "utf8") !== body).map(([p]) => p);
  if (stale.length) { console.error(`Generated contract is stale: ${stale.join(", ")}`); process.exit(1); }
} else for (const [p, body] of files) await writeFile(p, body);
{ "scripts": { "generate": "node scripts/generate-contract.mjs",
               "contract:check": "node scripts/generate-contract.mjs --check",
               "verify": "pnpm contract:check && pnpm typecheck && pnpm test && pnpm build" } }

The receipt. In our Workflows port of the HarperFlow pipeline, verified 2026-07-25: the export had 154 nodes — 150 executable, 4 sticky notes — and all 49 Code nodes were compiled from it, none re-implemented by hand. The generator refuses to run if a future export introduces a node type without an explicit native adapter, so coverage stays exhaustive by construction. pnpm verify is the contract check, a typecheck, 32 local tests and a Wrangler dry-run bundle. The label that shipped with it was deliberately modest: implemented and locally tested; not live-verified. Mechanical parity proves the same contracts, constraints and side-effects, not the same prose, because LLM output is nondeterministic.

Watch out.

  • Hash the stable serialisation (sorted keys), or a reordered export produces a false drift.
  • Generated files are committed and never hand-edited; the check catches drift in both directions.
  • Parity of contracts is not parity of behaviour. Keep a separate live-verification checklist and don't let a green build claim more than it proves.

Related: workflows-step-do-is-an-rpc-receiver · n8n-workflow-failover-three-bindings