workflow
Version:
Workflow SDK - Build durable, resilient, and observable workflows
96 lines (73 loc) • 5.06 kB
text/mdx
---
title: Idempotency
description: Make step retries safe and coordinate duplicate workflow starts with hook tokens.
type: guide
summary: Use step IDs for retry-safe external calls, and use deterministic hook tokens when duplicate requests must route to one active workflow.
---
<CopyPrompt
text="Make this workflow's side effects idempotent. For retry-safe external calls, read the deterministic step ID inside the "use step" function with `getStepMetadata()` from `workflow` and pass `stepId` as the idempotency key to the external API (for example Stripe's `Idempotency-Key` header) so step retries deduplicate. For duplicate workflow starts, derive a deterministic hook token from the domain key (for example `order:${orderId}`): in the API route, look up the active hook with `getHookByToken(token)` from `workflow/api` (catching `HookNotFoundError` from `workflow/errors`) and reuse its `runId`, otherwise call `start(...)`; inside the workflow, create the hook with the same token and check `await hook.getConflict()` before duplicate-sensitive work. Verify retried steps deduplicate, duplicate starts reuse the active run, and conflicts are handled."
/>
Use idempotency when a retry or duplicate request should not repeat the underlying work. In Workflow, there are two common patterns: use the step ID for retry-safe external calls, and use hook tokens to coordinate duplicate workflow starts.
- A step charges a payment, sends an email, enqueues work, or creates an external record.
- A route may receive duplicate requests that should map to one active workflow run.
Every step has a unique, deterministic `stepId` available via `getStepMetadata()`. Pass this as the idempotency key to external APIs:
```typescript
import { getStepMetadata } from "workflow";
export async function createCharge(
customerId: string,
amount: number
): Promise<{ id: string }> {
"use step";
const { stepId } = getStepMetadata(); // [!code highlight]
// Stripe uses the idempotency key to deduplicate requests.
// If this step is retried, Stripe returns the same charge.
const charge = await fetch("https://api.stripe.com/v1/charges", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
"Idempotency-Key": stepId, // [!code highlight]
},
body: new URLSearchParams({
amount: String(amount),
currency: "usd",
customer: customerId,
}),
});
if (!charge.ok) {
const error = await charge.json();
throw new Error(`Charge failed: ${error.message}`);
}
return charge.json();
}
```
See [Step Idempotency](/docs/foundations/idempotency
For duplicate workflow-start requests, derive a hook token from your domain key. You can avoid obvious duplicate starts by checking whether an active hook already owns that token before calling `start()`:
```typescript
import { getHookByToken, start } from "workflow/api";
import { HookNotFoundError } from "workflow/errors";
import { processOrder } from "./workflows/process-order";
export async function POST(request: Request) {
const { orderId } = await request.json();
const token = `order:${orderId}`;
try {
const hook = await getHookByToken(token); // [!code highlight]
return Response.json({ runId: hook.runId, reused: true });
} catch (error) {
if (!HookNotFoundError.is(error)) throw error;
}
const run = await start(processOrder, [orderId]); // [!code highlight]
return Response.json({ runId: run.runId, reused: false });
}
```
The workflow should create the deterministic hook and check `await hook.getConflict()` before duplicate-sensitive work: awaiting `getConflict()` suspends the workflow to commit the hook registration and resolves with the conflicting run when another active run already owns the token (or `null` once the hook is registered). See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for the full pattern, including how to steer an active run with `resumeHook()` and how to handle the current race between `start()` and hook registration.
## Key APIs
- [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions): Declares the orchestrator function.
- [`"use step"`](/docs/foundations/workflows-and-steps#step-functions): Declares step functions with full Node.js access.
- [`getStepMetadata()`](/docs/api-reference/workflow/get-step-metadata): Provides the deterministic `stepId` for idempotency keys.
- [`createHook()`](/docs/api-reference/workflow/create-hook): Creates a hook with an optional deterministic token.
- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token): Finds the hook that owns a token.
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Resumes the active hook when the duplicate request carries data.
- [`start()`](/docs/api-reference/workflow-api/start): Starts a new workflow run.