Most agency outbound stacks are five good tools and four bad handoffs. Sales Navigator finds the person. Clay enriches and qualifies the row. A sequencer sends. A CRM remembers. A sheet or an n8n workflow carries the row from one to the next. Each tool does its job well. The failures live in between, and they are invisible, because a fact that gets dropped at a boundary does not raise an error. The next tool simply never knew it.
This is the companion to Build it yourself, and what it costs. That page answers whether the stack is worth buying. This one is for the reader who builds it anyway, and wants it to stop contacting people who already said no.
Eight facts every lead carries, and where each one dies
Take one lead. Call her Jane, a VP of Sales at a 200-person SaaS company, found in a Sales Navigator search, enriched in Clay, sequenced through Instantly, logged in HubSpot, with n8n moving the row between them. Along the way she carries eight facts, and every one of them has a boundary where it is routinely lost.
1. A stable identity. Who Jane is, in a form that survives a job change and a new email. Sales Navigator knows her as a member id. Clay knows her as a row with a LinkedIn URL and, after enrichment, an email. The sequencer knows her as an email address. The CRM deduplicates on that same email: HubSpot's own rule is that a contact is matched on the Email property, and a second record with the same address is refused or merged. So the day Jane moves companies and gets a new address, the CRM creates a second Jane, the sequencer treats her as a brand new lead, and the only tool that ever knew the two were the same person was the one at the start of the chain. Nothing downstream can see it.
2. Source and consent provenance. Where the row came from and under what basis you may contact it. Sales Navigator will not export a lead list to CSV; LinkedIn's help page says so plainly, and the sanctioned route is a CRM sync on Advanced Plus. So the row leaves through a browser extension or a copy, and at that moment it stops carrying "found via Sales Navigator search X on date Y". Clay's CSV export mirrors the table, which means it carries whatever columns you thought to keep. By the sequencer, the provenance is whatever the import file was named.
3. The ICP score. Clay computes it, in a column, with a formula you wrote. The sequencer does not have a concept of a score. It has lists. So the score becomes the name of the list ("Tier 1 SaaS VPs"), and the reason Jane scored 82 rather than 61 is gone. When she replies and the CRM asks how qualified she was, the answer is the list name.
4. Last touch. The sequencer knows the last email it sent. The LinkedIn tool, if there is one, knows the last connection request. The CRM knows whichever of them wrote back to it last. Nobody holds the union. Jane gets an email on Tuesday and a connection note on Wednesday that opens as if the email never happened.
5. Reply state. Instantly's "stop sending emails on reply" setting pauses the follow-ups for a lead who answers, and the company-wide variant marks every other lead at that domain as completed. That is the right behaviour, and it is scoped to the sequencer. The CRM learns about the reply when the sync runs, if it runs, if the reply was on a thread it can see. The LinkedIn tool learns nothing. So the reply that should have stopped everything stops one channel.
6. Exclusion state. The hard one. The people you must never contact: current clients, competitors, the colleague who left, the person who asked to be deleted. Instantly holds a global blocklist that is checked at import and across running campaigns. The CRM holds an unsubscribe. The LinkedIn tool holds its own list. Clay holds nothing, because it is a workbench, not a sender. Four lists, none of them the source of truth, and an exclusion added in one is silently absent from the other three. The migration guide covers how the four kinds of "do not contact" differ; the point here is that in a multi-tool stack each kind lives in a different tool.
7. Retry ownership. When the enrichment call fails, or the sequencer's API returns a 429, who tries again, and who makes sure it does not try twice? n8n's node settings offer "retry on fail" with a maximum number of tries and a wait between them, plus an error workflow that fires when a run dies. That handles the retry. It does not handle idempotency: a retried "add lead to sequence" after a timeout that actually succeeded is a duplicate lead in the sequence, and the sequencer will happily send Jane the first email twice.
8. The reason she entered the campaign. She liked a competitor's post on Thursday. That is why she is in this list and not another. It is also the only fact that makes the opening line honest ("saw your comment on X"). It lives in whichever tool detected the like, as an event with a timestamp. By the sequencer it is a merge field, if someone mapped it, with no date. Three weeks later, when the sequence finally reaches her, the opener refers to a comment she has forgotten, as if it were this morning.
The boundary audit
The exercise is mechanical and takes an afternoon. For each boundary in your stack, for each of the eight facts, answer one question: does this fact cross, and in what form? Three possible answers: crosses intact (same key, same meaning), crosses degraded (a list name instead of a score, a URL instead of a member id, a flag instead of a date), or does not cross.
For the stack above, the grid looks roughly like this. Yours will differ, which is the point.
| Fact | Sales Nav to Clay | Clay to sequencer | Sequencer to CRM | CRM back to sequencer | Glue (n8n) |
|---|---|---|---|---|---|
| Stable identity | degraded (URL, not id) | degraded (email only) | intact (email) | intact | passes what it is given |
| Provenance | lost | degraded (file name) | lost | lost | lost unless mapped |
| ICP score | n/a | degraded (list name) | lost | lost | passes if mapped |
| Last touch | n/a | n/a | degraded (email only) | lost | lost |
| Reply state | n/a | n/a | degraded (on sync) | lost | lost |
| Exclusion state | lost | lost | lost | lost | lost |
| Retry ownership | n/a | manual | manual | manual | retry yes, dedup no |
| Reason for entry | lost | degraded (merge field, no date) | lost | lost | passes if mapped |
Two rows deserve a closer look. Exclusion state is lost at every boundary because no tool in the chain treats it as data that travels; each one treats it as a setting. And retry ownership is the row where the glue looks like it helps and does not: n8n retries, but the tool it retries against does not know it is a retry.
What to do about it, in order
You can fix a stack like this without replacing it. The fixes below are ordered by how many rows of the grid they repair, and the first two are not tools.
1. Pick one key and carry it everywhere. The LinkedIn member id if you have it, the email if you do not, and never the vanity URL, which is reassignable. Put that key in a column in Clay, a custom field in the sequencer, a property in the CRM, and pass it through every n8n node untouched. This is the fix for identity, and it is what makes every other fix possible, because the others all need to say "this row is that row".
2. Make the suppression list a file, not a setting. One CSV, outside every tool, with the four kinds of "do not contact" and the reason for each row. Import it into every sender as a blocklist, re-import it on a schedule, and run every new list against it before the list enters any tool. The migration worksheet has the column layout. This repairs the exclusion row, which was lost everywhere.
3. Carry the reason and its date as two fields, not one. entry_reason and entry_reason_at. The sequencer's opener then has both the event and its age, and a rule ("do not refer to an event older than 14 days") becomes possible. Without the date, the rule cannot exist.
4. Give every write an idempotency key. Before n8n adds a lead to a sequence, it computes a key from the person key plus the campaign, checks the sequencer for it, and skips if present. This is what turns "retry on fail" from a duplicate generator into a retry.
5. Make the CRM the reply ledger, and make it write back. The sequencer knows a reply happened. Push it to the CRM immediately over a webhook, not on the next sync, and have the CRM push "replied" back to every other sender, so the LinkedIn tool stops too. This repairs reply state and half of last touch.
6. Store the score, not just the list. A number column in the CRM, written by Clay, updated when the ICP changes. Then "how qualified was she when we reached out" has an answer that is not a list name.
After those six, the grid has far fewer "lost" cells, and the stack is honest about the ones that remain. That is usually enough for an agency running a handful of clients.
When the glue becomes the product
There is a point where the fixes stop being cheap. It is the point where you notice that the idempotency keys, the suppression re-imports, the reply write-backs and the event dates are not features of any tool you pay for. They are a sixth tool you built, and it is the one that holds every decision.
That is the honest case for consolidation, and it is narrower than the pitch usually makes it. The argument is not that one tool does everything better. Clay is a better workbench than anything a single-vendor product ships, and Sales Navigator is the professional graph. The argument is that the eight facts should live in one place, with one key, and the sending tools should read from it rather than each keeping a partial copy.
That is what CampaignStack is built around: one lead record with a stable member id, the source and the consent basis on the row, the ICP score as a number that is recomputed when the ICP changes, every touch on every channel in one ledger, a reply that stops every queued step for that person across channels, a workspace exclusion list that is re-checked at every send, retries that never send twice, and the entry reason stored as an event with its own timestamp so the opener can be told not to mention a like that is three weeks old. Clay, Apollo, HubSpot, Instantly, n8n, Zapier and Make all still connect to it, over an API key or a webhook, and the integration pages show the wiring. The difference is where the decisions live.
Run the audit first. If the grid comes back mostly intact, keep your stack. If it comes back mostly lost, the question is not which tool to add. It is where the facts should live.