Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when implementing delays that must survive process restarts (e.g., 24-hour cart abandonment, scheduled follow-ups), waiting for human approval or external events with timeouts (review gates, webhook callbacks, async API completion), polling external services without losing state on crashes, calling other functions and awaiting their results, memoizing expensive operations so they don't re-run on retry, or running async work in parallel inside a workflow. Covers Inngest step methods: step.run
.claude/skills/asymmetric-al-inngest-steps/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 40% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 105% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 133% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 45% | 0% |
Build robust, durable workflows with Inngest's step methods. Each step is a separate HTTP request that can be independently retried and monitored.
> These skills are focused on TypeScript. For Python or Go, refer to the Inngest documentation for language-specific guidance. Core concepts apply across all languages.
🔄 Critical: Each step re-runs your function from the beginning. Put ALL non-deterministic code (API calls, DB queries, randomness) inside steps, never outside.
📊 Step Limits: Every function has a maximum of 1,000 steps and 4MB total step data.
typescript// ❌ WRONG - will run 4 times export default inngest.createFunction( { id: "bad-example", triggers: [{ event: "test" }] }, async ({ step }) => { console.log("This logs 4 times!"); // Outside step = bad await step.run("a", () => console.log("a")); await step.run("b", () => console.log("b")); await step.run("c", () => console.log("c")); }, ); // ✅ CORRECT - logs once each export default inngest.createFunction( { id: "good-example", triggers: [{ event: "test" }] }, async ({ step }) => { await step.run("log-hello", () => console.log("hello")); await step.run("a", () => console.log("a")); await step.run("b", () => console.log("b")); await step.run("c", () => console.log("c")); }, );
Execute retriable code as a step. Each step ID can be reused - Inngest automatically handles counters.
typescript// Basic usage const result = await step.run("fetch-user", async () => { const user = await db.user.findById(userId); return user; // Always return useful data }); // Synchronous code works too const transformed = await step.run("transform-data", () => { return processData(result); }); // Side effects (no return needed) await step.run("send-notification", async () => { await sendEmail(user.email, "Welcome!"); });
✅ DO:
❌ DON'T:
Pause execution without using compute time.
typescript// Duration strings await step.sleep("wait-24h", "24h"); await step.sleep("short-delay", "30s"); await step.sleep("weekly-pause", "7d"); // Use in workflows await step.run("send-welcome", () => sendEmail(email)); await step.sleep("wait-for-engagement", "3d"); await step.run("send-followup", () => sendFollowupEmail(email));
Sleep until a specific datetime.
typescriptconst reminderDate = new Date("2024-12-25T09:00:00Z"); await step.sleepUntil("wait-for-christmas", reminderDate); // From event data const scheduledTime = new Date(event.data.remind_at); await step.sleepUntil("wait-for-scheduled-time", scheduledTime);
🚨 CRITICAL: waitForEvent ONLY catches events sent AFTER this step executes.
null return (means timeout, event never arrived)typescript// Basic event waiting with timeout const approval = await step.waitForEvent("wait-for-approval", { event: "app/invoice.approved", timeout: "7d", match: "data.invoiceId", // Simple matching }); // Expression-based matching (CEL syntax) const subscription = await step.waitForEvent("wait-for-subscription", { event: "app/subscription.created", timeout: "30d", if: "event.data.userId == async.data.userId && async.data.plan == 'pro'", }); // Handle timeout if (!approval) { await step.run("handle-timeout", () => { // Approval never came return notifyAccountingTeam(); }); }
✅ DO:
❌ DON'T:
In expressions, event = the original triggering event, async = the new event being matched. See Expression Syntax Reference for full syntax, operators, and patterns.
Wait for unique signals (not events). Better for 1:1 matching.
typescriptconst taskId = "task-" + crypto.randomUUID(); const signal = await step.waitForSignal("wait-for-task-completion", { signal: taskId, timeout: "1h", onConflict: "replace", // Required: "replace" overwrites pending signal, "fail" throws an error }); // Send signal elsewhere via Inngest API or SDK // POST /v1/events with signal matching taskId
When to use:
Fan out to other functions without waiting for results.
typescript// Trigger other functions await step.sendEvent("notify-systems", { name: "user/profile.updated", data: { userId: user.id, changes: profileChanges }, }); // Multiple events at once await step.sendEvent("batch-notifications", [ { name: "billing/invoice.created", data: { invoiceId } }, { name: "email/invoice.send", data: { email: user.email, invoiceId } }, ]);
Use when: You want to trigger other functions but don't need their results in the current function.
Call other functions and handle their results. Perfect for composition.
typescriptconst computeSquare = inngest.createFunction( { id: "compute-square", triggers: [{ event: "calculate/square" }] }, async ({ event }) => { return { result: event.data.number * event.data.number }; }, ); // Invoke and use result const square = await step.invoke("get-square", { function: computeSquare, data: { number: 4 }, }); console.log(square.result); // 16, fully typed! // For cross-app invocation (when you can't import the function directly): import { referenceFunction } from "inngest"; const externalFn = referenceFunction({ appId: "other-app", functionId: "other-fn", }); const result = await step.invoke("call-external", { function: externalFn, data: { key: "value" }, });
Warning: v4 Breaking Change: String function IDs (e.g., function: "my-app-other-fn") are no longer supported in step.invoke(). Use an imported function reference or referenceFunction() for cross-app calls.
Great for:
Reuse step IDs - Inngest handles counters automatically.
typescriptconst allProducts = []; let cursor = null; let hasMore = true; while (hasMore) { // Same ID "fetch-page" reused - counters handled automatically const page = await step.run("fetch-page", async () => { return shopify.products.list({ cursor, limit: 50 }); }); allProducts.push(...page.products); if (page.products.length < 50) { hasMore = false; } else { cursor = page.products[49].id; } } await step.run("process-products", () => { return processAllProducts(allProducts); });
Use Promise.all for parallel steps. In v4, parallel step execution is optimized by default
typescript// Create steps without awaiting const sendEmail = step.run("send-email", async () => { return await sendWelcomeEmail(user.email); }); const updateCRM = step.run("update-crm", async () => { return await crmService.addUser(user); }); const createSubscription = step.run("create-subscription", async () => { return await subscriptionService.create(user.id); }); // Run all in parallel const [emailId, crmRecord, subscription] = await Promise.all([ sendEmail, updateCRM, createSubscription, ]); // Parallel steps are optimized by default in v4 export default inngest.createFunction( { id: "parallel-heavy-function", triggers: [{ event: "process/batch" }], }, async ({ event, step }) => { const results = await Promise.all( event.data.items.map((item, i) => step.run(`process-item-${i}`, () => processItem(item)), ), ); }, ); // ⚠️ Promise.race() behavior with v4's optimized parallelism: // All promises settle before race resolves. Use group.parallel() for true race: const winner = await group.parallel(async () => { return Promise.race([ step.run("fast-service", () => callFastService()), step.run("slow-service", () => callSlowService()), ]); }); // To disable optimized parallelism if needed: // At the client level: new Inngest({ id: "app", optimizeParallelism: false }) // At the function level: { id: "fn", optimizeParallelism: false, triggers: [...] }
See inngest-flow-control for concurrency and throttling options.
Perfect for batch processing with parallel steps.
typescriptexport default inngest.createFunction( { id: "process-large-dataset", triggers: [{ event: "data/process.large" }] }, async ({ event, step }) => { const chunks = chunkArray(event.data.items, 10); // Process chunks in parallel const results = await Promise.all( chunks.map((chunk, index) => step.run(`process-chunk-${index}`, () => processChunk(chunk)), ), ); // Combine results await step.run("combine-results", () => { return aggregateResults(results); }); }, );
🔄 Function Re-execution: Code outside steps runs on every step execution ⏰ Event Timing: waitForEvent only catches events sent AFTER the step runs 🔢 Step Limits: Max 1,000 steps per function, 4MB per step output, 32MB per function run in total 📨 HTTP Requests: Checkpointing is enabled by default in v4, reducing HTTP overhead. For serverless platforms, configure maxRuntime on the client 🔁 Step IDs: Can be reused in loops - Inngest handles counters ⚡ Parallelism: Use Promise.all for parallel steps (optimized by default in v4). Note that Promise.race() waits for all promises to settle — use group.parallel() for true race semantics
Remember: Steps make your functions durable, observable, and debuggable. Embrace them!
These upstream Inngest instructions are vendored for agent tooling and integration work in this monorepo.
Use this skill when inngest-steps matches the current Inngest task. If the right skill is unclear, start with docs/ai/skills/inngest/SKILL.md.
integration.
inngest-brownfield-audit before changing existing app workflows orfragile background work.
AGENTS.md, reporulebooks, framework docs, and runtime evidence.
INNGEST_* envrequirements out of agent-tooling-only changes.
or dependencies.
workflow behavior.
port.
docs/ai/skills/inngest/references/upstream.md.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 16,487 | 8,805 | -47% | 1 | 1 | 0% | 3,651 | 5,129 | +40% | 0 | 0 | — |
case-02 | fail→pass | 13,289 | 10,036 | -24% | 1 | 1 | 0% | 2,570 | 5,264 | +105% | 0 | 0 | — |
case-03 | fail→pass | 11,016 | 6,962 | -37% | 1 | 1 | 0% | 1,991 | 4,596 | +131% | 0 | 0 | — |
case-04 | fail→pass | 9,252 | 4,028 | -56% | 1 | 1 | 0% | 1,726 | 4,022 | +133% | 0 | 0 | — |
case-05 | fail→pass | 19,155 | 8,931 | -53% | 1 | 1 | 0% | 3,420 | 4,948 | +45% | 0 | 0 | — |
case-06 | pass→pass | 4,542 | 4,766 | +5% | 1 | 1 | 0% | 851 | 4,054 | +376% | 0 | 0 | — |
case-07 | pass→pass | 6,718 | 4,777 | -29% | 1 | 1 | 0% | 1,288 | 4,129 | +221% | 0 | 0 | — |
case-08 | fail→pass | 8,113 | 5,050 | -38% | 1 | 1 | 0% | 1,591 | 4,231 | +166% | 0 | 0 | — |
case-09 | pass→pass | 11,582 | 4,296 | -63% | 1 | 1 | 0% | 2,022 | 4,031 | +99% | 0 | 0 | — |
case-10 | pass→pass | 8,726 | 6,034 | -31% | 1 | 1 | 0% | 1,727 | 4,382 | +154% | 0 | 0 | — |
case-11 | pass→pass | 10,959 | 7,527 | -31% | 1 | 1 | 0% | 2,374 | 4,722 | +99% | 0 | 0 | — |
case-12 | pass→pass | 6,657 | 3,595 | -46% | 1 | 1 | 0% | 1,201 | 3,894 | +224% | 0 | 0 | — |
case-13 | pass→pass | 13,056 | 11,257 | -14% | 1 | 1 | 0% | 2,181 | 5,289 | +143% | 0 | 0 | — |
case-14 | pass→pass | 5,808 | 2,261 | -61% | 1 | 1 | 0% | 936 | 3,557 | +280% | 0 | 0 | — |
case-15 | pass→pass | 12,842 | 6,217 | -52% | 1 | 1 | 0% | 2,336 | 4,501 | +93% | 0 | 0 | — |
case-16 | pass→pass | 9,879 | 9,249 | -6% | 1 | 1 | 0% | 2,069 | 5,085 | +146% | 0 | 0 | — |
case-17 | fail→fail | 11,609 | 10,775 | -7% | 1 | 1 | 0% | 2,150 | 5,217 | +143% | 0 | 0 | — |
case-18 | pass→pass | 9,713 | 7,778 | -20% | 1 | 1 | 0% | 1,687 | 4,592 | +172% | 0 | 0 | — |
case-19 | pass→pass | 11,088 | 9,059 | -18% | 1 | 1 | 0% | 2,112 | 4,992 | +136% | 0 | 0 | — |
case-20 | pass→pass | 9,561 | 2,651 | -72% | 1 | 1 | 0% | 1,645 | 3,633 | +121% | 0 | 0 | — |
case-21 | pass→pass | 9,520 | 3,974 | -58% | 1 | 1 | 0% | 1,703 | 3,880 | +128% | 0 | 0 | — |
case-22 | fail→pass | 17,068 | 2,987 | -82% | 1 | 1 | 0% | 2,691 | 3,676 | +37% | 0 | 0 | — |
case-23 | fail→pass | 11,716 | 2,776 | -76% | 1 | 1 | 0% | 2,025 | 3,702 | +83% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +35 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.