$npx -y skills add inngest/inngest-skills --skill inngest-stepsUse 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 st
| 1 | # Inngest Steps |
| 2 | |
| 3 | Build robust, durable workflows with Inngest's step methods. Each step is a separate HTTP request that can be independently retried and monitored. |
| 4 | |
| 5 | > **These skills are focused on TypeScript.** For Python or Go, refer to the [Inngest documentation](https://www.inngest.com/llms.txt) for language-specific guidance. Core concepts apply across all languages. |
| 6 | |
| 7 | ## Core Concept |
| 8 | |
| 9 | **🔄 Critical: Each step re-runs your function from the beginning.** Put ALL non-deterministic code (API calls, DB queries, randomness) inside steps, never outside. |
| 10 | |
| 11 | **📊 Step Limits:** Every function has a maximum of 1,000 steps and 4MB total step data. |
| 12 | |
| 13 | ```typescript |
| 14 | // ❌ WRONG - will run 4 times |
| 15 | export default inngest.createFunction( |
| 16 | { id: "bad-example", triggers: [{ event: "test" }] }, |
| 17 | async ({ step }) => { |
| 18 | console.log("This logs 4 times!"); // Outside step = bad |
| 19 | await step.run("a", () => console.log("a")); |
| 20 | await step.run("b", () => console.log("b")); |
| 21 | await step.run("c", () => console.log("c")); |
| 22 | } |
| 23 | ); |
| 24 | |
| 25 | // ✅ CORRECT - logs once each |
| 26 | export default inngest.createFunction( |
| 27 | { id: "good-example", triggers: [{ event: "test" }] }, |
| 28 | async ({ step }) => { |
| 29 | await step.run("log-hello", () => console.log("hello")); |
| 30 | await step.run("a", () => console.log("a")); |
| 31 | await step.run("b", () => console.log("b")); |
| 32 | await step.run("c", () => console.log("c")); |
| 33 | } |
| 34 | ); |
| 35 | ``` |
| 36 | |
| 37 | ## step.run() |
| 38 | |
| 39 | Execute retriable code as a step. **Each step ID can be reused** - Inngest automatically handles counters. |
| 40 | |
| 41 | ```typescript |
| 42 | // Basic usage |
| 43 | const result = await step.run("fetch-user", async () => { |
| 44 | const user = await db.user.findById(userId); |
| 45 | return user; // Always return useful data |
| 46 | }); |
| 47 | |
| 48 | // Synchronous code works too |
| 49 | const transformed = await step.run("transform-data", () => { |
| 50 | return processData(result); |
| 51 | }); |
| 52 | |
| 53 | // Side effects (no return needed) |
| 54 | await step.run("send-notification", async () => { |
| 55 | await sendEmail(user.email, "Welcome!"); |
| 56 | }); |
| 57 | ``` |
| 58 | |
| 59 | **✅ DO:** |
| 60 | |
| 61 | - Put ALL non-deterministic logic inside steps |
| 62 | - Return useful data for subsequent steps |
| 63 | - Reuse step IDs in loops (counters handled automatically) |
| 64 | |
| 65 | **❌ DON'T:** |
| 66 | |
| 67 | - Put deterministic logic in steps unnecessarily |
| 68 | - Forget that each step = separate HTTP request |
| 69 | |
| 70 | ## step.sleep() |
| 71 | |
| 72 | Pause execution without using compute time. |
| 73 | |
| 74 | ```typescript |
| 75 | // Duration strings |
| 76 | await step.sleep("wait-24h", "24h"); |
| 77 | await step.sleep("short-delay", "30s"); |
| 78 | await step.sleep("weekly-pause", "7d"); |
| 79 | |
| 80 | // Use in workflows |
| 81 | await step.run("send-welcome", () => sendEmail(email)); |
| 82 | await step.sleep("wait-for-engagement", "3d"); |
| 83 | await step.run("send-followup", () => sendFollowupEmail(email)); |
| 84 | ``` |
| 85 | |
| 86 | ## step.sleepUntil() |
| 87 | |
| 88 | Sleep until a specific datetime. |
| 89 | |
| 90 | ```typescript |
| 91 | const reminderDate = new Date("2024-12-25T09:00:00Z"); |
| 92 | await step.sleepUntil("wait-for-christmas", reminderDate); |
| 93 | |
| 94 | // From event data |
| 95 | const scheduledTime = new Date(event.data.remind_at); |
| 96 | await step.sleepUntil("wait-for-scheduled-time", scheduledTime); |
| 97 | ``` |
| 98 | |
| 99 | ## step.waitForEvent() |
| 100 | |
| 101 | **🚨 CRITICAL: waitForEvent ONLY catches events sent AFTER this step executes.** |
| 102 | |
| 103 | - ❌ Event sent before waitForEvent runs → will NOT be caught |
| 104 | - ✅ Event sent after waitForEvent runs → will be caught |
| 105 | - Always check for `null` return (means timeout, event never arrived) |
| 106 | |
| 107 | ```typescript |
| 108 | // Basic event waiting with timeout |
| 109 | const approval = await step.waitForEvent("wait-for-approval", { |
| 110 | event: "app/invoice.approved", |
| 111 | timeout: "7d", |
| 112 | match: "data.invoiceId" // Simple matching |
| 113 | }); |
| 114 | |
| 115 | // Expression-based matching (CEL syntax) |
| 116 | const subscription = await step.waitForEvent("wait-for-subscription", { |
| 117 | event: "app/subscription.created", |
| 118 | timeout: "30d", |
| 119 | if: "event.data.userId == async.data.userId && async.data.plan == 'pro'" |
| 120 | }); |
| 121 | |
| 122 | // Handle timeout |
| 123 | if (!approval) { |
| 124 | await step.run("handle-timeout", () => { |
| 125 | // Approval never came |
| 126 | return notifyAccountingTeam(); |
| 127 | }); |
| 128 | } |
| 129 | ``` |
| 130 | |
| 131 | **✅ DO:** |
| 132 | |
| 133 | - Use unique IDs for matching (userId, sessionId, requestId) |
| 134 | - Always set reasonable timeouts |
| 135 | - Handle null return (timeout case) |
| 136 | - Use with Realtime for human-in-the-loop flows |
| 137 | |
| 138 | **❌ DON'T:** |
| 139 | |
| 140 | - Expect events sent before this step to be handled |
| 141 | - Use without timeouts in production |
| 142 | |
| 143 | ### Expression Syntax |
| 144 | |
| 145 | In expressions, `event` = the **original** triggering event, `async` = the **new** event being matched. See [Expressi |