Failover from n8n to a Workflow: three bindings, one executor per run

Make the Workflow speak n8n's webhook and execution API, so failover is three producer vars; never let one run id reach both executors.

Migrating from n8n

· Chapter

14

·

4

min read

The answer. Build the Cloudflare Workflow as a drop-in executor behind the contract n8n already exposes — the same webhook path, the same execution status/stop API shape, the same signed callbacks — so the producer never knows which engine it is talking to. Failover is then a change to exactly three producer bindings: the webhook URL, the control-API base and the control-API key. Nothing else moves — not the accepted workflow id/version your publish gate checks. The one invariant you enforce by procedure is that a given run_id reaches one executor only; the engines may coexist for different runs, never the same one.

The pattern.

# api Worker [env.prod] — the only three values failover changes
EXECUTOR_WEBHOOK_URL = "https://<executor-host>/webhook/run"   # n8n today
EXECUTOR_API_URL     = "https://<executor-host>"               # status / stop API
# EXECUTOR_API_KEY (secret): the backup accepts the same value
// backup executor Worker — n8n-compatible surface over a Workflow
if (req.method === "POST" && url.pathname === "/webhook/run") {
  const raw = await req.text();                                 // raw bytes for the HMAC
  if (!(await verifyHmac(env.HMAC_SECRET, raw, req.headers))) return new Response(null, { status: 401 });
  if (env.EXECUTION_ENABLED !== "true") return new Response(null, { status: 503 }); // fail closed
  const { run_id } = JSON.parse(raw);
  const id = `run-${run_id}`;                                   // deterministic: duplicate dispatch = same instance
  await env.MY_WORKFLOW.create({ id, params: { run_id } }).catch(ignoreAlreadyExists);
  return Response.json({ executionId: id }, { status: 202 });
}

Failover order: deploy the backup disabled → /health reports ready: true → record in-flight n8n executions → pause dispatch → flip the three values → resume with one keyword → let n8n drain. Failback is the mirror, ending with the backup disabled again.

The receipt. HarperFlow, 2026-07-25: the owner authorised exactly that three-binding exception and one producer dispatch. Production was restored to its exact pre-cutover version 21 seconds after cutover. The Workflow accepted the run and live-verified callbacks, discovery and retry behaviour; it stopped before article generation on a rejected third-party credential, so no article, CMS item, image or credit was consumed. The rule that fell out: don't restart a Workflow against a terminal failed run — production ignores status callbacks for terminal runs, so you'd create an artifact with inconsistent run state.

Watch out.

  • Don't change the accepted workflow id/version because the executor changed; the backup emits the accepted producer identity so the publish gate stays put.
  • Never delete Workflow instances during an incident — their retained state is the evidence for classifying an ambiguous external write.
  • EXECUTION_ENABLED=false by default in every lane; a validly signed dispatch to a disabled backup still fails closed.

Related: production-canary-version-pinned-cutover · n8n-code-nodes-compiled-with-parameter-hash-contract · workflows-step-do-is-an-rpc-receiver