Skip to content
Blog

· [Founder name]

Why we don't blindly retry non-idempotent actions

A timed-out SMS or payment call may have gone through. How OutcomeOS uses idempotency keys and holds unsafe retries for a human.

Base URL: https://outcomeos.cloud. Competitor documentation quoted from pages checked 2026-11-10.

Retries are the first thing anyone adds to an automation. A call fails, you try again, it works. Most of the time that's exactly right.

But there's one kind of failure where retrying is the wrong move, and it's the one that hurts customers most: the call that failed from your side but succeeded on theirs.

The timeout problem

Your workflow sends an SMS. It makes an HTTP POST to the provider. Then one of these happens:

  • The request reaches the provider, the provider sends the SMS, and the response is lost on the way back.
  • The provider takes longer than your timeout to respond, then succeeds anyway.
  • The connection drops after the provider has started processing.

From your workflow's point of view, all three look the same: a timeout or a connection error. From your customer's point of view, they already have the message.

Now your retry fires. They get it twice. If the step was "charge the card", they're charged twice. If it was "create invoice", there are two invoices.

This is the difference between idempotent and non-idempotent actions:

  • Idempotent: doing it twice has the same effect as doing it once. A GET. A PUT that sets a field to a value. A DELETE of a specific record.
  • Non-idempotent: doing it twice does it twice. Most POSTs. Sending a message. Charging a payment. Appending a row.

A retry policy that doesn't know the difference will, eventually, double-send.

How retries usually work

Mainstream tools have solid retry machinery, and it's worth being precise about it.

Make automatically retries incomplete executions caused by RateLimitError, ConnectionError and ModuleTimeoutError, on an exponential backoff schedule, and "runs the scenario again, starting with the module that caused the error" (Make help). Incomplete executions are off by default and you enable them per scenario (Make help).

Zapier's autoreplay "will automatically replay any Zap run with an errored status", and you can set each Zap to "Always replay" or "Never replay" (Zapier help).

n8n lists automatic retries on every plan (n8n pricing).

These are reasonable designs, and you can build safe flows on all of them. The choice they give you is per error type, or per workflow. What we wanted was a choice made per action, based on whether that action can safely run twice.

What OutcomeOS does

Every step in an OutcomeOS workflow has its own retry policy, and every step has an idempotency key.

1. Retries are per step, with exponential backoff. A flaky GET can retry 5 times. A payment call can retry 0 times. You don't pick one setting for the whole workflow.

2. Steps carry idempotency keys. A retry of a step reuses that step's key. When the provider supports idempotency keys, a retry with the same key is safe: the provider recognises it and returns the original result instead of doing the work again.

3. Non-idempotent calls without a key are not retried blindly. If a step is a POST and there's no idempotency key the provider will honour, and the failure is ambiguous (a timeout, a dropped connection), OutcomeOS does not retry it. The step goes to Requires attention. The execution log shows what was sent, what came back, and why it stopped.

A person then decides. Check the provider. If the SMS went out, mark it and continue. If it didn't, retry it by hand.

4. Duplicate triggers are dropped. If the same webhook arrives twice, which happens more than you'd think, the second one doesn't start a second run.

5. Replay starts from the failure. When you replay a failed execution, steps that already succeeded keep their outputs and don't run again. Only the failed step and what comes after it run.

That last point matters for the same reason. Zapier's docs note that when you replay an entire Zap run, "the Zap will replay every single step … regardless of their run status", and successful steps count toward task usage again (Zapier also offers replaying only the errored steps). OutcomeOS's "replay from failure" doesn't re-run steps that already succeeded.

Why "requires attention" isn't a failure

It can feel like a step backwards to stop and ask a human. Isn't automation supposed to remove that?

Think about the cost on each side:

  • Holding the step: someone spends two minutes checking a provider dashboard. Annoying.
  • Retrying blindly: a customer gets two texts, or two charges. Then someone spends twenty minutes on a refund and an apology. The customer remembers.

For the narrow class of ambiguous failures on non-idempotent actions, holding is cheaper. For everything else (rate limits on reads, 503s on idempotent calls, steps with a provider-honoured idempotency key) OutcomeOS retries automatically with backoff, like you'd expect.

And because "Requires attention" is its own execution state, it's a queue, not a buried log line. You see how many are waiting and why. Executions that exhaust their retries go to a dead-letter queue, so nothing silently disappears.

How this connects to outcomes

This isn't just about tidiness. It's about the number you trust.

OutcomeOS marks an outcome achieved only from verified events, never because a workflow finished. (That's the subject of the companion post.) Safe retries are the other half. If the system double-sends, a "reply received" might be a reply to the second message, and your timing numbers are wrong. If it silently drops, an outcome stays in progress with no explanation.

Holding ambiguous failures keeps both honest: every message sent is one you meant to send, and every step that didn't finish has a visible reason.

Practical advice, whatever tool you use

  • Know which steps are non-idempotent. Mark them. Sends, charges, creates.
  • Use provider idempotency keys where they exist. Stripe, for example, supports them on POST requests. Pass a key derived from the run and the step.
  • Don't retry ambiguous failures on non-idempotent steps without a key. Alert instead.
  • Replay from the failure, not from the top, unless you're sure every earlier step is safe to repeat.
  • Make "stuck" visible. A queue of items that need a human is better than a log nobody reads.

Honest limits

OutcomeOS is new. It has 2 integrations available (signed webhooks, generic HTTP) and 3 in beta (Twilio SMS, HubSpot contacts, Stripe account check). The retry rules above apply to all of them, but the long tail of apps you might want isn't there yet.

If that's fine for your use case, Starter is free and the code is MIT: sign up, or see how we compare, with sources.

One short letter when we ship something worth switching for.

Double opt-in. Unsubscribe from any issue in one click.