A South America travel company earns its living on enquiries. A traveller fills in a form, calls, or opens a live chat, and the company passes that enquiry to one of several partner tour operators, who pay for it. The largest partner also receives every lead as a Lead in its own Salesforce org.
For years that routing ran on Pabbly Connect. Since 01:55 on 28 September 2026 it has run on n8n. This is the record of why I made the move, what I found when I took the old engine apart, and how the new one was proven on real enquiries before it was trusted with one.
If you landed here from a search box, these are probably the symptoms you’re looking at:
- One lead delivered twice by a single Pabbly Router. A Router isn’t if/else: every route whose conditions are true runs.
- A task marked
Partial Failedwhere the partner’s email went out but no CRM record exists. The early steps ran; a later one didn’t. - A filter that never matches, because it tests a Gravity Forms field that can’t hold a value. An HTML block submits nothing.
Stack, as of October 2026: n8n Cloud, Gravity Forms 3.1.2 with the Webhooks add-on 1.7.0 on WordPress, Google Sheets, Gmail, Salesforce REST API v62.0, CallTrackingMetrics for phone calls and Tawk.to for live chat.
Why did the lead routing need replacing?
On 20 August 2026 the routing rule changed shape. It had been a table: this destination goes to that partner. It became a default partner, a list of exceptions, and an override for anyone who arrived through a Google Ads click. That last part matters more than it looks. Once traffic source decides where a lead goes, attribution stops being a reporting question. A misread source sends a live traveller to the wrong company.
In Pabbly, coarse routing was decided by which workflow a lead hit. There was roughly one workflow per form, per source, per partner, so changing the rule meant editing many of them by hand.
The account had also outgrown its own documentation. A June 2026 audit counted 92 workflows, 51 of them active, most with an August 2024 CLONE - copy also switched on. When I read the live routing in August, two of those lineages had grown a third generation, dated 2026, that carried all the traffic, and the workflow actually routing trip-builder enquiries was one the June audit had never seen. The clones had clones.
The moment that made the case for me came in early September. I asked the largest partner whether it wanted Paraguay enquiries. It thought it was already getting them, and so did the company. Both were half right. Trip-builder enquiries that mentioned Paraguay did reach the partner, which is why everyone believed it worked. But the Paraguay travel-guide form had never had a webhook at all, so every enquiry from that form had stayed on the website. The neighbouring Bolivia guide form did have one, which tells you this was an oversight, not a decision.
A second one surfaced later. A partner the company had dropped in June was still receiving every Ecuador guide enquiry in September, through that form’s own feed. The documentation said no leads reached it. That was true of the main router, whose route to that partner was unreachable, and beside the point.
Neither problem was hiding in any one place. The router never saw leads that never reached it, and the partner’s CRM only held what arrived. No single system could answer the question that mattered: what is this partner actually receiving?
What did the audit of the old workflows find?
I read the live routing workflows field by field, along with all the task history Pabbly keeps. That’s 15 days for workflows without delay steps, so the evidence was expiring as I read it, and I captured it first. Nothing was saved or toggled.
The headline: in that 15-day window, 41% of hand-offs ended Partial Failed. In this account that status meant the partner had already been emailed and a later step, usually the CRM write, never ran. Those leads weren’t lost. They were unrecorded, which is its own kind of expensive.
The rest, roughly in order of how much it surprised me:
A Router is not if/else. Every route whose conditions are true runs. A “catch-all” route without negative conditions doesn’t act as a fallback; it sends a second copy. On the phone path, about a third of tracked calls in a 15-day window reached two partners at once.
Some filters could never fire. The paid-versus-organic gate tested a Gravity Forms HTML block, which never submits a value, so the gate did nothing. Two routes filtered on a page break. Of the six routes in the live trip-builder router, one did what it appeared to do.
“Paid” had three definitions: a cpc substring in the page URL, a non-empty click-ID field, and the HTML-block test that could never pass. Separately, the steps named for ad detection only built display strings, and nothing downstream tested them.
One empty field could skip half a workflow. A text formatter that fails on an empty optional input returns Required params, and every step that maps from it is then marked:
Skipped: This step did not run because it depends on a previous step that was not successful.
That holds even across a Router branch. A traveller who left the phone field blank got their partner email, and then the CRM write was skipped. The partner’s lead sheet still got its row. Sheet rows were a log, not proof of delivery.
The wiring contradicted the agreed rule in ten places. Some of them I watched fire:
- the dropped partner still receiving one country’s guide enquiries;
- a former partner that had asked to stop receiving leads still getting the occasional one, under its own branded welcome email;
- a quote sent to two partners;
- the Paraguay form that sent nothing anywhere.
The rest were configured that way, whether or not traffic happened to hit them: a partner Bcc’d on another partner’s region, one country’s guide leads sent to the same partner twice, paid leads going to a regional partner outside the trip builder, a country filter testing a field that actually held an IP address, the largest partner’s travellers getting no welcome email, and one brochure route with a broken filter and an empty To.
The drift showed, too. Some routes still read field IDs from an older version of the form. A popup tool emitted every lead twice, once as a webhook and once as a notification email, and both ingests were wired.
None of that is anyone’s negligence. It’s what years of a rule changing faster than its wiring looks like.
Why rebuild instead of repairing it?
I weighed five options.
Fix it in place. Rejected. Most of the wiring didn’t work, so the thing to port was the intent, not the wiring.
Rebuild it by hand in Pabbly. Rejected, and this is the one worth explaining. It isn’t a judgement on Pabbly as software; it’s a judgement on the shape of the problem. A rule change in Pabbly means editing many workflows by hand, with no version control and no undo. As of October 2026, Pabbly Connect offers no API for listing, reading or editing workflows, and its MCP server exposes individual action steps you’ve already configured, not the workflows themselves. I maintain systems through their APIs: scripts, version control, and increasingly AI-assisted tooling that reads and changes a system the same way. A routing layer that can only be changed by clicking through it one workflow at a time can’t be maintained like that. Rebuilding by hand in Pabbly would have reproduced the property that broke it.
Move the routing into Gravity Forms conditional logic. Rejected. It scatters the rule across about twenty form configurations, which is the old failure mode in a new building.
Zapier. Excluded early, on cost and integration fit.
n8n Cloud. Chosen. The rule lives in one node, in one workflow file, under version control. Two features decided it, and neither was volume: the public REST API, so the workflow is built and changed as versioned JSON, and execution search, which turns “did you send us that lead?” from an argument into a lookup.
Two principles carried through the build. Replicate the delivery contracts: partners’ systems expect particular payloads, and those were copied faithfully. Redesign the routing decision: the rule was rebuilt from what the business had confirmed, not ported from what the wiring did.
And migrate form by form, not partner by partner, because the live trip-builder router held every partner’s route in one workflow and couldn’t be split. The old system stayed as the rollback: deactivated, never deleted.
What replaced it?
One way out of every form. Each of the 18 forms that feed the engine has one identical Gravity Forms webhook feed into one endpoint: all fields, no field mapping, no conditional logic. The per-form field maps live in the engine, in one place. A form the engine doesn’t recognise goes to quarantine, where somebody sees it, instead of being quietly misrouted.
Archive before anything can fail. The raw payload is written first, so any lead can be replayed.
Isolate delivery from formatting. A lead that can’t be formatted is quarantined before any delivery, so a partner is never emailed a lead the engine then fails to record.
Nothing vanishes. Delivered, quarantined or unroutable, every lead writes a row, and the delivery log records the partner and the reason for every hand-off.
One rule, one place. The whole routing rule sits in one Code node, named Route. Almost everything else is n8n’s visual nodes, and 38 of the engine’s 39 nodes carry a note saying why they exist. It’s built to be read by whoever maintains it next.
The honest count is three, plus one that takes notes. Three workflows touch a lead: the routing engine, for web forms and for chat leads handed to it; a phone workflow; and a small intake adapter for live chat. The phone workflow is separate because the engine’s validity gate requires an email address, and a phone call doesn’t come with one. Giving calls their own workflow with the same conventions beat weakening the gate for every lead. The chat adapter exists because the engine refers to its webhook node by name in more than 200 places, and a second front door would have meant rewriting all of them on a live pipeline. A fourth workflow only records chat transcripts and delivers nothing.
One more fact, because people ask: no AI decides where a lead goes. Routing is deterministic: region, paid or organic, and call tags, in a fixed order.
How was the rule written so the business could own it?
Before any workflow existed, the rule was written as a platform-neutral function with a table of test cases. Every open business question became a row that passes or fails, so “does a Peru-and-Bolivia trip still count as pure?” became “look at row P4.”
The precedence, in order:
- A trip that includes the Guianas stays in-house. No partner sells them.
- Any Google Ads click goes to the largest partner.
- A trip that includes Colombia or Venezuela goes to Partner B.
- A trip of only Peru, only Bolivia, or both, goes to Partner B.
- A trip of only Brazil, only the Amazon, or both, goes to Partner C.
- Everything else goes to the largest partner.
Regions expand to countries before any test runs, so Patagonia, which spans Argentina and Chile, can never count as “only” anything. Every path returns exactly one partner.
“Paid” means a Google Ads click and nothing else, on purpose. Widening it moves leads from one partner to another, which is a commercial decision, not a technical one.
One diagram is the contract. The company’s owners signed a one-page routing chart on 15 September 2026, and the code reads top to bottom in the same order. The comment above it ends: “if a branch cannot be pointed at on the chart, it does not belong here.” Signing changed two behaviours. A Google Ads click on a Colombia or Venezuela trip now goes to the largest partner, because the ad click is decided before any destination. And a trip of Brazil and the Amazon together now goes to Partner C, where before only a single one of them did.
The chart is generated by a script, so updating it is an edit and a re-run, not a redraw. It still drifted: by cutover it was missing two intake channels and still marked the Guianas as undecided. I brought it up to date that day.
The tests drifted too, once. On 9 September two rows still held expectations from before the business refined the rule, and they were failing against the very rule they tested. Now the test runner pulls both the rule and the table out of the spec at run time, so neither can drift on its own. 41 cases plus 10 malformed-input cases pass, and the live node’s rule matches the spec.
That’s the ownership part, and it’s the part I care about most. One rule, one diagram, and a log of every lead with the partner it went to and why. Before, neither the company nor its partner could say which destinations the partner was actually receiving. Now that’s a query.
How was it proved before the switch?
From 17 September the engine ran live beside the old system in shadow mode: emails to a test inbox, Salesforce off, partner sheets untouched. The old system was still writing those sheets, and a second write would have doubled every row in what was, in practice, the company’s lead record.
The new feeds ran beside the old ones, which were left alone. The Webhooks add-on processes feeds asynchronously, so visitors saw no delay, and turning off either the new feed or the workflow stopped it. One detail worth knowing if you try this: Gravity Forms attempts each async feed once by default ($max_attempts = 1 in class-gf-feed-processor.php, raisable through the gform_max_async_feed_attempts filter). A webhook request that fails isn’t retried.
To exercise branches without firing the old system twice, real entries were replayed through the new feed only, using the add-on’s own processor on an in-memory copy of the feed:
gf_webhooks()->process_feed( $feed, $entry, $form );
Nothing was saved. Every branch was exercised, and the shadow widened form by form. Across the whole shadow period, not one email from the engine reached a partner.
Shadow has a blind spot: test mode never touches production-only paths. That mattered twice. A bug that only fired in production was caught by a local simulator before shadow began, and on cutover day a credential failed in a way no test could see.
This one is mine. Three days before cutover, only 6 of the 17 forms fed the engine. Switching the old feeds off would have taken eleven forms dark. It sat on my list as “add shadow feeds to the other guide forms,” and I never framed it as a launch blocker. It was one. The fix was to clone the one identical feed onto all eleven. The Paraguay form got its first webhook ever on 26 September.
Did the new engine agree with the old one?
The side-by-side comparison was on my cutover checklist, and I didn’t run it before switching. I ran it the same morning, after. It wasn’t useless, because any disagreement would still have been live and fixable, but it should have been the gate, and I treated it as a formality.
The method: take every lead from the shadow period’s delivery log and match it, by email, to what the old system had written into the three partner sheets.
The result: 93% of matched leads went to the same partner. Every disagreement was a case the old system was already known to get wrong:
- organic enquiries for Peru or Bolivia that the old wiring sent to the largest partner instead of Partner B;
- one for Venezuela, which the rule gives to Partner B;
- enquiries that included the Guianas, which the old wiring sent to a partner anyway and the engine keeps in-house.
There was no case where Pabbly was right and n8n was wrong.
One caveat, stated rather than buried. Matching by email across each sheet’s whole history makes two other counts noisy, because an older enquiry from the same person matches too, so I don’t quote them. The disagreements are the signal, because each one independently matches a known defect.
How was the switch made?
It was planned as three waves: the email-only partners, then the partner CRM, then the trip builder. On 28 September it went in one pass. Every branch had run in shadow, the production CRM checks were closed, and the old system was still misrouting live leads every day. Waiting bought nothing. What I should have had in hand was the comparison above.
Old off first. The 17 old feeds were deactivated at the source, and the engine moved to production seconds later. The worst case was a short gap where leads were archived but not delivered, which is recoverable. The alternative was duplicate records in a partner’s CRM that we can’t delete from our side.
The phone path waited until the old webhook was removed on the call-tracking side. Switching it first would have delivered every call twice, which is the exact defect being retired.
Starving a workflow isn’t switching it off. Three kinds of old workflow weren’t triggered by Gravity Forms at all: one parsed chat transcripts out of inbound email, one turned mailbox messages into SMS alerts, and one listened to a popup tool that had already been retired. So the workflows themselves were disabled, the chat one checked first, since a duplicate there would write a second CRM Lead for every chat.
There was no synthetic test lead in production. It would have emailed a real partner and created a permanent record. Verification meant watching the first real leads go through the delivery log.
Rollback is one move: turn the old feeds back on, and put the engine’s three modes back to test, off and shadow.
What did it cost, and what does it change?
Testing isn’t free anymore. A form submitted on the live site reaches a real partner. The safe smoke test is an enquiry for the Guianas, which stays in-house.
A green test suite proved nothing about the live credential. On cutover day every Salesforce write failed for the engine’s first hours live, while every test passed. That incident gets its own record. The short version: a failed CRM write cost attribution, not the lead, because the partner was still emailed.
Concurrency becomes your problem. n8n runs executions in parallel. Under simultaneous writes, the default Google Sheets append lost rows while every execution reported success; the useAppend option covers the spacing real form traffic produces. Workflow static data is written whole at the end of each execution, so two overlapping runs can overwrite each other’s changes.
The company owns it. The n8n account was registered to a company mailbox from the first day, and the Google credentials belong to the company’s team account, not to me. Handing it over is a billing change, not a migration.
Still open: the Paraguay enquiries that never left the website before that form got its feed. Replaying them is a decision, not a task: partner delivery only, with no welcome email arriving months late.
How it’s watched now: a canary follows every lead by ID, every hour, and stays quiet when everything reconciles.
What would I do first next time?
- Read the old system for its promises, not its wiring.
- Write the rule once, as a function whose test cases are the business’s open questions, and keep the diagram and the code in the same order.
- Archive first, and give every terminal state a row.
- Put every form in shadow well before cutover day, and compare against the old system before you switch, not after.
- Switch the old system off first when duplicates cost more than gaps, and disable its workflows rather than starving them.
Part of a series. Six System Debriefs on rebuilding the lead routing of a South America travel company. This is part 1.
- From 51 Pabbly Connect workflows to three in n8n (this post)
- Every test green, every Salesforce write failing: scope parameter not supported — the cutover-day credential failure, and what it cost.
- Writing Leads into a Salesforce org you don’t own — writing into a partner’s CRM you can’t see inside or delete from.
- Lead source tracking that decides routing: gclid, call tags and live chat — how a lead’s source decides which partner gets it.
- Changing a live n8n workflow without testing on real partners — how a change is made and proven before it reaches a partner.
- Missing leads between web form and CRM: reconcile, don’t guess — finding which leads went missing, and where.
System Debriefs are drafted from working-session context by the harness that did the work, then reviewed and published by me.