Idempotency
If you trigger a task with the same idempotencyKey twice, the second request returns the original run's handle instead of creating a new run.
Why Use Idempotency Keys?
Most common use case: Preventing duplicate child tasks when a parent task retries.
Other use cases:
- Preventing duplicate emails
- Avoiding double-charging customers
- One-time setup or migration tasks
- Deduplicating webhook processing
Basic Usage
ts
import { idempotencyKeys, task } from "@trigger.dev/sdk";
export const myTask = task({
id: "my-task",
retry: { maxAttempts: 4 },
run: async (payload: any) => {
// Scoped to this run — childTask only triggers ONCE across all retries
const idempotencyKey = await idempotencyKeys.create("my-task-key");
await childTask.trigger({ foo: "bar" }, { idempotencyKey });
throw new Error("Retry me"); // retries won't re-trigger child
},
});Scopes
| Scope | Hash input | Use case |
|---|---|---|
"run" (default) | key + parentRunId | Prevent duplicates within one parent run |
"attempt" | key + parentRunId + attemptNumber | Re-run child on each parent retry |
"global" | key only | Ensure a task runs only once ever |
run scope (default)
ts
// Same key in different parent runs = different hashes = both run
const key = await idempotencyKeys.create(`send-confirmation-${payload.orderId}`);attempt scope
ts
// fetchLatestData re-runs on each parent retry to get fresh data
const key = await idempotencyKeys.create(`fetch-${payload.userId}`, {
scope: "attempt",
});global scope
ts
// Welcome email only sent once per user, regardless of how many times triggered
const key = await idempotencyKeys.create(`welcome-email-${payload.userId}`, {
scope: "global",
});TTL for Idempotency Keys
ts
// Key expires after 60 seconds — next trigger creates a new run
await childTask.trigger(
{ foo: "bar" },
{ idempotencyKey, idempotencyKeyTTL: "60s" }
);Supported units: s, m, h, d. Default TTL: 30 days.
Failed Runs
When a run fails, its idempotency key is cleared — the next trigger creates a fresh run. Successful and canceled runs retain their key.
Resetting Keys
ts
import { idempotencyKeys } from "@trigger.dev/sdk";
// Reset a global key
await idempotencyKeys.reset("my-task", "my-key", { scope: "global" });
// Reset a run-scoped key from outside a task
await idempotencyKeys.reset("my-task", "my-key", {
scope: "run",
parentRunId: "run_abc123",
});You can also reset from the dashboard: navigate to a run → find the "Idempotency key" section → click Reset.
Payload-Based Idempotency
ts
import { createHash } from "node:crypto";
function hash(payload: any): string {
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
}
const idempotencyKey = await idempotencyKeys.create(hash(childPayload));
await tasks.trigger("child-task", payload, { idempotencyKey });Breaking Change in v4.3.1
In v4.3.0 and earlier, raw strings defaulted to global scope. From v4.3.1, raw strings default to run scope.
ts
// Before v4.3.1 — was global scope
await childTask.trigger(payload, { idempotencyKey: "my-key" });
// After v4.3.1 — explicitly use global if needed
const key = await idempotencyKeys.create("my-key", { scope: "global" });
await childTask.trigger(payload, { idempotencyKey: key });