Partial failures when an n8n workflow calls multiple APIs

Hello everyone I have a workflow that processes an order and calls several external APIs. The first two API calls succeed, but the third one fails.
The problem is that if I simply retry the entire workflow, the first two APIs are called again, which could create duplicate actions.
I’m currently tracking the workflow state with something like:return {
orderId: $json.orderId,
steps: {
payment: ‘completed’,
inventory: ‘completed’,
shipping: ‘pending’
}
};
What’s the best production pattern for handling partial failures in multi-step n8n workflows and Would you use checkpoints, sub-workflows, a database state table, or idempotency keys so the workflow can resume from the failed step instead of repeating successful operations?

Describe the problem/error/question

What is the error message (if any)?

Please share your workflow

(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)

Share the output returned by the last node

Information on your n8n setup

  • n8n version:
  • Database (default: SQLite):
  • n8n EXECUTIONS_PROCESS setting (default: own, main):
  • Running n8n via (Docker, npm, n8n cloud, desktop app):
  • Operating system:

Hi @Selena_Gloria
Both retry options replay the execution with the previous run’s data, so there is no resume-from-node in n8n and payment and inventory fire again every time. The state object you are returning cannot carry you across that, it lives inside the execution and dies with it.
Turn on Retry On Fail in the third node’s Settings and set Max Tries and Wait Between Tries. That reruns only that node inside the same execution, so a transient shipping failure never re-triggers the two calls that already succeeded.
For a failure that outlives those retries, set an error workflow under Options > Settings > Error workflow. The Error Trigger receives execution.lastNodeExecuted, the exact node that failed, alongside execution.id, so the recovery path branches on that name instead of you maintaining a step map by hand.

Hi there @Selena_Gloria

use a combination of checkpoints, sub-workflows, and idempotency keys.

Instead of restarting everything after a failure, store the status of each step:

return {
orderId: $json.orderId,
payment: 'completed',
inventory: 'completed',
shipping: 'pending'
};

Before each API call, check whether that step is already completed. If it is, skip it and continue from the next step.

For external APIs, use a stable idempotency key such as:

order-123-payment

I would also split major operations into sub-workflows:

ran into this exact thing before. retry on fail only helps if the execution is still alive, once it dies and you re-trigger, n8n has no memory that steps 1-2 already ran, that’s your duplicate issue.

fix is a status table keyed by order_id with pending/done/failed per step, and an IF before each api call to skip if already done. add idempotency keys too so even if the db and the actual call ever drift, the external api won’t double process. break the big steps into sub-workflows so it doesn’t turn into a mess of IFs on one canvas.

for recovery, hook an error trigger workflow off execution.lastNodeExecuted and just re-fire the parent with the same order_id, the status table handles skipping what’s done automatically.

So there is a few things to take into account here but @Mehdi_Belkassi covered pretty much everything

The main problem here, is we don’t know what the specific error is, so it’s hard to give you a full proof solution but Here’s how to systematically approach this.

  1. Set up a state trascking db
  2. Break down the workflow into sperate workflows
  3. Use a stable idempotency key
  4. Use retry on fail on the failing node
  5. This really depends on why is the node failing but here are a few things you can do
    5.a. If it’s a timing error, (ex: the shipping takes 5 min to swtich status after inventory is marked completed) then simply add a wait node somewhere
    5.b. Set an error database, super important
    5.c. For webhook trigger workflow you can simply append the payload to the error db, and a button to resend the payload, (aka, you press a button the workflow reruns, you don’t have to go through all the executions logs and open everything)
    5.d. depending on what the error root cause is you can use the error output path to handle it specifically

There is more but without a more defined error type it’s hard to give a more complete answer.

Thanks for the reply guys If an API call succeeds but n8n crashes before it can save the checkpoint, how would you confirm whether that step actually completed before retrying it? Would you rely on the API’s idempotency key, query the API to check whether the transaction already exists, or use a combination of both to make the workflow safely recoverable?

@Selena_Gloria use both an idempotency key and an API status check. You don’t want n8n to assume the request failed just because it crashed before saving the checkpoint.

For example, generate a stable idempotency key based on the transaction:

order-123-payment

If n8n crashes after the API call but before saving the checkpoint, the workflow can check the API first:

GET /payments/order-123

If the API confirms that the payment was already completed, n8n can safely save the checkpoint and continue.

If the payment doesn’t exist, n8n can retry the original request using the same idempotency key:order-123-payment

The recovery flow would basically be:

API call

n8n crashes

Workflow retries

Check API status

Already completed?
├── Yes → Save checkpoint → Continue
└── No → Retry API call with same idempotency key

@Selena_Gloria

I would do this

create durable operation intent
        |
        v
call API with stable idempotency key
        |
        +-- response received --> save completed result
        |
        +-- n8n crashes/timeouts
                  |
                  v
          query API for operation
             |
             +-- completed --> checkpoint success
             +-- pending   --> poll/reconcile
             +-- absent    --> retry same idempotency key
             +-- ambiguous --> stop and alert

Hi @Selena_Gloria ,

To prevent duplicate API calls during retries, there are two standard production patterns in n8n:

  1. Idempotency Keys: Pass a unique orderId in the request headers of your Payment and Inventory APIs. If n8n retries, the receiving APIs recognise the key and return the original success response without repeating the action.
  2. Database State Checkpoints: Store order progress in a database (like Postgres or Redis). Place an If node before each API call to check if that step was already completed. If yes, n8n skips it and jumps straight to the failed step.

Using Idempotency Keys alongside a DB status check is the most reliable way to make multi-step order workflows safe for retries.

Your payment → inventory → shipping example is a really good case where retrying an execution and retrying a business operation are two very different things.

One thing I’d check before deciding on the recovery strategy is whether each side-effecting step is independently idempotent. If payment succeeds and shipping fails, the workflow should be safe even if recovery itself gets triggered twice.

The checkpoint and the provider idempotency key protect different failure windows, so I would keep both—but make the durable row an operation intent, not only a completed flag written after the call.

For each side-effecting step, persist something like:

(order_id, step, operation_key UNIQUE, request_hash,
 state, provider_id, attempts, updated_at)

Then use this order:

  1. Insert/upsert state = prepared before the API call, with a stable operation_key such as order:123:payment.
  2. If the row is already confirmed, skip the call.
  3. For prepared or ambiguous, query the provider by idempotency key/external reference first.
  4. If the provider confirms the operation exists, store its ID and mark confirmed.
  5. Only if it is definitely absent, retry with the same operation key.
  6. A timeout, broken connection, or crash leaves the row ambiguous; it should not automatically become “safe to create again.”

That closes the specific gap in “call succeeds, n8n crashes before saving completed.” A checkpoint written only after success cannot close that window.

Also guard concurrent recovery runs: claim the row atomically (or use a short lease such as claimed_until) so two executions do not both reconcile and retry the same step.

The five crash-window tests I use for this pattern are written out here, including timeout-after-commit and crash-after-side-effect: