The busiest money path in the company reported 99 / 100 steps in its editor header. Zapier caps a Zap at one hundred steps. That automation turns a record in our CRM into a customer invoice in Stripe, it had grown by copy-paste for three years, and it had one step of headroom left before any further change to it required a rewrite.
We spent two weeks moving it and everything around it onto n8n, self-hosted. Eighteen Zaps became eleven workflows. I did the work with Claude Code on Opus 5 at extra effort, plus the account owner’s own logged-in browser session, and the surprise was where the fortnight went.
Building the eleven workflows was the cheap part. n8n has a real authoring surface, and an agent that can validate a node graph against the node definitions before publishing it makes that surface cheaper still. The expensive work sat on either side of the build: reading what the Zaps actually did, and proving that the replacements did the same thing. Both are legibility problems, and legibility is what a migration costs.
Which produces a decision you make about eighteen times over. A migration reads every line of automation logic you have been running without looking at, so you will find defects, and for each one you choose between porting it and fixing it. Ours came to twelve worth writing down: a currency hardcoded to USD on one branch of the invoice builder, a quantity read from a CRM field that has been empty since the day it was wired so Stripe silently received 1 every time, and three Zaps creating invoice records in parallel with no knowledge of each other, which had left 889 duplicate invoice pairs in the CRM’s history.
An inventory is a measurement, and it moves while you take it
Count the same migration in four units and you get four different projects.
In Zaps: 140 in the account, of which 18 mattered. In steps: those eighteen came to roughly 300 field-mapping panels, one Zap holding 99 of them and most of the rest under fifteen. In folders: eight, one of them labelled with an individual’s name and holding the channel by which Finance marks invoices paid. In what replaced it: eleven n8n workflows, the largest 24 nodes, inside an instance that now runs 34 live workflows with 39 more archived.
The number I got wrong was the simplest one. For the final stretch I believed five Zaps were still switched on. Five was what the folder in front of me showed, and a written snapshot from May agreed with it, which is precisely the condition under which a person stops checking. The correct question is the account-wide asset list filtered to status On with the default “owner is me” chip cleared, and it answers nine. Two of those nine had been built during the migration by someone solving today’s problem with the tool that was still there.
The account was almost idle by then, at 20 tasks used against a billing-period allowance of 10,000, and it was still load-bearing. Idle and load-bearing at the same time is the normal end state of one of these platforms, and it is why the remainder goes unnoticed.
The most expensive finding was a shape rather than a count. A departing colleague’s mail connection expired, and the ten Zaps authenticating through it stayed switched on and went quiet. A trigger that cannot poll produces no runs, and no runs produce no errors, so the history stayed clean and every toggle stayed green. An estimated 130 records never reached the CRM across six working days before anyone connected the complaints to a cause.
[devil's-advocate]The steps only exist on a screen
Zapier’s own MCP server answers questions about a Zap: its title, its id, whether it is on, who last edited it, when it was last switched on. It returns nothing about the steps. I checked twice, two months apart, in case I had missed a capability. No tier we could find sells the field mappings over an API at all.
So the fork was real, and it is the fork anyone leaving a closed automation platform arrives at.
| criterion | vendor MCP | plan upgrade | human screenshots | drive the browser chosen |
|---|---|---|---|---|
| returns field mappings | no | unclear that any tier does | yes | yes |
| returns filter conditions | no | unclear | yes | yes, via the draft editor |
| cost | none | a new contract on a platform we are leaving | days of a colleague | none |
| risk to live automations | none | none | none | opens a draft on a live Zap |
| throughput | n/a | n/a | one panel per request, async | about a panel a minute |
Buying a higher tier of a product we were three weeks from cancelling was the option I liked least, and it was also the one I could not confirm would work. So Claude Code drove the browser. What worked: enter through the folder listing rather than a direct editor URL, click a step to open its panel, then pull the panel’s text with a short script that anchors on a known label and walks up to the container holding it. I verified it twice against a panel I had also read by eye.
What resisted is worth knowing in advance. The Zapier canvas is a panned surface rather than a list, so only the currently visible nodes exist in the DOM. Enumerating the steps and iterating over them is impossible; you pan to each one, which turns the reading into a physical activity with a mouse in it. Filter conditions and inline code bodies live one layer deeper, behind “Edit Zap”, and opening that editor forks a draft against a live automation. Reading a draft is safe while nothing gets published, and it leaves a badge on the Zap that somebody has to go and clear afterwards.
The judgement that saved the week was about the 99-step Zap. Ninety-nine steps was six shapes, copied. It had unrolled a loop: three branches, each handling up to six line items, with a create-price then create-item then write-back sequence repeated per item. Read one instance of each shape and the job drops from 99 panels to 6, and the n8n replacement puts a single loop over the line-item subform where the copies used to be.
An agent will transcribe all 99 if you ask it to, cheerfully, for an hour. Deciding that six is the right number stays with you.
Porting the topology ports the bug
Eighteen Zaps became eleven workflows, and the shrinkage came from one property of the platform we were leaving. Zapier has no branching. One piece of logic with two conditions therefore lives as two Zaps, and those two Zaps have no way to know the other exists.
That is the mechanism behind the 889 duplicate pairs. Two Zaps created invoice records from different Stripe events, a third created one as a side effect of recording a payment, and each of the three checked only whether it had already run. The fix in n8n is structural: exactly one workflow may create that record type, it dedups against both the recent list and the full search index before writing, and the workflow that records payments finds an invoice or waits, then alerts, and creates nothing.
- one logic with two conditions lives as two automations that cannot see each other
- three automations create the same record type, none aware of the others
- a trigger listens for 'a record changed'; a missed event is gone permanently
- the done-flag is cleared before the outbound call that might fail
- a step set to continue on error makes the run green when the API did nothing
- why it was built that way lives in somebody's chat history
- one workflow, one branch node, both conditions readable on one canvas
- exactly one workflow may create it, with a two-tier dedup before the write
- most workflows ask 'what is still unfinished' every ten minutes
- the flag is set after the call returns success
- a shared error handler posts to a channel, and the response body is classified
- each workflow carries a sticky note naming the Zap it replaces and what changed
The third row is the one I would move first in any similar migration. Zapier listened for events, and an event a trigger misses is unrecoverable. n8n mostly polls for state, so a flag on the record is the work queue, and a queue made of records survives a restart and a deploy in a way that a queue made of events does not. The first run of the contact-sync replacement cleared 40 records that had been waiting to reach Stripe, the oldest since September 2025, in 3.8 seconds. Nobody had known they were waiting, because an event model gives you nothing to look at.
The switchovers themselves came down to a minute of clicking with a clock running. For the last pair I turned both Zaps off by hand, watched the confirmation land, then published the replacement about sixty seconds later, and afterwards went back through Stripe to confirm that no order had fallen into the gap.
Testing scales once a test stops being a single run
Verification is where a capable model changed the character of the work, and it did so by making several independent kinds of checking affordable at the same time.
The whole population. Rather than asking whether one run worked, ask how many records in the entire history carry the defect. The duplicate investigation queried the full invoice population, about 51,000 records, which is the difference between a bug report and a measurement. It also stopped a dangerous remediation: my first proposal was to delete one invoice of each pair, and the population showed that the two halves carry different fields, so deleting either one loses data.
The detector, refined until it separates. My first signature for a duplicate pair matched 9,980 records, of which 889 were real. The false ones were a 2023 data import and manually raised invoices. Rewriting the signature until it separates cleanly is the test, and the finished signature then becomes a scheduled routine that runs every morning, so a regression announces itself instead of waiting to be discovered.
The old platform’s code, run locally. The inline scripts inside the Zaps went into a local run against real inputs before their logic reached a workflow. One of those runs caught a genuine bug in my own translation of it.
The neighbouring systems. The same fact gets checked in Stripe, in the CRM, and in the warehouse. Where the three disagree you have found something either way, and several of the twelve defects surfaced there rather than in any test I designed.
A sibling on the same instance. After one workflow was published, its Stripe endpoint silently failed to register. What exposed it was another Stripe-triggered workflow on the same instance running normally on the paired event, over the same window, for the same payment. n8n reported the new workflow as active with a trigger count of one either way, because activation is recorded locally before the remote registration is attempted. The active flag records that we asked; Stripe’s endpoint list records whether it happened. An unpublish and republish fixed it, and the next payment produced a run in 47 milliseconds.
None of the five needed a model to invent them. They needed a model to be cheap. A query across 51,000 records, a signature refined over four passes, a local execution of somebody else’s JavaScript, a three-system reconciliation: each is an hour of a person’s day, and there were dozens of them. At an hour each you run the two that seem most likely to pay. At a minute each you run all of them, which is a different activity.
What one person needs, and what they would have built instead
I put the estimate together this way.
Reading: roughly 300 field-mapping panels, at ten minutes each because most carry eight to fifteen mapped fields and half reference an earlier step by number, so you cross-check as you go. Fifty hours, plus three inventory passes and the filters that only open in the draft editor. Three weeks.
Building: eleven workflows, fifteen nodes on average, credentials to wire and a webhook to register per trigger. A day to two days each. Three weeks.
Verifying: bounded by how often real events arrive rather than by how hard anyone works. One of these paths fires every two or three days. Two to three weeks of calendar time whatever you do.
The audit and the cleanup: twelve defects, each hours to days of investigation, and the remediation merged 854 of the 889 duplicate pairs by hand.
Three to four months of one person, undivided, already knowing both platforms and the business domain on day one.
That comparison is the wrong one, though. A person doing this by hand builds a different project. Nobody transcribing their two-hundredth field panel stops to ask whether the currency on that branch is hardcoded. The hand-built version ships in about six weeks, works on the day it lands, and carries all twelve defects across intact, because a port that faithfully reproduces its source is a successful port.
So the honest claim is that the same fortnight bought a different piece of work: an audit that happened to end in a migration. Cheap reading changes what a project is, well before it changes how fast the planned version of it finishes.
One Zap is still on while its owner decides whether the thing it does is still wanted. And the replacement for the product line that bills without an invoice has been confirmed on its trigger, its gate and its skip branch, while the branch that actually writes records has yet to run, because it needs a real order and those arrive every second or third day. I check it in the morning.