All playbooks
Automation
22 min read

Migrating from Zapier to n8n

Zapier is a fine on-ramp to automation. It is not a fine destination. Here is the staged migration I run when a client's bill has crept past the cost of a junior dev's day, without breaking any of the workflows their business actually depends on.

Why bother migrating

The argument for moving off Zapier is not really about money, although the money is real. The argument is about ceiling. Zapier optimises for getting non-technical users to a working automation in five minutes. That is a wonderful starting point. It also means anything beyond a linear, two-step zap becomes either impossible or absurdly expensive.

n8n inverts the trade. It assumes you can handle a little bit of code, in exchange for: branching, error handling, real loops, sub-workflows, custom nodes, a JavaScript Function node, predictable per-month pricing, and the option to self-host. Most teams on a £200-400/month Zapier plan have outgrown Zapier and just have not noticed.

The right time to leave Zapier is the first time you find yourself paying more for tasks than for engineers.

The audit nobody wants to do

Before you migrate anything, list everything. Zapier's UI does not make this easy because zaps tend to multiply quietly. Export the list and put it in a spreadsheet with the following columns: name, trigger, integrations used, monthly task count, business owner, last edited.

You will discover three categories. Roughly:

  • Critical and active (10-20%) — invoicing, customer onboarding, lead routing. These move last and most carefully.
  • Routine and active (40-60%) — Slack notifications, calendar syncs, simple field copies. These migrate first; they are easy wins.
  • Dead (20-40%) — built for a campaign two years ago, fired three times last month, owned by someone who left. Delete these in Zapier first. There is no point migrating dead code.

Mapping Zapier to n8n

Most Zapier concepts have a direct n8n equivalent. A few are subtly different and that subtlety is where bugs live.

  • Trigger → n8n's trigger nodes. App-specific (Gmail, HubSpot, Stripe) where possible, otherwise a Webhook node.
  • Action step → app-specific node, or HTTP Request node for anything not built in.
  • Filter → n8n's IF node, which evaluates a single condition, or a Switch node for multi-branch logic.
  • Formatter → JavaScript in a Function node. Far more powerful, slightly more responsibility.
  • Paths → just multiple branches off a Switch node. Cleaner than Zapier's paths.
  • Sub-zap → "Execute Workflow" node. This is genuinely better than Zapier's sub-zaps and worth designing for from the start.

Run them in parallel first

Resist the urge to flip a switch. For each workflow being migrated, run the n8n version alongside the Zapier version for at least a week. The pattern looks like this:

  • Build the n8n workflow with the same trigger.
  • Make every "destructive" action (sending an email, creating a record, hitting a billing API) a no-op or a write to a side channel like Slack.
  • Compare the n8n outputs to Zapier's actual outputs daily.
  • When they match for five consecutive runs, swap the no-ops for real actions and disable the Zapier zap.

For high-volume workflows where running both would cause double-spend, use the n8n IF node to route 10% of traffic to n8n and 90% to a passthrough that lets Zapier handle it. Increase the percentage daily.

javascript
// In an n8n Function node — deterministic 10% rollout const id = $json.lead_id || $json.email const hash = require('crypto') .createHash('sha1') .update(String(id)) .digest('hex') const bucket = parseInt(hash.slice(0, 8), 16) % 100 return [{ json: { ...$json, route: bucket < 10 ? 'n8n' : 'zapier' } }]

Patterns that translate badly

A handful of Zapier idioms turn into landmines if you copy them literally into n8n.

Looping

Zapier loops are awkward — most users avoid them by chaining steps. n8n loops over arrays by default; every node runs once per item. This is brilliant when you expect it and catastrophic when you do not. If you intended one Slack message and you accidentally feed in an array of fifty leads, you will send fifty Slack messages. Use the Aggregate node or wrap in a sub-workflow when you want "exactly once" semantics.

Built-in retries

Zapier retries automatically and silently for many errors. n8n does not — by design. Configure the per-node "Retry on Fail" option for any external API call. For genuinely critical paths, wrap the workflow in error-trigger handling so a failure pages you, not just logs.

Storage

Zapier's "storage" feature is a small KV store. n8n has nothing equivalent built in. Either use a real database (Postgres if you self-host, Supabase otherwise), or for trivial counters and de-dupes use the static workflow data API.

The cutover

For each workflow that has run in parallel without divergence for a week, the cutover is the boring part:

  • Announce the change in your team channel with the workflow name and time.
  • Disable (do not delete) the Zapier zap.
  • Switch the n8n workflow from no-op to active.
  • Watch for an hour. Watch the n8n execution log, the destination system, and any error channel.
  • If it is fine, leave the Zapier zap disabled for thirty days before deleting it. Cheap insurance.

Observability after the fact

The thing you lose moving off Zapier is their dashboard — the at-a-glance "this many tasks ran, this many failed". You should rebuild a minimal version of this. The simplest version is an n8n workflow that runs hourly, queries the executions table directly, and posts a summary to Slack.

sql
-- Hourly health check, run from an n8n SQL node select workflow_id, count(*) filter (where finished is false) as in_flight, count(*) filter (where status = 'error') as errored, count(*) filter (where status = 'success') as ok, max(started_at) as last_run from execution_entity where started_at > now() - interval '1 hour' group by 1 order by errored desc, last_run desc;

Pipe anything with errored > 0 into a Slack message that tags the workflow owner. The first month is the test — you will discover which of your migrated zaps were silently failing in Zapier the whole time.

Pitfalls

OAuth scope creep

Re-authenticating Google or Microsoft from n8n often requires broader scopes than Zapier asked for. Get IT approval up front; do not be the engineer who triggered a Tuesday-morning security review.

Webhook URL changes

Every Zapier-managed webhook has a URL that you do not control. n8n webhook URLs you do. When you move a trigger from Zapier to n8n, every external service holding the old URL must be updated. Track them.

Time zones

Zapier does most things in UTC unless told otherwise. n8n nodes vary. Set GENERIC_TIMEZONE on the n8n container and double-check any cron-based trigger before you ship it.

Hidden Zapier "by Zapier" steps

Zapier inserts implicit steps for things like deduplication. When you reach a workflow that "just worked" in Zapier and behaves oddly in n8n, look for these — they are usually the culprit.

Wrap-up

A clean Zapier-to-n8n migration is the kind of project that pays for itself in the first quarter and keeps paying every quarter after. Take it slowly, run things in parallel, kill the dead zaps before you migrate them, and resist the urge to cut over everything in a single weekend.

The best signal that you have done it right is that nobody outside the engineering team notices. Same data flowing to the same places, just on infrastructure you actually own.

Want this done for you?

If you would rather skip the YAK shave and have someone who has done this fifty times set it up properly, that is what I do for a living.

Start a project