The scenario goes like this: every execution completes, the circle turns green, no error fires. Then you check the target system — the CRM, the database, the external API — and the data isn't there. Not partially there. Nowhere. A discussion in the n8n community recently documented this with around 19,000 write operations from a multi-agent setup that all returned success codes but never appeared in the destination. Every run looked clean.
This is a different problem from silent AI output errors, where the model returns a plausible but semantically wrong answer. That's a content failure. This one is structural: the write itself isn't landing, and n8n's error handler has no way to know because no error was thrown.
Why a 200 doesn't guarantee anything
Most external APIs return 200 OK as soon as they've accepted a request. Accepted isn't written. A platform can queue the operation, process it asynchronously, hit a constraint in its own database, and silently discard the write — while n8n's execution is already stamped complete.
It comes up most with CRMs that deduplicate or merge contacts before committing, webhook endpoints that respond immediately and process in a background job, and any API under load that acknowledges receipt without confirming the write landed. The API isn't misbehaving. It's doing exactly what the HTTP spec allows. The 200 means "I received this." Nothing more.
A 200 is a postal receipt, not a delivery confirmation.
For most automations running low volume, the failure rate is small enough that nobody notices. The problem is that you're not measuring it, so you have no idea what "small enough" actually is in your case.
The inline read-back problem
The obvious response is to read the record back right after writing it. Check what's in the target. If the field doesn't match what you sent, throw an error. That catches the synchronous failure case and it's worth having.
But it doubles your API calls on every single write. If your write failure rate is 0.2%, you're paying the full read-back cost on the other 99.8% to catch it. At meaningful volume, that's real money.
The worse issue is async writes. If the target system processes your request in a background job — common in CRMs and most third-party platforms — your immediate read-back queries the record before the write commits. Old value, check passes, you move on. Still broken. The inline check didn't help.
Read-back is a reasonable layer. On its own, it doesn't close the gap.
A better pattern: the correlation ID
Generate a unique ID before each write and attach it to the record. This is the core of the pattern.
In a Code node before your write step:
const correlationId = `wf-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
// example: "wf-1723456789123-a3f9k"
Write it into any field the target API accepts — a custom field, a note, a description, an external_id. Whatever the platform exposes. Then, right after the write node, store a ledger entry:
// push to your tracking table
return [{
correlationId,
target: 'hubspot_contact',
recordId: $json.id,
expectedAt: new Date().toISOString(),
resolved: false
}];
The ledger can live anywhere queryable: a PostgreSQL table, a Google Sheet, Airtable, a Notion database. One row per write. It adds a single Code node and one append-to-table step to your workflow, and the per-run cost is negligible.
The reconciliation workflow
A second n8n workflow runs on a schedule — every 5 minutes for most setups, every 15 for lighter volume. It pulls unresolved ledger entries older than a grace window. Two to three minutes usually works, long enough for async writes to finish but short enough to catch failures in the same duty cycle.
For each unresolved entry, the workflow queries the target by record ID and checks whether the correlation ID field is present. Match: mark it resolved. No match: alert. Slack, email, PagerDuty — whatever your team actually reads at 2am.
Set the grace window carefully. Too short and you'll alert on in-flight writes that haven't committed yet. Too long and real failures drift past a full cycle undetected.
Once this runs for a week, you have a number: write failures per workflow per day. For most businesses it comes back near zero. But you know that instead of assuming it.
This is the kind of reliability layer that separates a workflow you trust from one you quietly check on every Monday morning.
The agent case is worse
In a deterministic n8n workflow, every step is explicit. In an agentic workflow — where Claude or another model calls tools and decides what to do next — the agent interprets tool results as text. If a write tool returns "the contact has been updated," the agent reads it as confirmation and continues. It has no idea whether the write landed.
The first fix is enforcing that every write tool returns a machine-readable receipt, not prose.
"Contact updated successfully." is not a receipt. {"contactId": "hs-1234", "correlationId": "wf-abc"} is.
If the tool result contains no record ID, treat it as a failure. Retry the write or escalate to a human — don't continue. This catches the most obvious gap without adding any infrastructure.
The reconciliation workflow still runs behind this. Between a receipt-enforcing tool and async verification against a write ledger, you're catching failures both inline and after the fact. Neither alone is enough for a production agentic workflow that touches data you'd notice going missing.
For any write-heavy automation we build and maintain, the correlation ID ledger is a default — something we put in at build time, not after discovering a problem months in. If you're running n8n workflows that push data to external systems and you're not sure whether they're verifying or just trusting the 200, that's worth a quick conversation.
— Cole
Sources
- n8n Community, "How do you verify that an action actually landed in the target system?": community.n8n.io/t/307185