<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[VividAlgo]]></title><description><![CDATA[VividAlgo]]></description><link>https://vividalgo.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 27 Oct 2026 15:00:23 GMT</lastBuildDate><atom:link href="https://vividalgo.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Make Your No-Code AI Workflows Idempotent (So Retries Don’t Double-Send)
]]></title><description><![CDATA[Idempotency sounds like an enterprise buzzword. It is not. It means: if the same event shows up twice, your workflow’s side effects happen once. That…]]></description><link>https://vividalgo.hashnode.dev/make-your-no-code-ai-workflows-idempotent-so-retries-don-t-double-send</link><guid isPermaLink="true">https://vividalgo.hashnode.dev/make-your-no-code-ai-workflows-idempotent-so-retries-don-t-double-send</guid><category><![CDATA[automation]]></category><category><![CDATA[AI]]></category><category><![CDATA[No Code]]></category><category><![CDATA[webhooks]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[David Oscar]]></dc:creator><pubDate>Fri, 23 Oct 2026 07:24:08 GMT</pubDate><content:encoded><![CDATA[<p>Idempotency sounds like an enterprise buzzword.</p>
<p>It is not.</p>
<p>It means: <strong>if the same event shows up twice, your workflow’s side effects happen once.</strong></p>
<p>That matters the moment you add AI to a no-code stack.</p>
<p>Because AI steps are slow.
Slow steps miss webhook timeouts.
Missed timeouts cause retries.
Retries re-run the whole scenario.
And suddenly your “helpful” assistant sends two follow-ups, creates two CRM notes, or posts two Slack alerts for one invoice dispute.</p>
<p>This post is a practical pattern you can paste into Zapier, Make, or n8n without writing a full backend.
No prices.
No tool worship.
Just the reliability habit most demos skip.</p>
<h2>The failure mode in one story</h2>
<p>Imagine a booking form posts to your webhook.</p>
<p>Your scenario:</p>
<ol>
<li>Receives the payload</li>
<li>Asks an AI model to draft a confirmation email</li>
<li>Creates a CRM contact</li>
<li>Sends the email</li>
</ol>
<p>Step 2 takes 12 seconds.
The sender’s webhook client times out at 10 seconds and retries.
Your platform starts a <strong>second</strong> run with the same payload.</p>
<p>Result:</p>
<ul>
<li>Two contacts (or a messy upsert race)</li>
<li>Two emails</li>
<li>One confused customer</li>
</ul>
<p>Nothing “AI-specific” caused this.
AI just made the window for retries wider.</p>
<h2>What the platforms actually guarantee (verified)</h2>
<p>Do not assume the automation tool will dedupe for you.</p>
<p><strong>Zapier (official help):</strong></p>
<ul>
<li><strong>Polling triggers</strong> compare each item’s unique ID to IDs seen before and only run on new IDs (<a href="https://help.zapier.com/hc/en-us/articles/8496260269965-How-Zapier-handles-duplicate-data-in-Zap-workflows">How Zapier handles duplicate data</a>).</li>
<li><strong>Instant triggers do not use that deduplication</strong>, because Zapier’s docs treat them as “apps only send new data.”</li>
<li><strong>Actions</strong> follow the destination app: some create duplicates, some error, some update, some silently ignore.</li>
</ul>
<p>Translation for builders: if your Zap is webhook/instant-driven and then creates records or sends messages, <strong>you still need your own duplicate gate</strong> for side effects.</p>
<p><strong>Make (official webhook docs):</strong></p>
<ul>
<li>Instant webhooks process requests as they arrive.</li>
<li>Default is <strong>parallel</strong> execution; “Process data in order” makes Make wait for the previous run before starting the next (<a href="https://help.make.com/webhooks">Make webhooks help</a>).</li>
<li>Ordered processing reduces races. It does <strong>not</strong> by itself mean “same event ID runs once.”</li>
<li>Make’s own support guidance in the wild for duplicate webhook deliveries is: enable ordered processing, <strong>search a Data Store for the ID</strong>, and only continue if it is new (documented in vendor/community threads such as Square’s webhook duplicate discussion summarizing Make support’s steps).</li>
</ul>
<p><strong>n8n:</strong></p>
<ul>
<li>No magic “ignore retries” switch on a generic Webhook node.</li>
<li>Official-style templates show the durable pattern: validate payload → check a Data Table for <code>eventId</code> → claim/process → respond deterministically on duplicates (<a href="https://n8n.io/workflows/20364-handle-reliable-webhooks-with-http-request-and-data-table-nodes/">Handle reliable webhooks with HTTP Request and Data Table nodes</a>).</li>
</ul>
<h2>The pattern: Claim before you mutate</h2>
<p>Treat every customer-facing or write action as forbidden until the event is <strong>claimed</strong>.</p>
<h3>Step A — Extract a stable event ID</h3>
<p>Prefer IDs the source system already owns:</p>
<ul>
<li>Stripe: <code>event.id</code> (evt_…)</li>
<li>Help desk: ticket id + “created” or message id</li>
<li>Forms: submission id</li>
<li>Email: <code>Message-ID</code> header when available</li>
</ul>
<p>If the source sends no ID, create one <strong>deterministically</strong> from immutable fields, for example:</p>
<p><code>sha256(source + customer_email + submitted_at + amount)</code></p>
<p>Do <strong>not</strong> use <code>now()</code> or a random UUID as your only key if retries must collapse to one run.</p>
<h3>Step B — Claim the ID first (before AI)</h3>
<p>Write the claim <strong>before</strong> the slow AI step.</p>
<p>Minimum columns:</p>
<ul>
<li><code>event_id</code> (unique)</li>
<li><code>status</code> (<code>claimed</code> | <code>succeeded</code> | <code>failed</code>)</li>
<li><code>claimed_at</code></li>
<li><code>workflow_name</code></li>
<li><code>last_error</code> (optional)</li>
</ul>
<p>Logic:</p>
<ol>
<li>Try to insert <code>event_id</code> with status <code>claimed</code>.</li>
<li>If insert fails because it already exists → <strong>stop</strong> (or return “already processed”).</li>
<li>If insert succeeds → continue to AI + actions.</li>
<li>On success → set status <code>succeeded</code>.</li>
<li>On terminal failure → set status <code>failed</code> (and decide whether a human can replay).</li>
</ol>
<p>This is the same idea as <code>INSERT … ON CONFLICT DO NOTHING</code> in SQL, expressed with:</p>
<ul>
<li>Zapier: Storage / Tables / a small database app with a unique key</li>
<li>Make: Data Store search → filter “not found” → add record</li>
<li>n8n: Data Table or Postgres unique constraint</li>
</ul>
<h3>Step C — Make side effects safe even if claim races</h3>
<p>If two runs start before either write lands (rare but real under parallel execution):</p>
<ul>
<li>Prefer a store with a <strong>unique constraint</strong> and treat conflict as “I lost the race.”</li>
<li>Or turn on Make’s <strong>Process data in order</strong> for that scenario so runs serialize (<a href="https://help.make.com/webhooks">Make webhooks help</a>).</li>
<li>Keep AI drafts in a draft state (CRM note, Gmail draft) until a human or a second gate sends.</li>
</ul>
<p>Zapier’s trustworthy-agent guidance matches this instinct: separate draft from irreversible send, and require approval for customer-facing and hard-to-reverse actions (<a href="https://zapier.com/blog/safe-trustworthy-ai-agents/">safe and trustworthy AI agents</a>).</p>
<h2>Worked example: invoice dispute intake</h2>
<p><strong>Canonical payload</strong></p>
<pre><code class="language-json">{
  "event_id": "stripe_evt_1Qdispute_88421",
  "type": "invoice.dispute_opened",
  "customer_email": "alex@example.com",
  "invoice_id": "in_88421",
  "amount_cents": 18900,
  "currency": "usd",
  "received_at": "2026-10-22T11:02:04Z"
}
</code></pre>
<p><strong>Scenario sketch</strong></p>
<ol>
<li>Webhook received</li>
<li>Normalize fields (do not let the model invent <code>invoice_id</code>)</li>
<li><strong>Claim</strong> <code>event_id</code> in Data Store / Table</li>
<li>If duplicate → respond 200 “duplicate” and exit</li>
<li>AI step: produce JSON <code>{summary, suggested_internal_note, risk: low|medium|high}</code> with low temperature</li>
<li>Validate JSON schema in a Code / Tools step</li>
<li>Create internal ticket / CRM note with the draft</li>
<li>If <code>risk</code> is high → Slack human; <strong>do not</strong> email the customer automatically</li>
<li>Mark claim <code>succeeded</code></li>
</ol>
<p>Notice what is missing on purpose: auto-send to the customer on first build.</p>
<h2>Zapier Tables claim recipe (no custom server)</h2>
<p>If you are on Zapier and want a concrete claim step:</p>
<ol>
<li>Create a Table <code>webhook_claims</code> with a text field <code>event_id</code> (treat it as unique in practice) plus <code>status</code>, <code>claimed_at</code>, <code>zap_name</code>.</li>
<li>Right after the trigger, <strong>Find Record</strong> where <code>event_id</code> equals your extracted id.</li>
<li>Path A — record found and <code>status</code> is <code>succeeded</code> or <code>claimed</code>: stop (or respond duplicate).</li>
<li>Path B — no record: <strong>Create Record</strong> with <code>claimed</code>, then continue to AI.</li>
<li>After successful side effects: <strong>Update Record</strong> to <code>succeeded</code>.</li>
<li>After terminal failure: <code>failed</code>, and decide whether a human replay Zap may run.</li>
</ol>
<p>Remember Zapier’s own distinction: polling triggers already dedupe by unique id; instant triggers do not get that safety net (<a href="https://help.zapier.com/hc/en-us/articles/8496260269965-How-Zapier-handles-duplicate-data-in-Zap-workflows">duplicate data help</a>). Your Table is the safety net.</p>
<p>Also remember actions may still create duplicate CRM rows if the destination app allows it — claiming prevents <em>your</em> Zap from trying twice; it does not rewrite another app’s API semantics.</p>
<h2>Make Data Store claim recipe</h2>
<ol>
<li>Scenario setting: enable <strong>Process data in order</strong> when retries overlap (<a href="https://help.make.com/webhooks">webhooks help</a>).</li>
<li>After the webhook: <strong>Data Store → Search records</strong> for <code>event_id</code>.</li>
<li>Filter: continue only when search returns zero bundles.</li>
<li><strong>Add a record</strong> with <code>event_id</code> + <code>claimed</code> <em>before</em> the AI module.</li>
<li>AI → validate → mutate → update record to <code>succeeded</code>.</li>
</ol>
<p>This mirrors what Make support has recommended when external systems appear to deliver duplicates under load: search, filter, store, then proceed.</p>
<h2>AI makes idempotency harder — plan for it</h2>
<p>Three AI-specific twists:</p>
<ol>
<li><strong>Latency:</strong> models expand the retry window. Claim <em>before</em> the model call.</li>
<li><strong>Non-determinism:</strong> the second run may draft different text. That is fine if you never send twice; store <code>prompt_version</code> so you can explain score changes later.</li>
<li><strong>Partial success:</strong> AI succeeds, email send fails, retry arrives. Your claim row must distinguish <code>claimed</code> vs <code>succeeded</code>, or a naive “ID exists → skip” will block a legitimate replay after a true failure.</li>
</ol>
<p>A practical policy:</p>
<ul>
<li>Duplicate while <code>succeeded</code> → no-op</li>
<li>Duplicate while <code>claimed</code> and younger than N minutes → treat as in-flight (no-op or wait)</li>
<li>Duplicate while <code>failed</code> → allow a <strong>manual</strong> replay path only</li>
</ul>
<h2>Test plan you can run in an afternoon</h2>
<ol>
<li>Send the same webhook payload twice in 2 seconds.</li>
<li>Confirm one claim row and one customer-facing action.</li>
<li>Force the AI step to take &gt; sender timeout (or mock a delay) and confirm retries do not double-send.</li>
<li>Kill the run after AI but before send; confirm status stays <code>claimed</code>/<code>failed</code> and your replay rules behave.</li>
<li>Change the prompt; confirm logs record <code>prompt_v2</code> without resetting event IDs.</li>
</ol>
<p>If step 1 already double-sends, do not add more AI features yet.</p>
<h2>When <em>not</em> to obsess</h2>
<ul>
<li>Pure internal logging with no customer impact</li>
<li>Reads-only enrichment that is cheap and harmless to repeat</li>
<li>Platforms where the trigger is polling with a stable unique ID <em>and</em> your actions are naturally upserts</li>
</ul>
<p>Even then, once money, inbox, or deletes are involved, claim first.</p>
<h2>Takeaways</h2>
<ul>
<li>Idempotency = same event, one set of side effects.</li>
<li>Official docs: Zapier dedupes <strong>polling</strong> triggers by ID; <strong>instant</strong> triggers are not covered by that mechanism; Make parallel webhooks need your own gate; n8n expects you to build it.</li>
<li>Claim the event ID <strong>before</strong> slow AI steps.</li>
<li>Separate drafts from sends.</li>
<li>Test double-delivery on purpose.</li>
</ul>
<p>Reliability is not the opposite of shipping fast.
It is how your automation still looks smart on a bad network day.</p>
<p>If you are also thinking through the trust and access side of wiring AI into your stack, I wrote a plain-English companion on <a href="https://vividalgo.com/is-it-safe-to-connect-your-business-apps-to-ai-automation-tools">whether it is safe to connect your business apps to AI automation tools</a>.</p>
]]></content:encoded></item></channel></rss>