# Inngest — Full Documentation > Complete documentation for Inngest, the durable workflow engine for AI applications. > TypeScript SDK v3 reference and framework-specific examples are excluded to reduce size. > Fetch individual pages via https://www.inngest.com/docs-markdown/ or browse the index at https://www.inngest.com/llms.txt. # Node.js Quick Start Source: https://www.inngest.com/docs/getting-started/nodejs-quick-start Description: Get started with Inngest in any Node.js framework. Set up the serve handler, write a durable background function, and run it locally in under 10 minutes. metaTitle = "Node.js Quick Start | Add Background Jobs to Node.js" In this tutorial you will add Inngest to a Node.js app to easily run background tasks and build complex workflows. Inngest makes it easy to build, manage, and execute durable functions. Some use cases include scheduling drip marketing campaigns, building payment flows, or chaining LLM interactions. By the end of this ten-minute tutorial you will: - Set up and run Inngest on your machine. - Write your first Inngest function. - Trigger your function from your app and through Inngest Dev Server. Let's get started! ## Select your Node.js framework Choose your preferred Node.js web framework to get started. This guide uses ESM (ECMAScript Modules), but it also works for Common.js with typical modifications. Inngest works with any Node, Bun or Deno backend framework, but this tutorial will focus on some of the most popular frameworks. ### Optional: Use a starter project If you don't have an existing project, you can clone the following starter project to run through the quick start tutorial: Once you've chosen a project, open it in a code editor. ## Starting your project Start your server using your typical script. We recommend using something like [`tsx`](https://www.npmjs.com/package/tsx) or [`nodemon`](https://www.npmjs.com/package/nodemon) for automatically restarting on file save. If you're using the v4 SDK, set `INNGEST_DEV=1` so your app connects to the local Dev Server instead of Inngest Cloud: ```shell {{ title: "tsx" }} INNGEST_DEV=1 npx tsx watch ./index.ts # replace with your own main entrypoint file ``` ```shell {{ title: "nodemon" }} INNGEST_DEV=1 nodemon ./index.js # replace with your own main entrypoint file ``` You can also add `INNGEST_DEV=1` to your `.env` file so you don't need to pass it inline every time. Now let's add Inngest to your project. ## 1. Install the Inngest SDK In your project directory's root, run the following command to install Inngest SDK: ```shell {{ title: "npm" }} npm install inngest ``` ```shell {{ title: "yarn" }} yarn add inngest ``` ```shell {{ title: "pnpm" }} pnpm add inngest ``` ```shell {{ title: "bun" }} bun add inngest ``` ## 2. Run the Inngest Dev Server Next, start the [Inngest Dev Server](/docs/local-development#inngest-dev-server), which is a fast, in-memory version of Inngest where you can quickly send and view events and function runs. This tutorial assumes that your Node.js server will be running on port `3000`; change this to match your port if you use another. ```shell {{ title: "Bash installer" }} # Install the CLI - follow the steps to complete install: curl -sSfL https://cli.inngest.com/install.sh | sh # Run the dev server inngest dev -u http://localhost:3000/api/inngest ``` ```shell {{ title: "npx" }} npx inngest-cli@latest dev -u http://localhost:3000/api/inngest ``` 👉 For bun we also use `npx`. The Inngest npm package relies on lifecycle scripts to install the CLI binary, which Bun doesn't allow by default
You should see a similar output to the following: ```bash {{ language: 'js' }} $ inngest dev -u http://localhost:3000/api/inngest 12:33PM INF executor > service starting 12:33PM INF runner > starting event stream backend=redis 12:33PM INF executor > subscribing to function queue 12:33PM INF runner > service starting 12:33PM INF runner > subscribing to events topic=events 12:33PM INF no shard finder; skipping shard claiming 12:33PM INF devserver > service starting 12:33PM INF devserver > autodiscovering locally hosted SDKs 12:33PM INF api > starting server addr=0.0.0.0:8288 Inngest dev server online at 0.0.0.0:8288, visible at the following URLs: - http://127.0.0.1:8288 (http://localhost:8288) Scanning for available serve handlers. To disable scanning run `inngest dev` with flags: --no-discovery -u ```
In your browser open [`http://localhost:8288`](http://localhost:8288) to see the development UI where later you will test the functions you write: [IMAGE] ## 3. Create an Inngest client Inngest invokes your functions securely via an [API endpoint](/docs/learn/serving-inngest-functions) at `/api/inngest`. To enable that, you will create an [Inngest client](/docs/reference/typescript/v4/client/create) in your project, which you will use to send events and create functions. Create a file in the directory of your preference. We recommend creating an `inngest` directory for your client and all functions. ```ts {{ filename: "src/inngest/index.ts" }} // Create a client to send and receive events inngest = new Inngest({ id: "my-app" }); // Create an empty array where we'll export future Inngest functions functions = []; ``` ## 4. Set up the Inngest HTTP endpoint Inngest supports 20+ Node.js HTTP frameworks. You will find the instructions to set up your Inngest HTTP endpoint with your preferred framework using the following links:
* [AWS Lambda](/docs/learn/serving-inngest-functions#framework-aws-lambda) * [Bun](/docs/learn/serving-inngest-functions#bun-serve) * [Cloudflare Pages](/docs/learn/serving-inngest-functions#framework-cloudflare-pages-functions) * [Cloudflare Workers](/docs/learn/serving-inngest-functions#framework-cloudflare-workers) * [DigitalOcean Functions](/docs/learn/serving-inngest-functions#framework-digital-ocean-functions) * [ElysiaJS](/docs/learn/serving-inngest-functions#framework-elysia-js) * [Fastify](/docs/learn/serving-inngest-functions#framework-fastify) * [Fresh (Deno)](/docs/learn/serving-inngest-functions#framework-fresh-deno) * [Google Cloud Run Functions](/docs/learn/serving-inngest-functions#framework-google-cloud-run-functions) * [Firebase Cloud functions](/docs/learn/serving-inngest-functions#framework-firebase-cloud-functions) * [Hono](/docs/learn/serving-inngest-functions#framework-hono) * [Koa](/docs/learn/serving-inngest-functions#framework-koa) * [Nitro](/docs/learn/serving-inngest-functions#framework-nitro) * [Nuxt](/docs/learn/serving-inngest-functions#framework-nuxt) * [Redwood](/docs/learn/serving-inngest-functions#framework-redwood) * [Remix](/docs/learn/serving-inngest-functions#framework-remix) * [Supabase Edge Functions](/docs/learn/serving-inngest-functions#framework-supabase-edge-functions) * [SvelteKit](/docs/learn/serving-inngest-functions#framework-svelte-kit)
## 5. Write your first Inngest function {/* TODO - Change this from hello world */} In this step, you will write your first durable function. This function will be triggered whenever a specific event occurs (in our case, it will be `test/hello.world`). Then, it will sleep for a second and return a "Hello, World!". To define the function, use the [`createFunction`](/docs/reference/typescript/v4/functions/create) method on the Inngest client.
Learn more: What is `createFunction` method? The `createFunction` method takes two arguments: - **Configuration**: A unique `id` is required and it is the default name that will be displayed on the Inngest dashboard to refer to your function. You also specify `triggers` to define what starts the function (an `event` name or a `cron` schedule). You can also specify [additional options](/docs/reference/typescript/v4/functions/create#configuration) such as `concurrency`, `rateLimit`, `retries`, or `batchEvents`, and others. Learn more about triggers [here](/docs/features/events-triggers). - **Handler**: The function that is called when the trigger fires. The `event` payload is passed as an argument. Arguments include `step` to define durable steps within your handler and [additional arguments](/docs/reference/typescript/v4/functions/create#handler) include logging helpers and other data.
Define a function in the same file where we defined our Inngest client: ```ts {{ filename: "src/inngest/index.ts" }} inngest = new Inngest({ id: "my-app" }); // Your new function: inngest.createFunction( { id: "hello-world", triggers: [{ event: "test/hello.world" }] }, async ({ event, step }) => { await step.sleep("wait-a-moment", "1s"); return { message: `Hello ${event.data.email}!` }; }, ); // Add the function to the exported array: functions = [ helloWorld ]; ``` In the previous step, we configured the exported `functions` array to be passed to our Inngest http endpoint. Each new function must be added to this array in order for Inngest to read it's configuration and invoke it. Now, it's time to run your function! ## 5. Trigger your function from the Inngest Dev Server UI You will trigger your function in two ways: first, by invoking it directly from the Inngest Dev Server UI, and then by sending events from code. With your Node.js server and Inngest Dev Server running, open the Inngest Dev Server UI and select the "Functions" tab [`http://localhost:8288/functions`](http://localhost:8288/functions). You should see your function. (Note: if you don't see any function, select the "Apps" tab to troubleshoot) [IMAGE] To trigger your function, use the "Invoke" button for the associated function: [IMAGE] In the pop up editor, add your event payload data like the example below. This can be any JSON and you can use this data within your function's handler. Next, press the "Invoke Function" button: ```json { "data": { "email": "test@example.com" } } ``` [IMAGE] The payload is sent to Inngest (which is running locally) which automatically executes your function in the background! You can see the new function run logged in the "Runs" tab: [IMAGE] When you click on the run, you will see more information about the event, such as which function was triggered, its payload, output, and timeline: [IMAGE] In this case, the payload triggered the `hello-world` function, which did sleep for a second and then returned `"Hello, World!"`. No surprises here, that's what we expected! [IMAGE] {/* TODO - Update this when we bring back edit + re-run */} To aid in debugging your functions, you can quickly "Rerun" or "Cancel" a function. Try clicking "Rerun" at the top of the "Run details" table: [IMAGE] After the function was replayed, you will see two runs in the UI: [IMAGE] Now you will trigger an event from inside your app. ## 6. Trigger from code Inngest is powered by events.
Learn more: events in Inngest. It is worth mentioning here that an event-driven approach allows you to: - Trigger one _or_ multiple functions from one event, aka [fan-out](/docs/guides/fan-out-jobs). - Store received events for a historical record of what happened in your application. - Use stored events to [replay](/docs/platform/replay) functions when there are issues in production. - Interact with long-running functions by sending new events including [waiting for input](/docs/features/inngest-functions/steps-workflows/wait-for-event) and [cancelling](/docs/features/inngest-functions/cancellation/cancel-on-events).
To trigger Inngest functions to run in the background, you will need to send events from your application to Inngest. Once the event is received, it will automatically invoke all functions that are configured to be triggered by it. To send an event from your code, you can use the `Inngest` client's `send()` method.
Learn more: `send()` method. Note that with the `send` method used below you now can: - Send one or more events within any API route. - Include any data you need in your function within the `data` object. In a real-world app, you might send events from API routes that perform an action, like registering users (for example, `app/user.signup`) or creating something (for example, `app/report.created`).
You will now send an event from within your server from a `/api/hello` `GET` endpoint. Create a new get API endpoint triggering an event using the following snippet: ```ts await inngest.send({ name: "test/hello.world", data: { email: "testUser@example.com", }, }) ``` Every time this API route is requested, an event is sent to Inngest. To test it, open [`http://localhost:3000/api/hello`](http://localhost:3000/api/hello) (change your port if your Node.js app is running elsewhere). You should see the following output: `{"message":"Event sent!"}` [IMAGE] If you go back to the Inngest Dev Server, you will see a new run is triggered by this event: [IMAGE] And - that's it! You now have learned how to create Inngest functions and you have sent events to trigger those functions. Congratulations 🥳 ## Next Steps To continue your exploration, feel free to check out: - [Examples](/docs/examples) of what other people built with Inngest. - [Case studies](/customers) showcasing a variety of use cases. - [Our blog](/blog) where we explain how Inngest works, publish guest blog posts, and share our learnings. You can also read more: - About [Inngest functions](/docs/learn/inngest-functions). - About [Inngest steps](/docs/learn/inngest-steps). - About [Durable Execution](/docs/learn/how-functions-are-executed) - How to [use Inngest with other frameworks](/docs/learn/serving-inngest-functions). - How to [deploy your app to your platform](/docs/apps/cloud). # Python Quick Start Source: https://www.inngest.com/docs/getting-started/python-quick-start Description: Add durable background functions to a Python app using Flask, FastAPI, or Django in 10 minutes. No queue infrastructure required. metaTitle = "Python Quick Start | Add Background Jobs to Python" {/* This is a duplicate of /docs/getting-started/python-quick-start.mdx, which will soon be deleted*/} This guide will teach you how to add Inngest to a FastAPI app and run an Inngest function. 💡 If you prefer to explore code instead, here are example apps in the frameworks currently supported by Inngest: [FastAPI](https://github.com/inngest/inngest-py/tree/main/examples/fast_api), [Django](https://github.com/inngest/inngest-py/tree/main/examples/django), [Flask](https://github.com/inngest/inngest-py/tree/main/examples/flask), [DigitalOcean Functions](https://github.com/inngest/inngest-py/tree/main/examples/digital_ocean), and [Tornado](https://github.com/inngest/inngest-py/tree/main/examples/tornado). Is your favorite framework missing here? Please open an issue on [GitHub](https://github.com/inngest/inngest-py)! --- ## Create an app ⚠️ Use Python 3.10 or higher. Create and source virtual environment: ```sh python -m venv .venv && source .venv/bin/activate ``` Install dependencies: ```sh pip install fastapi inngest uvicorn ``` Create a FastAPI app file: ```py {{ filename: "main.py" }} from fastapi import FastAPI app = FastAPI() ``` --- ## Add Inngest Let's add Inngest to the app! We'll do a few things 1. Create an **Inngest client**, which is used to send events to an Inngest server. 1. Create an **Inngest function**, which receives events. 1. Serve the **Inngest endpoint** on the FastAPI app. ```py {{ filename: "main.py" }} import logging from fastapi import FastAPI import inngest import inngest.fast_api # Create an Inngest client inngest_client = inngest.Inngest( app_id="fast_api_example", logger=logging.getLogger("uvicorn"), ) # Create an Inngest function @inngest_client.create_function( fn_id="my_function", # Event that triggers this function trigger=inngest.TriggerEvent(event="app/my_function"), ) async def my_function(ctx: inngest.Context) -> str: ctx.logger.info(ctx.event) return "done" app = FastAPI() # Serve the Inngest endpoint inngest.fast_api.serve(app, inngest_client, [my_function]) ``` Start your app: ```sh (INNGEST_DEV=1 uvicorn main:app --reload) ``` 💡 The `INNGEST_DEV` environment variable tells the Inngest SDK to run in "dev mode". By default, the SDK will start in [production mode](/docs/reference/python/overview/prod-mode). We made production mode opt-out for security reasons. Always set `INNGEST_DEV` when you want to sync with the Dev Server. Never set `INNGEST_DEV` when you want to sync with Inngest Cloud. --- ## Run Inngest Dev Server Inngest functions are run using an **Inngest server**. For this guide we'll use the [Dev Server](https://github.com/inngest/inngest), which is a single-binary version of our [Cloud](https://app.inngest.com) offering. The Dev Server is great for local development and testing, while Cloud is for deployed apps (e.g. production). Start the Dev Server: ```shell {{ title: "Bash installer" }} # Install the CLI - follow the steps to complete install: curl -sSfL https://cli.inngest.com/install.sh | sh # Run the dev server inngest dev -u http://localhost:8000/api/inngest ``` ```shell {{ title: "brew" }} # Install the CLI: brew install inngest/tap/inngest # If Homebrew warns about untrusted taps, run this and then install again brew trust --cask inngest/tap/inngest # Run the dev server inngest dev -u http://localhost:8000/api/inngest ``` ```sh {{ title: "Docker" }} docker run -p 8288:8288 inngest/inngest \ inngest dev -u http://host.docker.internal:8000/api/inngest --no-discovery ``` After a few seconds, your app and function should now appear in the Dev Server UI: [IMAGE] [IMAGE] 💡 You can sync multiple apps and multiple functions within each app. --- ## Run your function Click the function's "Trigger" button and a run should appear in the Dev Server stream tab: [IMAGE] --- Note: Framework-specific quick-start guides (Next.js, Express, NestJS, Astro, TanStack Start, H3) follow the same pattern as the Node.js guide above. Find them at: - https://www.inngest.com/docs/getting-started/nextjs-quick-start - https://www.inngest.com/docs/getting-started/express-quick-start - https://www.inngest.com/docs/getting-started/nestjs-quick-start - https://www.inngest.com/docs/getting-started/astro-quick-start - https://www.inngest.com/docs/getting-started/tanstack-start-quick-start - https://www.inngest.com/docs/getting-started/h3-quick-start # Agent Evals Source: https://www.inngest.com/docs/learn/agent-evals Description: Use scoring, deferred scoring, sessions, traces, experiments, and Insights to evaluate production AI workflows on Inngest. metaTitle = "Agent Evals | Evaluate AI Workflows in Production" Agent Evals help you measure how well AI agents and workflows perform in production. You attach scores to function runs, group related work into sessions, preserve execution details in traces, and compare changes with experiments. ## Ship Your First Eval webinar recording They are useful when you want to: - Score an agent run based on guardrails, model confidence, or output quality. - Wait for a real product signal, such as user feedback, ticket resolution, conversion, or human review. - Compare prompts, models, providers, tools, or workflow changes against production traffic. - Debug a bad result by tracing the run, session, model calls, tool calls, and outcome signal together. Agent Evals is not a separate SDK package. It is the production evaluation workflow you build with Inngest functions, scoring, deferred scoring, sessions, traces, step experiments, and Insights. Scoring and deferred scoring require the TypeScript SDK v4 and currently use beta APIs. ## Basic example Add `scoreMiddleware()` to your Inngest client, then score the current run from inside a function. ```ts inngest = new Inngest({ id: "support-agent", middleware: [scoreMiddleware()], }); export default inngest.createFunction( { id: "answer-support-ticket", triggers: { event: "support/ticket.created" }, }, async ({ event, step }) => { await step.run("generate-answer", async () => { return generateAnswer(event.data.ticket); }); await step.run("check-answer", async () => { return validateAnswer(answer); }); await step.score("score-answer-quality", { name: "answer-quality", value: passed, }); return { answer, passed }; } ); ``` [`step.score()`](/docs/reference/typescript/v4/functions/scoring?ref=docs-agent-evals#step-score-id-options) is durable and memoized. If the function retries or replays, Inngest does not record the same score twice. ## How Agent Evals works A production eval usually has four parts: 1. **Run the workflow.** Put model calls, tool calls, waits, and side effects inside Inngest functions and steps. 2. **Preserve context.** Use traces, AI metadata, OpenTelemetry, and sessions so the run can be inspected later. 3. **Attach scores.** Use direct scoring when the result is known during the run, or deferred scoring when the outcome arrives later. 4. **Compare changes.** Use step experiments when you need to compare prompts, models, tools, providers, or workflow variants. ## Add session context [Sessions](/docs/features/events-triggers/sessions?ref=docs-agent-evals) group related function runs by an ID from your product, such as a conversation, support ticket, import, or agent task. Add `meta.sessions` when sending the event that starts the workflow: ```ts await inngest.send({ name: "support/ticket.created", data: { ticketId: "tk_123", message: "I can't sign in.", }, meta: { sessions: { ticket_id: "tk_123", }, }, }); ``` The session does not change which functions run. It makes every related run easier to find and inspect in the dashboard. ## Score during the run Use [direct scoring](/docs/features/inngest-functions/steps-workflows/scoring?ref=docs-agent-evals) when the outcome is known before the function finishes. Good direct scores include: - guardrail pass or fail - JSON validity - retrieval confidence - tool success - model confidence - LLM-as-a-judge result that runs inline ```ts await step.run("score-retrieval", async () => { return calculateRetrievalConfidence(results); }); await step.score("score-retrieval-confidence", { name: "retrieval-confidence", value: confidence, }); ``` Scores can be numbers or booleans. Use consistent score names across runs so results can be aggregated. ## Score after the run Use [deferred scoring](/docs/features/inngest-functions/steps-workflows/deferred-scoring?ref=docs-agent-evals) when the signal arrives after the workflow finishes. For example, a support agent may answer a ticket now, but the useful score may arrive later when the user clicks "helpful" or the ticket reopens. ```ts feedbackScorer = createScorer( inngest, { id: "support-feedback-scorer", schema: z.object({ ticketId: z.string() }), }, async ({ event, step }) => { await step.waitForEvent("wait-for-feedback", { event: "support/feedback.received", timeout: "7d", if: `async.data.ticketId == '${event.data.ticketId}'`, }); return { name: "user-feedback", value: feedback?.data.helpful ? 1 : 0, }; } ); ``` Trigger the scorer from the function that produced the result: ```ts export default inngest.createFunction( { id: "answer-support-ticket", triggers: { event: "support/ticket.created" }, }, async ({ event, step, defer }) => { await step.run("generate-answer", async () => { return generateAnswer(event.data.ticket); }); defer("score-feedback", { function: feedbackScorer, data: { ticketId: event.data.ticketId }, }); return { answer }; } ); ``` The scorer runs separately in the background. Its returned score is attributed to the parent run that deferred it. ## Compare variants Use [step experiments](/docs/features/inngest-functions/steps-workflows/step-experiments?ref=docs-agent-evals) when you need to compare more than one version of an AI workflow. For example, you can split traffic between two prompt strategies, then score the selected variant when feedback arrives: ```ts await group.experiment("answer-style", { variants: { concise: () => step.run("answer-concise", () => answerConcise(event.data)), detailed: () => step.run("answer-detailed", () => answerDetailed(event.data)), }, select: experiment.bucket(event.data.accountId, { weights: { concise: 50, detailed: 50 }, }), }); defer("score-answer-feedback", { function: feedbackScorer, data: { ticketId: event.data.ticketId }, experiment: experimentRef, }); return result; ``` `experimentRef` identifies the experiment and variant that served the result. Passing it to [`defer()`](/docs/reference/typescript/v4/functions/scoring?ref=docs-agent-evals#defer-id-options) lets the scorer attribute the later score to the selected variant. ## Attribute scores from later runs When a score is written outside the run that produced the result, pass the run ID you want to score. ```ts await inngest.score({ name: "user-feedback", value: 1, runId: "01ABC123...", }); ``` For experiment variants, pass both the original `runId` and the `experimentRef` returned by `group.experiment()`. ```ts await inngest.score.experiment({ name: "clickthrough", value: 1, experiment: experimentRef, runId: "01ABC123...", }); ``` See the [scoring reference](/docs/reference/typescript/v4/functions/scoring?ref=docs-agent-evals) for the full attribution rules. ## Inspect results Agent Evals connects several docs and dashboard surfaces: | Surface | Use it to | | --- | --- | | [Traces](/docs/platform/monitor/traces?ref=docs-agent-evals) | Inspect the function run, steps, model calls, tool calls, errors, and scores. | | [Sessions](/docs/features/events-triggers/sessions?ref=docs-agent-evals) | Find all runs related to a conversation, ticket, account, job, or agent task. | | [Scoring](/docs/features/inngest-functions/steps-workflows/scoring?ref=docs-agent-evals) | Attach numeric or boolean quality signals to runs and steps. | | [Deferred scoring](/docs/features/inngest-functions/steps-workflows/deferred-scoring?ref=docs-agent-evals) | Evaluate outcomes that arrive after the original workflow finishes. | | [Step experiments](/docs/features/inngest-functions/steps-workflows/step-experiments?ref=docs-agent-evals) | Split traffic across workflow variants and compare scored outcomes. | | [Insights](/docs/platform/monitor/insights?ref=docs-agent-evals) | Query historical run, event, step, and trace data. | ## Choose the right eval tool | Goal | Use | | --- | --- | | Score a guardrail, validation check, model confidence, or inline judge result | [`step.score()` or `inngest.score()`](/docs/reference/typescript/v4/functions/scoring?ref=docs-agent-evals) | | Wait for user feedback, ticket resolution, conversion, or review before scoring | [Deferred scoring](/docs/features/inngest-functions/steps-workflows/deferred-scoring?ref=docs-agent-evals) | | Compare prompts, models, providers, tools, or workflow rewrites | [Step experiments](/docs/features/inngest-functions/steps-workflows/step-experiments?ref=docs-agent-evals) | | Keep a user, account, or tenant on the same variant while testing | [`experiment.bucket()`](/docs/features/inngest-functions/steps-workflows/step-experiments?ref=docs-agent-evals#bucket) | | Find all runs related to one conversation, ticket, import, or agent task | [Sessions](/docs/features/events-triggers/sessions?ref=docs-agent-evals) | | Inspect the model call, tool call, database query, or HTTP request inside a run | [OpenTelemetry](/docs/examples/open-telemetry?ref=docs-agent-evals) and [Extended Traces](/docs/reference/typescript/v4/extended-traces?ref=docs-agent-evals) | | Query historical events, runs, steps, traces, and scores | [Insights](/docs/platform/monitor/insights?ref=docs-agent-evals) | ## Notes and best practices - Start with the product outcome you care about, then decide where that signal appears. - Use direct scoring for signals known during the run. - Use deferred scoring for signals that arrive later or take time to compute. - Use sessions when many runs belong to the same conversation, ticket, user task, or job. - Use stable score names. Changing score names creates separate metrics. - Score experiments with `experimentRef` so outcomes are attributed to the variant that produced them. - Keep model calls, tool calls, waits, and side effects inside steps so traces explain the score. ## Troubleshooting | Issue | Solution | | --- | --- | | **Setup** | | | Scoring is not available. | Use the TypeScript SDK v4 and install the latest SDK version. | | `step.score()` is missing or throws. | Register `scoreMiddleware()` on the Inngest client: `middleware: [scoreMiddleware()]`. | | **Attribution** | | | A score does not appear on the run you expected. | When scoring outside the current run, pass the target `runId`. See [attribute scores from later runs](#attribute-scores-from-later-runs). | | A deferred score does not attach to an experiment variant. | Pass the `experimentRef` returned by `group.experiment()` when calling `defer()`. See [attribute scores from later runs](#attribute-scores-from-later-runs). | | Related runs are hard to find. | Add `meta.sessions` to the events that start or connect the workflow. | | **Interpreting results** | | | Boolean scores look like numbers in aggregates. | Boolean scores aggregate numerically. Treat `true` as `1` and `false` as `0`. | | A model call succeeded but the score is bad. | Inspect the trace and session. Production evals measure the outcome, not only whether the model request completed. | ## Resources ## Related docs - [Score a function run](/docs/features/inngest-functions/steps-workflows/scoring?ref=docs-agent-evals) - [Build a deferred scorer](/docs/features/inngest-functions/steps-workflows/deferred-scoring?ref=docs-agent-evals) - [Step experiments](/docs/features/inngest-functions/steps-workflows/step-experiments?ref=docs-agent-evals) - [Run experiments in production](/docs/patterns/ai-evals/run-experiments-in-production?ref=docs-agent-evals) - [Sessions](/docs/features/events-triggers/sessions?ref=docs-agent-evals) - [Traces](/docs/platform/monitor/traces?ref=docs-agent-evals) - [Set up OpenTelemetry with Inngest](/docs/examples/open-telemetry?ref=docs-agent-evals) - [Deferred Functions reference](/docs/reference/typescript/v4/functions/deferred-functions?ref=docs-agent-evals) - [Scoring reference](/docs/reference/typescript/v4/functions/scoring?ref=docs-agent-evals) # Durable Agents Source: https://www.inngest.com/docs/learn/durable-agents Description: A durable agent is designed to survive failures, restarts, and resume after being paused, all without losing a single step of progress. metaTitle = "Durable Agents" ## What is a Durable Agent? Most agent frameworks treat execution as ephemeral. Your agent runs in a single process — a loop of LLM calls, tool invocations, and decisions — and if that process crashes, times out, or gets deployed over, everything is gone. The agent starts from scratch, or it doesn't start at all. A durable agent is different. Every step, every tool call, every decision the agent makes is checkpointed as it happens. If the process dies at step 7 of 12, the agent picks up at step 7 — not step 1. If the agent needs to wait three hours for a human to approve something, it suspends entirely, holding zero resources, and resumes when the approval arrives. This isn't retry logic bolted onto an agent loop. It's a fundamentally different execution model — one that treats the entire agent lifecycle as recoverable infrastructure. The agent's progress is a durable record, not an in-memory variable that vanishes when the process does. ## Why Agents Need Durability If you've run agents in production, you already know the failure modes. If you haven't, you'll hit them fast. **LLMs and tool calls fail.** Every network request is something you don't control. LLM providers have outages, tool calls to third party APIs timeout or hit rate limits. Each chain in your agent loop is an opportunity to fail. With more steps, the probability of failure compounds. Without durability, a failure on your 6th step means re-running steps 1 through 5. **Agents take longer than you expect.** The interesting agents aren't the ones that finish in 500 milliseconds. They're the ones that perform complex tasks, research across sources, pause for input, or coordinate with other agents. These operations take minutes, hours, or longer. These must be durable, inspectable, and resumable. **Failure is expensive.** Every LLM call costs tokens. Every token costs money. Replaying an entire agent run because a single step failed near the end is wasteful in the most literal sense — you're paying to redo work the agent already completed successfully. At scale, this waste becomes a real line item. **You can't debug what you can't see.** When an agent makes a bad decision in production, you need to know exactly what happened: what context the model received, what it decided, what tools it called, what those tools returned. Ephemeral execution gives you logs if you're lucky. Durable execution gives you a full, structured audit trail of every decision and action — because checkpointing the work *is* recording the work. ## The Agent Loop as a Workflow The mental model for an agent is straightforward: it's a loop. Observe the current state, think about what to do next, take an action, observe the result, and repeat. This is the core loop behind every ReAct agent, every tool-using assistant, every autonomous system. What makes this loop interesting from an infrastructure perspective is that each iteration is an independently meaningful unit of work. The agent called a tool and got a result — that's a fact that doesn't need to be recomputed. The agent made an LLM call and chose a next action — that decision, and its reasoning, can be stored. Each cycle through the loop is a step that can be checkpointed on its own. The critical difference between an agent loop and a traditional workflow DAG is that the loop is ***dynamic***. A workflow DAG has a known shape at design time — step A, then step B, then fan out to C and D. In contrast, an agent's shape is decided ***at runtime*** by the model. The number of iterations, which tools get called, the order they run in, whether the agent loops back to retry something — none of this is known in advance. The workflow graph is drawn as the agent runs. This is what makes agents powerful. It's also what makes them challenging to run reliably — and it's why the durability mechanism matters. → [Guide: Agent tool loops](/docs/ai-patterns/agent-tool-loops) ## How Durability Works: Memoization and Deterministic Replay Durability for agents requires solving two problems simultaneously: **1. Support non-deterministic execution.** An agent's control flow is decided at runtime by the model. Which tools get called, how many loop iterations run, whether the agent backtracks — none of this is known in advance. There's no static DAG, no pre-declared workflow graph. Steps are defined *dynamically* as the agent runs. The durability system can't require you to declare the shape of execution ahead of time, because you don't know it. **2. Support deterministic replay.** When a process fails or an agent resumes after a pause, the system needs to reconstruct the agent's *full state* — every prior decision, every tool result, every branch taken — so execution can continue exactly where it left off. This reconstruction must be deterministic: the same state, every time. These sound contradictory, but they're not. Inngest solves both with the same mechanism: **step-level memoization**. Every time a step completes — an LLM call, a tool invocation, any side effect — Inngest persists the result. If the process fails and the function needs to recover, it re-executes from the top. But every previously completed step is short-circuited: instead of running again, it returns its memoized result. This is deterministic replay. The function walks the exact same execution path it walked before, because the memoized results *force* the same path. An LLM call that originally returned "use the search tool" will return that same result on replay — the call isn't re-made, its stored result is injected. So the agent takes the same branch, calls the same next tool, follows the same dynamic path, step by step, until all memoized steps have replayed. Then execution continues forward with new steps. This is what makes the model work for agents specifically. The execution doesn't need to be pre-declared: steps are defined at runtime, as the agent decides what to do. But on replay, those decisions are reproduced exactly, because the inputs that drove them (prior step results - the agent's state) are replayed exactly. Non-deterministic code on the first run. Deterministic replay on recovery. → [Concept: How Inngest Durability Works](/docs/learn/how-functions-are-executed) ## Core Primitives Inngest provides a small set of primitives that give agents durability without requiring you to redesign how they work. Each primitive wraps a specific kind of operation, making it checkpointed, retryable, and resumable. **[`step.run()`](/docs/learn/inngest-steps)** wraps any operation that has a side effect — an LLM call, a tool invocation, a database write, an API request. The result is memoized: if the step succeeds, it's never re-executed. If it fails, it's retried according to your configured policy. This is the building block that makes each iteration of the agent loop durable. ```tsx await step.run("search-documents", async () => { return await vectorDB.search(query); }); ``` **[`step.waitForEvent()`](/docs/features/inngest-functions/steps-workflows/wait-for-event)** pauses execution until an external signal arrives. The agent suspends completely — no process, no connection, no resources held. When the matching event is received (a human approval, a webhook callback, a message from another agent), execution resumes with the event data. This is what makes human-in-the-loop and inter-agent coordination possible without polling or timeouts. **[`step.invoke()`](/docs/guides/invoking-functions-directly?guide=python)** calls another Inngest function or sub-agent and waits for its result. This is how you compose agents — an orchestrator agent can delegate sub-tasks to sub-agents, each running with their own durability guarantees, and collect the results. **[`step.sleep()` and `step.sleepUntil()`](/docs/features/inngest-functions/steps-workflows/sleeps)** pause execution for a duration or until a specific time. Like `waitForEvent`, the agent fully suspends. You can sleep for five seconds or five days — the cost is the same (zero), because no process is running while the agent waits. {/* Link to sessions guide tomorrow */} **Sessions** - Agents often span multiple runs. Conversations have multiple turns, sub-agents are fanned-out, and replies can be decoupled. Sessions enable all runs to be grouped in any way that match your application: conversation ids, project ids, or ticket ids. Use one or more session ID to create the full picture of your system. These primitives compose naturally. An agent loop might call `step.run()` for each tool invocation, `step.waitForEvent()` when it needs human input, and `step.invoke()` when it delegates to another agent. The durability is granular — each primitive is independently checkpointed, so the agent never loses more than the currently-executing step. ## Human-in-the-Loop Some of the most valuable agent workflows require a human somewhere in the middle. An agent researches a topic and drafts an email, but a human needs to approve it before it's sent. An agent triages a support ticket and proposes a resolution, but the resolution requires sign-off. An agent generates a code change, and a developer reviews it. In a non-durable system, this is painful. The agent either holds a process open while waiting (expensive and fragile) or you build an elaborate system of queues, state machines, and polling to park the work and pick it back up (complex and error-prone). With durable agents, the pattern is simple. The agent runs until it needs human input, calls `step.waitForEvent()` with a match condition, and suspends. No process is running. No connection is held. The agent's entire state — every step it completed, every decision it made — is safely persisted. When the human acts (clicks approve, submits feedback, makes an edit), an event is sent, and the agent wakes up exactly where it left off with the human's input available. This is where durability becomes more than a reliability feature — it changes what agents can *do*. Workflows that span hours or days, that cross the boundary between automated and manual work, become straightforward to build. The agent doesn't care if the human takes 30 seconds or 3 days. It's not waiting. It's suspended. → [Guide: Human-in-the-Loop](/docs/ai-patterns/human-in-the-loop) ## Multi-Agent Coordination Real systems rarely have a single agent doing everything. You may opt to use sub-agents to isolate context or create specialized agents with their own subset of tools. Coordinating multiple agents is an architecture problem, and durability is central to solving it well. A durable agent can invoke other agents using `step.invoke()`, fan out parallel work, and collect results — the classic orchestrator-worker pattern. But because each agent is independently durable, the failure characteristics are fundamentally better than a single monolithic agent. If the code-review agent fails, the search agent's completed work isn't lost. The orchestrator can retry the failed sub-agent, [gracefully recover](/docs/features/inngest-functions/error-retries/failure-handlers), or surface the partial results. This independence also means agents can communicate asynchronously through events. One agent can emit an event that wakes up another agent that's been suspended, waiting for exactly that signal. You can build pipelines, fan-out/fan-in patterns, and feedback loops — all with durability at every stage. The key insight is that durability makes coordination *safe*. Without it, multi-agent systems are brittle: a failure in one agent cascades unpredictably, and recovering coordinated state across multiple processes is a nightmare. With durability, each agent's progress is preserved independently, and the coordination layer can reason about what succeeded, what failed, and what to do about it. → [Guide: Sub-agent delegation](/docs/ai-patterns/sub-agent-delegation) ## Error Handling & Recovery Agents fail. The question is what happens next. Every step wrapped in `step.run()` gets automatic retries with configurable policies — you control the number of attempts, the backoff strategy, and the timeout. This handles the common case: a transient API failure, a rate limit, a momentary network issue. The step retries, succeeds, and the agent continues without losing progress. When retries are exhausted, the step fails permanently. But this doesn't necessarily mean the *agent* fails. This distinction is important. A single step failure might be something the agent can reason about — "the search API is down, let me try an alternative approach" — or it might be terminal. You can catch step failures and implement fallback logic, or let them propagate to fail the entire agent run. When an agent run does fail completely, the checkpointed state is still there. You know exactly which step failed, what the inputs were, what the error was, and what the agent had accomplished up to that point. Partial progress isn't lost — it's recorded. You can inspect it, fix the underlying issue, and in some cases, resume from the failure point rather than starting over. The design philosophy is that failure should be *granular*. The blast radius of a single API timeout shouldn't be an entire agent run. Each step is its own unit of failure, with its own retry policy and its own recovery path. → [Guide: Error Handling](/docs/guides/error-handling) ## Observability & Debugging Agents are non-deterministic systems. The same agent with the same input might take a different path every time. In production, this means you can't rely on reading the code to understand what happened — you need to see what *actually* happened on each run. Because every step is checkpointed, you get a complete, structured trace of the agent's execution for free. Every LLM call, every tool invocation, every decision point — recorded with inputs, outputs, and timing. This isn't logging you have to add. It's a natural byproduct of the durability model. This trace is the single most important debugging tool for production agents. When an agent produces a bad result, you can walk through exactly what it saw, what it decided, what tools it called, and what those tools returned. You can identify whether the problem was a bad model response, a tool returning unexpected data, or a flaw in the agent's prompt or logic. At scale, these traces become the foundation for understanding agent behavior in aggregate — which tools fail most often, where agents get stuck in loops, how long different operations take, and where you're spending the most tokens. Observability isn't an add-on for durable agents. It's built into how they work. → [Guide: Tracing & Observability](/docs/platform/monitor/traces) --- ## What's Next Start building with the core primitives, or go deeper on the patterns: - [Steps](/docs/learn/inngest-steps) — how to leverage step-level durability - [Human-in-the-Loop](/docs/ai-patterns/human-in-the-loop) — building agents that collaborate with humans - [Multi-Agent Patterns](/docs/ai-patterns/sub-agent-delegation) — orchestrating multiple agents - [Error Handling](/docs/guides/error-handling) — retry policies, failure handlers, and recovery - [Tracing & Observability](/docs/platform/monitor/traces) — debugging and monitoring agents in production - [How Inngest Durability Works](/docs/learn/how-functions-are-executed) — the architecture behind durable execution # Durable Endpoints Streaming Source: https://www.inngest.com/docs/learn/durable-endpoints/streaming Description: Stream incremental responses back to clients from a Durable Endpoint while running durable step logic in the background. Combine streaming UX with reliability. metaTitle = "Streaming with Durable Endpoints" Durable Endpoints can stream data back to clients in real-time using Server-Sent Events (SSE). This lets you stream AI inference tokens, progress updates, or any other data, while keeping the durability guarantees of [durable steps](/docs/learn/inngest-steps). Streaming works across multiple steps within a single endpoint invocation, and handles the transition from sync to async mode seamlessly. If a step fails and retries, any data streamed during that step is automatically rolled back on the client. Durable Endpoints streaming is currently only available in the TypeScript SDK. This guide assumes you've already [set up a Durable Endpoint](/docs/learn/durable-endpoints). ## When to use streaming - **AI inference** — Stream LLM tokens to the browser as they're generated, so users see results immediately. - **Status updates** — Send progress messages during long-running endpoint executions. - **Making existing streaming endpoints durable** — Wrap your existing streaming HTTP endpoints with steps to add retry and observability at no cost to functionality. If you don't need to stream data directly to an HTTP client, consider using [Realtime](/docs/features/realtime) to push updates from background Inngest functions via pub/sub channels. ## Example In this example, we'll create an HTTP endpoint that generates a haiku and then translates it to French. The client will be the browser, and it'll render the haiku and its translation as they're generated. The user will see the streamed LLM output appear in realtime. ### Server Import `step` from `inngest` and `stream` from `inngest/experimental/durable-endpoints`, then use `stream.push()` or `stream.pipe()` inside your endpoint handler: ```typescript GET = inngest.endpoint(async () => { // Option A: push() with an SDK event callback await step.run("generate", async () => { stream.push("Generating...\n"); new Anthropic(); client.messages.stream({ model: "claude-sonnet-4-20250514", max_tokens: 512, messages: [{ role: "user", content: "Write a haiku about durability." }], }); response.on("text", (token) => stream.push(token)); return await response.finalText(); }); // Option B: pipe() — streams each chunk AND returns the collected text await step.run("translate", async () => { stream.push(`\nTranslating...\n`); new Anthropic(); client.messages.stream({ model: "claude-sonnet-4-20250514", max_tokens: 256, messages: [{ role: "user", content: `Translate to French: ${text}` }], }); return stream.pipe(async function* () { for await (const event of response) { if ( event.type === "content_block_delta" && event.delta.type === "text_delta" ) { yield event.delta.text; } } }); }); return new Response("\nDone!"); }); ``` ### Client Use `fetchWithStream()` from `inngest/experimental/durable-endpoints/client` to consume the stream. It handles SSE parsing, sync-to-async redirects, and commit/rollback automatically. Chunks arrive on the client in the order they are pushed or yielded on the server. ```typescript "use client"; export default function Generate() { useState([]); useRef(0); async function run() { setChunks([]); uncommittedCountRef.current = 0; await fetchWithStream("/api/generate", { onData: ({ data }) => { if (typeof data === "string") { uncommittedCountRef.current++; setChunks((prev) => [...prev, data]); } }, onRollback: () => { // A step failed and will retry — remove the chunks it produced uncommittedCountRef.current; setChunks((prev) => prev.slice(0, prev.length - count)); uncommittedCountRef.current = 0; }, onCommit: () => { // Step completed — its chunks are now permanent uncommittedCountRef.current = 0; }, }); // The endpoint's return value is available as the Response body await resp.text(); setChunks((prev) => [...prev, result]); } return (
{chunks.join("")}
); } ``` ## Server API ### `stream.push(data)` Send a single chunk of data to the client as an SSE event. ```typescript stream.push("Loading..."); stream.push({ progress: 50, message: "Halfway there" }); ``` - Accepts any JSON-serializable value. - Fire-and-forget. Does not block execution or return a value. - No-op outside of an Inngest execution context, so your code works the same when called outside of a durable endpoint. `push()` is ideal for one-off status messages or streaming via provider SDK event callbacks. ### `stream.pipe(source)` Pipe a stream source to the client and resolve with the concatenated text of all chunks. Each chunk is sent as an SSE event in real-time. The simplest case is piping a `ReadableStream`, like a `fetch` response body: ```typescript await fetch("https://api.example.com/stream"); await stream.pipe(response.body); // `text` contains the full response; the client received it chunk by chunk ``` When you need to transform or filter chunks before they're sent, pass an async generator function. Each `yield` sends one chunk to the client: ```typescript await stream.pipe(async function* () { for await (const event of response) { // Only yield the parts you want the client to see if (event.type === "content_block_delta") { yield event.delta.text; } } }); ``` `pipe()` accepts three source types: - **`ReadableStream`** — piped directly, decoded from bytes to string chunks. - **`AsyncIterable`** — each value in the iterable becomes a chunk. - **`() => AsyncIterable`** — a function that returns an async iterable. This is what lets you pass `async function*` generators directly to `pipe()`. No-op outside of an Inngest execution context (resolves with an empty string). For the full `stream.push()` and `stream.pipe()` API reference, see the [Streaming reference](/docs/reference/typescript/v4/durable-endpoints#streaming). ## Client API ### `fetchWithStream(url, options)` The primary way to consume a streaming Durable Endpoint. Import it from `inngest/experimental/durable-endpoints/client`: ```typescript ``` `fetchWithStream()` returns a `Promise`. `await` it to drive the stream to completion. When the endpoint finishes, the returned `Response` contains the endpoint's final return value. If the endpoint does not use streaming, `fetchWithStream()` returns the raw `Response` as-is. The core callbacks handle the majority of streaming use cases: - **`onData({ data, hashedStepId })`** — Called for each chunk. `data` is the deserialized value; `hashedStepId` identifies which step produced it (or `null` if streamed outside a step). Data should be considered uncommitted until `onCommit` fires. - **`onRollback({ hashedStepId })`** — Called when a step fails and will retry. Your code is responsible for tracking and removing the chunks produced by that step (see the [example above](#client) for a pattern using a ref counter). - **`onCommit({ hashedStepId })`** — Called when a step completes successfully. Chunks from that step are now permanent and will never be rolled back. Because `stream.push()` accepts any JSON-serializable value, `data` in the `onData` callback is typed as `unknown`. Narrow the type in your callback as needed: ```typescript { current: 0 }; await fetchWithStream("/api/generate", { onData: ({ data }) => { if (typeof data === "string") { uncommittedCount.current++; console.log("Chunk:", data); } }, onRollback: () => { // Discard uncommitted chunks and reset counter uncommittedCount.current = 0; }, onCommit: () => { // Chunks are permanent — reset counter uncommittedCount.current = 0; }, }); await resp.text(); ``` For all available options see the [full API reference](/docs/reference/typescript/v4/durable-endpoints#client-fetchwithstream). ## How it works ### Sync-to-async transitions When a client calls a streaming Durable Endpoint, the SSE stream flows directly from your app to the client. If the endpoint needs to go async (e.g. due to `step.sleep()`, `step.waitForEvent()`, or a retry), the SDK sends a redirect event telling the client where to reconnect, and the stream continues through the Inngest server. `fetchWithStream()` handles this redirect automatically. The client sees a single continuous stream regardless of sync-to-async transitions. ### Streaming activation Streaming is activated lazily. The endpoint only sends an SSE response if: - The client sends the `Accept: text/event-stream` header (which `fetchWithStream()` does automatically), **and** - Your code calls `stream.push()` or `stream.pipe()` during execution. If neither `push()` nor `pipe()` is called, the endpoint behaves like a regular non-streaming Durable Endpoint. ### Rollback on retry Each chunk is tagged with the step that produced it (via `hashedStepId`). When a step completes, `onCommit` fires and those chunks become permanent. When a step fails and retries, `onRollback` fires and your client code should discard the uncommitted chunks from that step. On the retry attempt, the step streams fresh data that replaces what was rolled back. See the [example above](#client) for an implementation pattern. Data streamed outside of a `step.run()` is never rolled back. ### SSE event types The stream uses SSE with the following event types. The `inngest.*` events are internal protocol events handled by `fetchWithStream()` automatically; only `inngest.stream` events contain user data. | Event name | Payload | Purpose | |---|---|---| | `inngest.metadata` | `{ runId }` | Always first. Identifies the run. | | `inngest.stream` | `{ data, hashedStepId? }` | User data from `push()` / `pipe()`. | | `inngest.commit` | `{ hashedStepId }` | Step succeeded. Its streamed data is permanent. | | `inngest.rollback` | `{ hashedStepId }` | Step failed. Discard its uncommitted data. | | `inngest.redirect_info` | `{ runId, url }` | Tells the client to reconnect for async continuation. | | `inngest.response` | `{ status, response: { body, headers, statusCode } }` | Terminal event. Closes the stream. | ## Limitations Durable Endpoints streaming is currently in developer preview. In addition to any [general Durable Endpoint limitations](/docs/learn/durable-endpoints#limitations), the following apply: - **15 minute timeout** — Client connections time out after 15 minutes, meaning your endpoint should complete within this window (including any retries) to ensure the stream is delivered end-to-end. - **No rollback outside of steps** — Data streamed outside of a `step.run()` is never rolled back. If you need rollback guarantees, stream from within a step. - **One streaming parallel step** — You can stream from at most one parallel step. Streaming from multiple parallel steps will result in interleaved output that cannot be disambiguated by the client. - **No streaming from child functions** — `step.invoke()` calls cannot stream data back to the parent function's client. - **Raw `Response` objects may be lost on async transition** — If your endpoint returns a `Response` (like a file download) and goes async, the Response is lost because it can't be memoized. Use `stream.push()` or `stream.pipe()` instead. ## SDK support | SDK | Support | Version | |-----|---------|---------| | TypeScript | Developer Preview | >= 4.x (with `endpointAdapter`) | # Durable Endpoints Source: https://www.inngest.com/docs/learn/durable-endpoints Description: Make any HTTP endpoint durable with automatic retries and step-based checkpointing using Inngest Durable Endpoints. No queue or worker infrastructure required. metaTitle = "Durable Endpoints | Turn Any HTTP Handler into a Durable Workflow" Durable Endpoints let you build or transform your API into fault-tolerant endpoints simply by wrapping your critical logic into [durable steps](/docs/learn/inngest-steps). Durable Endpoints behave like normal endpoints. The mental model stays the same: request, response. But each step brings you tracing, observability, and retry logic from the point of failure. Durable Endpoints are available in the TypeScript and Go SDKs. ## When to use Durable Endpoints **You have endpoints that fail partway through.** Any endpoint with multiple steps where failure at step 3 means steps 1 and 2 were wasted work. Instead of writing try/catch logic everywhere or hoping for the best, simply wrap your code in steps and let failures resume from where they left off. **You want observability without the setup.** If you want visibility into your endpoints without configuring a bunch of external services, Durable Endpoints give you that instantly. **You're already using Inngest.** You can add durability to other endpoints without refactoring everything into a workflow or thinking heavily about event logic. ## Quick Start If you have a traditional endpoint: ```typescript {{ title: "Next.js" }} POST = async (req: NextRequest) => { await req.json(); await db.users.find(userId); { ...data, account: user.accountId }; await processData(enriched); await sendNotification(userId, result); return Response.json({ success: true, result }); }; ``` ```typescript {{ title: "Bun" }} Bun.serve({ port: 3000, routes: { "/process": async (req) => { await req.json(); await db.users.find(userId); enriched = { ...data, account: user.accountId }; await processData(enriched); await sendNotification(userId, result); return new Response(JSON.stringify({ success: true, result })); }, }, }); ``` Create an Inngest client: ```typescript {{ title: "Next.js" }} new Inngest({ id: "my-app", endpointAdapter, }); ``` ```typescript {{ title: "Bun" }} new Inngest({ id: "my-app", endpointAdapter, }); ``` Then, wrap your API endpoint with `inngest.endpoint` and move your endpoint's critical logic into `step.run` blocks: ```typescript {{ title: "Next.js" }} POST = inngest.endpoint(async (req: NextRequest) => { await req.json(); // Step 1: Validate and enrich the data await step.run("enrich-data", async () => { await db.users.find(userId); return { ...data, account: user.accountId }; }); // Step 2: Process the enriched data await step.run("process", async () => { return await processData(enriched); }); // Step 3: Send notification await step.run("notify", async () => { await sendNotification(userId, result); }); return Response.json({ success: true, result }); }); ``` ```typescript {{ title: "Bun" }} new Inngest({ id: "my-app", endpointAdapter }); Bun.serve({ port: 3000, routes: { "/process": inngest.endpoint(async (req) => { await req.json(); // Step 1: Validate and enrich the data await step.run("enrich-data", async () => { await db.users.find(userId); return { ...data, account: user.accountId }; }); // Step 2: Process the enriched data await step.run("process", async () => { return await processData(enriched); }); // Step 3: Send notification await step.run("notify", async () => { await sendNotification(userId, result); }); return new Response(JSON.stringify({ success: true, result })); }), }, }); ``` If `process` fails, the endpoint will retry from that step. `enrich-data` won't re-run. **Read the [Durable Endpoint TypeScript SDK Reference](/docs/reference/typescript/durable-endpoints) for more detailed usage information.** ### Using Steps Durable Endpoints support all the same step methods as Inngest functions. See the [Steps documentation](/docs/learn/inngest-steps) for the full reference: - [`step.run()`](/docs/learn/inngest-steps#step-run): Reliably execute the provided block by retrying upon failure - [`step.sleep()`](/docs/learn/inngest-steps#step-sleep): Pause execution for a duration - [`step.waitForEvent()`](/docs/learn/inngest-steps#step-wait-for-event-step-wait-for-event): Wait for an external event In order to start using steps within your API endpoints, you must first set up middleware to intercept HTTP requests. ```go import ( "context" "github.com/inngest/inngestgo/step" "github.com/inngest/inngestgo/stephttp" ) func setuphttp() { // provider adds inngest support to http handlers provider := stephttp.Setup(stephttp.SetupOpts{ Domain: "api.example.com", // add your api domain here. }) // provider allows you to wrap individual http handlers via `provider.servehttp`, // and provides stdlib-compatible middleware via `provider.middleware` http.HandleFunc("/users", provider.ServeHTTP(handleUsers)) // or, via middleware with, for example, chi: r := chi.NewRouter() r.Use(provider.Middleware) r.Get("/users", handleUsers) } ``` Once you've added the middleware, you can configure functions and execute steps within REST endpoints directly: ```go import ( "context" "github.com/inngest/inngestgo/step" "github.com/inngest/inngestgo/stephttp" ) func handleUsers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() stephttp.Configure(ctx, stephttp.FnOpts{ // Configure the function ID, removing IDs from the URL: ID: "/users/{id}" }) // Step 1: Authenticate (with full observability) auth, err := step.Run(ctx, "authenticate", func(ctx context.Context) (*AuthResult, error) { // You can chain steps as usual... return nil, nil }) if err != nil { http.Error(w, "Authentication failed", http.StatusUnauthorized) return } // ... } ``` Durable Endpoints are not yet available in the Python SDK. ## Requesting a Durable Endpoint Durable Endpoints behave like regular API endpoints on the success path. You can request them from your front-end (_or back-end_) using `fetch()` or your favorite query or http library: However, when a failure triggers retries, a Durable Endpoint returns a redirect to a dedicated endpoint on Inngest Cloud to poll the final result. Here is a snippet handling both the direct result and the redirected result after retries: ```typescript function handleError(error) { // ... } async function handleResult(result) { await res.json() // ... } fetch(`/api/your-durable-endpoint`) .then((res) => { if (res.redirected) { // follow the redirect fetch(res.url) .then(handleResult) .catch(handleError); } else { handleResult(res) } }) .catch(handleError); ``` As the Durable Endpoint redirects the request to a dedicated endpoint on Inngest's Cloud, `fetch()` cannot simply follow this redirect for you (_CORS policy_). Instead, you need to get the redirect URL (`res.url`) and trigger a new `fetch()` request. ## SDK Support | SDK | Support | Version | |-----|---------|---------| | TypeScript | ✅ Beta | >= 3.x (with `endpointAdapter`) | | Go | ✅ | >= v0.14.0 | ## Streaming Durable Endpoints can stream data back to clients in real-time using Server-Sent Events (SSE). Stream LLM tokens, progress updates, or any other data while keeping full durability guarantees. If a step fails and retries, streamed data from that step is automatically rolled back on the client. Read the [full guide](/docs/learn/durable-endpoints/streaming?ref=docs-durable-endpoints) for setup, client integration, rollback semantics, and more. ## Limitations Durable Endpoints is currently in beta. The following limitations apply: - **Flow control is not supported** — Features like concurrency limits and rate limiting are not available for Durable Endpoints - **POST body is not yet supported** — Prefer using query strings for passing data. POST body support is coming soon - **Standard HTTP responses only** — Durable Endpoints should return a standard HTTP response. Streaming responses [are supported](#streaming) ## Examples The [Durable Endpoints example page](/docs/examples/durable-endpoints) provides practical pattern examples such as parallel steps. The following demos are also available to check out and run locally with the Inngest Dev Server: } iconPlacement="top" > Clone this example locally to run it and explore the full source code. } iconPlacement="top" > Explore a more advanced example with a DeepResearch interface entirely built with Durable Endpoints. ## Further Reference - [Durable Endpoint - TypeScript SDK Reference](/docs/reference/typescript/durable-endpoints) - [Steps Overview](/docs/learn/inngest-steps) # Glossary Source: https://www.inngest.com/docs/learn/glossary Description: Definitions for core Inngest concepts: functions, steps, events, triggers, environments, apps, durable execution, flow control, and more. metaTitle = "Inngest Glossary | Key Terms Explained" This glossary serves as a quick reference for key terminology used in Inngest's documentation. The terms are organized alphabetically. ## Batching Batching is one of the methods offered by Inngest's [Flow Control](#flow-control). It allows you to process multiple events in a single batch function to improve efficiency and reduce system load. By handling high volumes of data in batches, you can optimize performance, minimize processing time, and reduce costs associated with handling individual events separately. Read more about [Batching](https://www.inngest.com/docs/guides/batching). ## Concurrency Management Concurrency management is one of the methods offered by Inngest's [Flow Control](#flow-control). It involves controlling the number of [steps](#inngest-step) executing simultaneously within a [function](#inngest-function). It prevents system overload by limiting how many processes run at once, which can be set at various levels such as globally, per-function, or per-user. This ensures efficient resource use and system stability, especially under high load conditions. Read more about [Concurrency Management](/docs/guides/concurrency). ## Debouncing Debouncing is one of the methods offered by Inngest's [Flow Control](#flow-control). It prevents a [function](#inngest-function) from being executed multiple times in rapid succession by ensuring it is only triggered after a specified period of inactivity. This technique helps to eliminate redundant function executions caused by quick, repeated events, thereby optimizing performance and reducing unnecessary load on the system. It is particularly useful for managing user input events and other high-frequency triggers. Read more about [Debouncing](/docs/guides/debounce). ## Durable Execution Durable Execution ensures that functions are fault-tolerant and resilient by handling failures and interruptions gracefully. It uses automatic retries and state persistence to allow [functions](#inngest-function) to continue running from the point of failure, even if issues like network failures or timeouts occur. This approach enhances the reliability and robustness of applications, making them capable of managing even complex and long-running workflows. Read more about [Durable Execution](/docs/learn/how-functions-are-executed). ## Fan-out Function A fan-out function (also known as "fan-out job") in Inngest is designed to trigger multiple [functions](#inngest-function) simultaneously from a single [event](#inngest-event). This is particularly useful when an event needs to cause several different processes to run in parallel, such as sending notifications, updating databases, or performing various checks. Fan-out functions enhance the efficiency and responsiveness of your application by allowing concurrent execution of tasks, thereby reducing overall processing time and enabling complex workflows. Read more about [Fan-out Functions](/docs/guides/fan-out-jobs). ## Flow Control Flow control in Inngest encompasses rate, throughput, priority, timing, and conditions of how functions are executed in regard to events. It helps optimize the performance and reliability of workflows by preventing bottlenecks and managing the execution order of tasks with tools like [steps](#inngest-step). Read more about [Flow Control](/docs/guides/flow-control). ## Function Replay Function replay allows developers to rerun failed functions from any point in their execution history. This is useful for debugging and correcting errors without needing to manually re-trigger events, thus maintaining workflow integrity and minimizing downtime. Read more about [Function Replay](/docs/platform/replay). ## Idempotency Idempotency is one of the methods offered by Inngest's [Flow Control](#flow-control). It guarantees that multiple identical requests have the same effect as a single request, preventing unintended side effects from repeated executions. By handling idempotency, you can avoid issues such as duplicate transactions or repeated actions, ensuring that your workflows remain accurate and dependable. Read more about [Handling idempotency](/docs/guides/handling-idempotency). ## Inngest App Inngest apps are higher-level constructs that group multiple [functions](#inngest-function) and configurations under a single entity. An Inngest app can consist of various functions that work together to handle complex workflows and business logic. This abstraction helps in organizing and managing related functions and their configurations efficiently within the Inngest platform. Read more about [Inngest Apps](/docs/apps/cloud). ## Inngest Client The Inngest client is a component that interacts with the Inngest platform. It is used to define and manage [functions](#inngest-function), send [events](#inngest-event), and configure various aspects of the Inngest environment. The client serves as the main interface for developers to integrate Inngest's capabilities into their applications, providing methods to create functions, handle events, and more. Read more about [Inngest Client](/docs/reference/typescript/v4/client/create). ## Inngest Cloud Inngest Cloud (also referred to as "Inngest UI" or inngest.com) is the managed service for running and managing your [Inngest functions](#inngest-function). It comes with multiple environments for developing, testing, and production. Inngest Cloud handles tasks like state management, retries, and scalability, allowing you to focus on building your application logic. Read more about [Inngest Cloud](/docs/platform/environments). ## Inngest Dev Server The Inngest Dev Server provides a local development environment that mirrors the production setup. It allows developers to test and debug their [functions](#inngest-function) locally, ensuring that code behaves as expected before deployment. This tool significantly enhances the development experience by offering real-time feedback and simplifying local testing. Read more about [Inngest Dev Server](/docs/local-development). ## Inngest Event An event is a trigger that initiates the execution of a [function](#inngest-function). Events can be generated from various sources, such as user actions or external services (third party webhooks or API requests). Each event carries data that functions use to perform their tasks. Inngest supports handling these events seamlessly. Read more about [Events](/docs/events). ## Inngest Function Inngest functions are the fundamental building blocks of the Inngest platform, which enable developers to run reliable background logic, from background jobs to complex workflows. They provide robust tools for retrying, scheduling, and coordinating complex sequences of operations. They are composed of [steps](#inngest-step) that can run independently and be retried in case of failure. Inngest functions are powered by [Durable Execution](#durable-execution), ensuring reliability and fault tolerance, and can be deployed on any platform, including serverless environments. Read more about [Inngest Functions](/docs/learn/inngest-functions). ## Inngest Step In Inngest, a "step" represents a discrete, independently retriable unit of work within a [function](#inngest-function). Steps enable complex workflows by breaking down a function into smaller, manageable blocks, allowing for automatic retries and state persistence. This approach ensures that even if a step fails, only that task is retried, not the entire function. Read more about [Inngest Steps](/docs/learn/inngest-steps). ## Priority Priority is one of the methods offered by Inngest's [Flow Control](#flow-control). It allows you to assign different priority levels to [functions](#inngest-function), ensuring that critical tasks are executed before less important ones. By setting priorities, you can manage the order of execution, improving the responsiveness and efficiency of your workflows. This feature is essential for optimizing resource allocation and ensuring that high-priority operations are handled promptly. Read more about [Priority](/docs/guides/priority). {/* Once we add the new o11y ## Observability Observability in Inngest refers to the ability to monitor and analyze the execution of functions. It includes features like real-time metrics, full logs, and historical data of function runs. This visibility helps in diagnosing issues, optimizing performance, and ensuring the reliability of applications. Read more about [Observability](). */} ## Rate Limiting Rate limiting is one of the methods offered by Inngest's [Flow Control](#flow-control). It controls the frequency of [function](#inngest-function) executions over a specified period to prevent overloading the system. It helps manage API calls and other resources by setting limits on how many requests or processes can occur within a given timeframe, ensuring system stability and fair usage. Read more about [Rate Limiting](/docs/guides/rate-limiting). ## SDK The Software Development Kit (SDK) is a collection of tools, libraries, and documentation that allows developers to easily integrate and utilize Inngest's features within their applications. The SDK simplifies the process of creating, managing, and executing functions, handling events, and configuring workflows. It supports multiple programming languages and environments, ensuring broad compatibility and ease of use. Currently, Inngest offers SDKs for TypeScript, Python, and Go. Read more about [Inngest SDKs](/docs/reference). ## Step Memoization Step memoization in Inngest refers to the technique of storing the results of steps so they do not need to be re-executed if already completed. This optimization enhances performance and reliability by preventing redundant computations and ensuring that each step's result is consistently available for subsequent operations. Read more about [Step Memoization](/docs/learn/how-functions-are-executed#secondary-executions-memoization-of-steps). ## Throttling Throttling is one of the methods offered by Inngest's [Flow Control](#flow-control). It controls the rate at which [functions](#inngest-function) are executed to prevent system overload. By setting limits on the number of executions within a specific timeframe, throttling ensures that resources are used efficiently and helps maintain the stability and performance of your application. It can be configured on a per-user or per-function basis, allowing for flexible and precise control over execution rates. Read more about [Throttling](/docs/guides/throttling). ## Next Steps - Explore Inngest through our [Quick Start](/docs/getting-started/nextjs-quick-start?ref=docs-glossary). - Learn about [Inngest Functions](/docs/learn/inngest-functions). - Learn about [Inngest Steps](/docs/learn/inngest-steps). - Understand how [Inngest functions are executed](/docs/learn/how-functions-are-executed). # How Inngest functions are executed: Durable Execution Source: https://www.inngest.com/docs/learn/how-functions-are-executed Description: How Inngest's Durable Execution Engine runs your functions: how steps are checkpointed, state is persisted, and retries work without re-running past steps. metaTitle = "How Inngest Functions Execute | Durable Execution" Most systems that offer durable execution require you to manage separate worker infrastructure, learn custom runtimes, or rewrite your application code to fit a specific programming model. Inngest takes a different approach: you write standard TypeScript, Python, or Go functions using a simple SDK, and Inngest handles execution durability, state persistence, retries, and flow control for you. There are no queues to configure, no workers to deploy, and no infrastructure to manage. Your functions run on your own compute, in any environment, including serverless. One of the core features of Inngest is Durable Execution. Durable Execution allows your functions to be fault-tolerant and resilient to failures. The end result is that your code, and therefore, your overall application, is more reliable. This page covers what Durable Execution is, how it works, and how it works with Inngest functions. {/* Note - this page is written a specific way for search optimization */} ## What is Durable Execution? Durable Execution is a fault-tolerant approach to executing code that is achieved by handling failures and interruptions gracefully with automatic retries and state persistence. This means that your code can continue to run even if there are issues like network failures, timeouts, infrastructure outages, and other transient errors. Key aspects of Durable Execution include: * **State persistance** - Function state is persisted outside of the function execution context. This enables function execution to be resumed from the point of failure on the same _or_ different infrastructure. * **Fault-tolerance** - Errors or exceptions are caught by the execution layer and are automatically retried. Retry behavior can be customized to handle the accepted number of retries and handle different types of errors. In practice, Durable Execution is implemented in the form of "durable functions," sometimes also called "durable workflows." Durable functions can throw errors or exceptions and automatically retry, resuming execution from the point of failure. Durable functions are designed to be long-running and stateful, meaning that they can persist state across function invocations and retries. ## How Inngest functions work Inngest functions are durable: they throw errors or exceptions, automatically retry from the point of failure, and can be stateful and long-running. Inngest functions use "**Steps**" to define the execution flow of a function. Each step: * Is a unit of work that can be run and retried independently. * Captures any error or exception thrown within it. * Will not be re-executed if it has already been successfully executed. * Returns state (_data_) that can be used by subsequent steps. * Can be executed in parallel or sequentially, depending on the function's configuration. Complex functions can consist of many steps. This allows a long-running function to be broken down into smaller, more manageable units of work. As each step is retried independently, and the function can be resumed from the point of failure, avoiding unnecessary re-execution of work. In comparison, some Durable Execution systems modify the runtime environment to persist state or interrupt errors or exceptions. Inngest SDKs are written using standard language primitives, which enables Inngest functions to run in any environment or runtime - including serverless environments - without modification. ### How steps are executed Inngest functions are defined with a series of steps that define the execution flow of the function. Each step is defined with a unique ID and a function that defines the work to be done. The data returned can be used by subsequent steps. Inngest functions execute incrementally, _step by step_. As a function is executed, the results of each step are returned to Inngest and persisted in a managed function state store. The steps that successfully executed are [_memoized_](https://en.wikipedia.org/wiki/Memoization). The function then resumes, skipping any steps that have already been completed and the SDK injects the data returned by the previous step into the function. Each step in your function is executed as **a separate HTTP request**. Any non-deterministic logic (such as DB calls or API calls) must be placed within a `step.run()` call to ensure it executes efficiently and correctly in the context of the execution model. Let's look at an example of a function and walk through how it is executed: ```typescript inngest.createFunction( { id: "import-contacts", triggers: { event: "contacts/csv.uploaded" } }, // The function handler: async ({ event, step }) => { await step.run("parse-csv", async () => { return await parseCsv(event.data.fileURI); }); await step.run("normalize-raw-csv", async () => { getNormalizedColumnNames(); return normalizeRows(rows, normalizedColumnMapping); }); await step.run("input-contacts", async () => { return await importContacts(normalizedRows); }); return { results }; } ); ``` ### Initial execution 1. When the function is first called, the _function handler_ is called with only the `event` payload data sent. 2. When the first step is discovered, the `"parse-csv"` step is run. As the step has not been executed before, the step's code (the callback function) is run and the result is captured. 3. The function does not continue executing beyond this step. Each SDK uses a different method to interrupt the function execution before running any more code in your function handler. 4. Internally, the step's ID (`"parse-csv"`) is hashed as the state identifier to be used in future executions. Additionally, the steps' index (`0` in this case) is also included in the result. 5. The result is sent back to Inngest and persisted in the function state store. ### Secondary executions - Memoization of steps Each of the subsequent steps leverages the state of previous executions and memoization. Here's how it works: 6. The function is re-executed, this time with the `event` payload data and the state of the previous execution in JSON. 7. The next step is discovered (`"parse-csv"`). 8. The previous result is found in the state of previous executions. Internally, the SDK uses the hash of the step name to look up the result in the state data. 9. The step's code is not executed, instead the SDK injects the result into the return value of `step.run`, (in this example, the data will be returned as `rows`). 10. The function continues execution until the next step is discovered (`"normalize-raw-csv"`). 11. The step's code is executed and the result is returned to Inngest (in the same approach as steps 2-5 above). ### Error handling Some steps may throw errors or exceptions during execution. Here's how error handling works within function execution: 12. If an error occurs during the execution of a step (for example, `"input-contacts"`), the function is interrupted and the error is caught by the SDK. 13. The error is serialized and returned to Inngest. The number of attempts are logged and the error is persisted in the function state store. 14. Depending on the number of attempts configured for the function, the function may be retried (see: [Error handling](/docs/guides/error-handling)): * If the the function _has not_ exhausted the number of attempts, the function is re-executed from the point of failure with the state of all previous step executions. The step is re-executed and follows the same process as above (see: steps 6-11). * If the function _has_ exhausted the number of attempts, the function is re-executed with the error thrown. The function can then catch and handle the error as desired (see: [Handling a failing step](/docs/guides/error-handling#handling-a-failing-step)). {/* TODO - Add how parallel steps are executed differently (more complex topic) */} To learn about how determinism is handled and how you can version functions, read the [Versioning long running functions](/docs/learn/versioning) guide. ## How Inngest's execution model compares to Temporal Temporal is a pull-based durable execution platform, and teams often evaluate both Temporal and Inngest when choosing how to build reliable, long-running workflows. The two systems take fundamentally different approaches to the problem. **Infrastructure and setup.** Temporal requires you to run and manage a Temporal Server cluster (or use Temporal Cloud), along with separate worker processes that poll for tasks. Inngest requires no separate infrastructure. Your functions run on your existing compute, whether that's a serverless platform, a container, or a traditional server. Inngest provides two ways to connect your functions to its execution engine: [**serve**](/docs/learn/serving-inngest-functions) for serverless environments using HTTP endpoints, and [**connect**](/docs/setup/connect) for persistent worker-style deployments. Both approaches let Inngest handle orchestration, state management, and retries without requiring you to manage queue infrastructure or task polling. **Programming model.** Temporal uses a deterministic replay model where your entire workflow function is re-executed from the beginning on each step, relying on an internal event history to skip completed work. This requires developers to learn and follow strict determinism rules. Inngest uses a step-based memoization model where each step runs once, its result is persisted, and subsequent executions skip completed steps by injecting their stored results. This uses standard language features with no custom runtime rules. **Flow control.** Inngest includes [concurrency controls](/docs/functions/concurrency), [prioritization](/docs/guides/priority), [throttling](/docs/guides/throttling), [debouncing](/docs/guides/debounce), [rate limiting](/docs/guides/rate-limiting), and [idempotency](/docs/guides/handling-idempotency) as built-in features of the SDK, available in every environment: local development, self-hosted, and cloud. Temporal has recently introduced priority and fairness features for task queues, but fairness is a paid feature available only in Temporal Cloud. **Self-hosting.** Inngest can be [self-hosted](/docs/self-hosting) as a single binary with SQLite or Postgres as a backing store. Temporal self-hosting requires running multiple server components with a Cassandra or Postgres dependency, along with separate worker infrastructure. | | Inngest | Temporal | | --- | --- | --- | | **Infrastructure** | No separate infrastructure. Serve (serverless) or Connect (workers) on your compute. | Requires Temporal Server cluster + separate worker processes. | | **Programming model** | Step-based memoization with standard language primitives. | Deterministic replay with strict runtime rules. | | **Flow control** | Built-in: [concurrency](/docs/functions/concurrency), [priority](/docs/guides/priority), [throttling](/docs/guides/throttling), [debounce](/docs/guides/debounce), [rate limiting](/docs/guides/rate-limiting). Available everywhere. | Priority and fairness features are paid, cloud-only. | | **Self-hosting** | [Single binary with SQLite or Postgres](/docs/self-hosting). | Multi-component cluster with Cassandra/Postgres. | | **Serverless support** | Native. Designed for serverless-first environments. | Requires persistent worker processes; not serverless-native. | ## Conclusion Inngest functions use steps and memoization to execute functions incrementally and durably. This approach ensures that functions are fault-tolerant and resilient to failures. By breaking down functions into steps, Inngest functions can be retried and resumed from the point of failure. This approach ensures that your code is more reliable and can handle transient errors gracefully. ## Further reading More information on Durable Execution in Inngest: - Blog post: ["How we built a fair multi-tenant queuing system"](/blog/building-the-inngest-queue-pt-i-fairness-multi-tenancy) - Blog post: ["Debouncing in Queueing Systems: Optimizing Efficiency in Asynchronous Workflows"](/blog/debouncing-in-queuing-systems-optimizing-efficiency-in-async-workflows) - Blog post: ["Accidentally Quadratic: Evaluating trillions of event matches in real-time"](/blog/accidentally-quadratic-evaluating-trillions-of-event-matches-in-real-time) - Blog post: ["Queues aren't the right abstraction"](/blog/queues-are-no-longer-the-right-abstraction) # Inngest Functions Source: https://www.inngest.com/docs/learn/inngest-functions Description: Inngest functions are durable, retriable units of background logic triggered by events or cron schedules. Write standard TypeScript, Python, or Go functions. import { RiGitPullRequestFill, RiGuideFill, RiTimeLine, RiCalendarLine, RiMistFill, } from "@remixicon/react"; metaTitle = "Inngest Functions | Durable Background Logic"; Inngest functions are durable, retriable units of background logic that run on your own compute and are triggered by events, cron schedules, or webhooks. Use them for background jobs, scheduled work, and multi-step workflows that need automatic retries, step-level state, and observability without adding a queue or workflow engine. You write standard TypeScript, Python, or Go code, then expose functions with [`serve()`](/docs/learn/serving-inngest-functions) or [Connect](/docs/setup/connect) so Inngest can invoke and coordinate runs from the platform. ## What are Inngest functions? An Inngest function is a regular function wrapped with Inngest trigger and execution metadata. Inngest starts a run when a matching event, schedule, or webhook arrives, then records each step so failed work can retry from the last successful checkpoint instead of restarting from scratch. At a high level, an Inngest function has three core parts: } href={'/docs/features/events-triggers'}> A list of Events, Cron schedules or webhook events that trigger Function runs. } href={'/docs/guides/flow-control'}> Control how Function runs get distributed in time with Concurrency, Throttling and more. } href={'/docs/learn/inngest-functions'}> Transform your Inngest Function into a workflow with retriable checkpoints. ```ts inngest.createFunction({ id: "sync-systems", // A Function is triggered by events triggers: { event: "auto/sync.request" }, // Easily add Throttling with Flow Control throttle: { limit: 3, period: "1min"}, }, async ({ step }) => { // step is retried if it throws an error await step.run("get-data", async () => { return getDataFromExternalSource(); }); // Steps can reuse data from previous ones await step.run("save-data", async () => { return db.syncs.insertOne(data); }); } ); ``` ```python @inngest_client.create_function( id="sync-systems", # trigger (event or cron) trigger=inngest.TriggerEvent(event="auto/sync.request"), ) def sync_systems(ctx: inngest.ContextSync) -> None: # step is retried if it throws an error data = ctx.step.run("Get data", get_data_from_external_source) # Steps can reuse data from previous ones ctx.step.run("Save data", db.syncs.insert_one, data) ``` ```go !snippet:path=snippets/go/docs/functions/sync_systems_function.go ``` {/* Increase your Inngest Functions durability by leveraging: - **[Retries features](/docs/guides/error-handling)** - Configure a custom retry policy, handle rollbacks and idempotency. - **[Cancellation features](/docs/features/inngest-functions/cancellation)** - Dynamically or manually cancel in-progress runs to prevent unnecessary work. - **[Versioning best practices](/docs/learn/versioning)** - Strategies to gracefully introducing changes in your Inngest Functions. */} ## Using Inngest Functions Start using Inngest Functions by using the pattern that fits your use case: } href={'/docs/guides/background-jobs'}> Run long-running tasks out of the critical path of a request. } href={'/docs/guides/delayed-functions'}> Schedule Functions that run in the future. } href={'/docs/guides/scheduled-functions'}> Build Inngest Functions as CRONs. } href={'/docs/learn/inngest-steps'}> Start creating workflows by leveraging Inngest Function Steps. ## Learn more about Functions and Steps Functions and Steps are powered by Inngest's Durable Execution Engine. Learn about its inner working by reading the following guides: } href={'/docs/learn/how-functions-are-executed'}> A deep dive into Inngest's Durable Execution Engine with a step-by-step workflow run example. } href={'/docs/learn/inngest-steps'}> Discover by example how steps enable more reliable and flexible functions with step-level error handling, conditional steps and waits. ## SDK References } > API reference } > API reference } > Go API reference # Inngest Steps Source: https://www.inngest.com/docs/learn/inngest-steps Description: Steps are the building blocks of Inngest functions. Each step is memoized, retriable, and can sleep, wait for events, or invoke other functions independently. metaTitle = "Steps in Inngest | Checkpointed, Retriable Units of Work"; Inngest steps are checkpointed, retriable units of work inside [Inngest functions](/docs/learn/inngest-functions) and [Durable Endpoints](/docs/learn/durable-endpoints). Put side effects, API calls, sleeps, waits, and function invocations in steps so Inngest can memoize results, retry failures independently, and resume long-running work from the last successful checkpoint. Steps are the building blocks of durable execution. They let a function pause without holding compute, recover individual units of work after an error, and compose multi-step workflows from normal TypeScript, Python, or Go code. On this page, you will learn about the benefits of using steps, and get an overview of the available step methods. ## Benefits of Using Steps - **Improved reliability**: structured steps enable precise control and handling of each task within a function. - **Error handling**: capturing and managing errors at the step level means better error recovery. - **Retry mechanism**: failing steps can be retried and recovered independently, without re-executing other successful steps. - **Independent testing**: each step can be tested and debugged independently from others. - **Improved code readability**: modular approach makes code easier to navigate and refactor. If you'd like to learn more about how Inngest steps are executed, check the ["How Inngest functions are executed"](/docs/learn/how-functions-are-executed) page. ## Anatomy of an Inngest Step The first argument of every Inngest step method is an `id`. Each step is treated as a discrete task which can be individually retried, debugged, or recovered. Inngest uses the ID to memoize step state across function versions. ```typescript export default inngest.createFunction( { id: "import-product-images", triggers: { event: "shop/product.imported" } }, async ({ event, step }) => { await step.run( // step ID "copy-images-to-s3", // other arguments, in this case: a handler async () => { return copyAllImagesToS3(event.data.imageURLs); }); } ); ``` ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( client, // config inngestgo.FunctionOpts{ ID: "import-product-images", }, // trigger (event or cron) inngestgo.EventTrigger("shop/product.imported", nil), // handler function func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // Here goes the business logic // By wrapping code in steps, it will be retried automatically on failure s3Urls, err := step.Run("copy-images-to-s3", func() ([]string, error) { return copyAllImagesToS3(input.Event.Data["imageURLs"].([]string)) }) if err != nil { return nil, err } return nil, nil }, ) ``` ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="import-product-images", event="shop/product.imported" ) async def import_product_images(ctx: inngest.Context): uploaded_image_urls = await ctx.step.run( # step ID "copy-images-to-s3", # other arguments, in this case: a handler lambda: copy_all_images_to_s3(ctx.event.data["image_urls"]) ) ``` The ID is also used to identify the function in the Inngest system. Inngest's SDK also records a counter for each unique step ID. The counter increases every time the same step is called. This allows you to run the same step in a loop, without changing the ID. Place non-deterministic side effects, such as database writes or API calls, inside `step.run()` so Inngest can checkpoint the result and avoid re-running completed work during retries. ## Available Step Methods ### step.run() This method executes a defined piece of code. Code within `step.run()` is automatically retried if it throws an error. When `step.run()` finishes successfully, the response is saved in the function run state and the step will not re-run. Use it to run synchronous or asynchronous code as a retriable step in your function. ```typescript export default inngest.createFunction( { id: "import-product-images", triggers: { event: "shop/product.imported" } }, async ({ event, step }) => { // Here goes the business logic // By wrapping code in steps, it will be retried automatically on failure await step.run("copy-images-to-s3", async () => { return copyAllImagesToS3(event.data.imageURLs); }); } ); ``` ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "import-product-images", }, inngestgo.EventTrigger("shop/product.imported", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // Here goes the business logic // By wrapping code in steps, it will be retried automatically on failure s3Urls, err := step.Run("copy-images-to-s3", func() ([]string, error) { return copyAllImagesToS3(input.Event.Data["imageURLs"].([]string)) }) if err != nil { return nil, err } return nil, nil }, ) ``` ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="import-product-images", event="shop/product.imported" ) async def import_product_images(ctx: inngest.Context): # Here goes the business logic # By wrapping code in steps, it will be retried automatically on failure uploaded_image_urls = await ctx.step.run( # step ID "copy-images-to-s3", # other arguments, in this case: a handler lambda: copy_all_images_to_s3(ctx.event.data["image_urls"]) ) ``` `step.run()` acts as a code-level transaction. The entire step must succeed to complete. ### step.sleep() This method pauses execution for a specified duration. Even though it seems like a `setInterval`, your function does not run for that time (you don't use any compute). Inngest handles the scheduling for you. Use it to add delays or to wait for a specific amount of time before proceeding. At maximum, functions can sleep for a year (seven days for the [free tier plans](/pricing)). ```typescript export default inngest.createFunction( { id: "send-delayed-email", triggers: { event: "app/user.signup" } }, async ({ event, step }) => { await step.sleep("wait-a-couple-of-days", "2d"); // Do something else } ); ``` ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "send-delayed-email", }, inngestgo.EventTrigger("app/user.signup", nil), // handler function func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { step.Sleep("wait-a-couple-of-days", 2*time.Day) return nil, nil }, ) ``` ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="send-delayed-email", trigger=inngest.TriggerEvent(event="app/user.signup") ) async def send_delayed_email(ctx: inngest.Context): await ctx.step.sleep("wait-a-couple-of-days", datetime.timedelta(days=2)) # Do something else ``` ### step.sleepUntil() / step.sleep_until() This method pauses execution until a specific date time. Any date time string in the format accepted by the Date object, for example `YYYY-MM-DD` or `YYYY-MM-DDHH:mm:ss`. At maximum, functions can sleep for a year (seven days for the [free tier plans](/pricing)). ```typescript export default inngest.createFunction( { id: "send-scheduled-reminder", triggers: { event: "app/reminder.scheduled" } }, async ({ event, step }) => { new Date(event.data.remind_at); await step.sleepUntil("wait-for-the-date", date); // Do something else } ); ``` Go SDK does not have a `sleepUntil` method. Use `step.Sleep()` with a calculated duration instead. ```python import inngest from src.inngest.client import inngest_client from datetime import datetime @inngest_client.create_function( fn_id="send-scheduled-reminder", trigger=inngest.TriggerEvent(event="app/reminder.scheduled") ) async def send_scheduled_reminder(ctx: inngest.Context): date = datetime.fromisoformat(ctx.event.data["remind_at"]) await ctx.step.sleep_until("wait-for-the-date", date) # Do something else ``` ### step.waitForEvent() / step.wait_for_event() This method pauses a run's execution until a specific event is received. ```typescript export default inngest.createFunction( { id: "send-onboarding-nudge-email", triggers: { event: "app/account.created" } }, async ({ event, step }) => { await step.waitForEvent( "wait-for-onboarding-completion", { event: "app/onboarding.completed", timeout: "3d", if: "event.data.userId == async.data.userId" } ); // Do something else } ); ``` ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/errors" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "send-delayed-email", }, inngestgo.EventTrigger("app/user.signup", nil), // handler function func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // Sample from the event stream for new events. The function will stop // running and automatically resume when a matching event is found, or if // the timeout is reached. fn, err := step.WaitForEvent[FunctionCreatedEvent]( ctx, "wait-for-activity", step.WaitForEventOpts{ Name: "Wait for a function to be created", Event: "api/function.created", Timeout: time.Hour * 72, // Match events where the user_id is the same in the async sampled event. If: inngestgo.StrPtr("event.data.user_id == async.data.user_id"), }, ) if err == step.ErrEventNotReceived { // A function wasn't created within 3 days. Send a follow-up email. _, _ = step.Run(ctx, "follow-up-email", func(ctx context.Context) (any, error) { // ... return true, nil }) return nil, nil } return nil, nil }, ) ``` ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="send-onboarding-nudge-email", trigger=inngest.TriggerEvent(event="app/account.created") ) async def send_onboarding_nudge_email(ctx: inngest.Context): onboarding_completed = await ctx.step.wait_for_event( "wait-for-onboarding-completion", event="app/wait_for_event.fulfill", if_exp="event.data.user_id == async.data.user_id", timeout=datetime.timedelta(days=1), ); # Do something else ``` ### step.waitForSignal() / step.wait_for_signal() This method pauses a run's execution until a specific signal is received via our SDK or API. Signals must be unique across runs, and offer an API to resume specific runs given a unique signal vs events. ```typescript export default inngest.createFunction( { id: "send-onboarding-nudge-email", triggers: { event: "app/account.created" } }, async ({ event, step }) => { await step.waitForSignal( "wait-for-specific-signal", { signal: "task/0e7d16d1-8335-4b93-9122-76e7cc6d9eb6", timeout: "3d" } ); // Do something with signal data } ); ``` ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/errors" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "send-delayed-email", }, inngestgo.EventTrigger("app/user.signup", nil), // handler function func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // Sample from the event stream for new events. The function will stop // running and automatically resume when a matching event is found, or if // the timeout is reached. signal, err := step.WaitForSignal[string]( ctx, "wait-for-signal", step.WaitForSignalOpts{ Name: "Wait for a signal to be POSTed", Signal: "task/0e7d16d1-8335-4b93-9122-76e7cc6d9eb6", Timeout: time.Hour * 72, }, ) if err == step.ErrSignalNotReceived { // A signal wasn't received within 3 days. Send a follow-up email. } return nil, nil }, ) ``` ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="send-onboarding-nudge-email", trigger=inngest.TriggerEvent(event="app/account.created") ) async def send_onboarding_nudge_email(ctx: inngest.Context): onboarding_completed = await ctx.step.wait_for_signal( "wait-for-onboarding-completion", signal="task/0e7d16d1-8335-4b93-9122-76e7cc6d9eb6", if_exp="event.data.user_id == async.data.user_id", timeout=datetime.timedelta(days=1), ); # Do other steps ``` ### step.invoke() This method is used to asynchronously call another Inngest function ([written in any language SDK](/blog/cross-language-support-with-new-sdks)) and handle the result. Invoking other functions allows you to easily re-use functionality and compose them to create more complex workflows or map-reduce type jobs. This method comes with its own configuration, which enables defining specific settings like concurrency limits. ```typescript // A function we will call in another place in our app inngest.createFunction( { id: "compute-square", triggers: { event: "calculate/square" } }, async ({ event }) => { return { result: event.data.number * event.data.number }; // Result typed as { result: number } } ); // In this function, we'll call `computeSquare` inngest.createFunction( { id: "main-function", triggers: { event: "main/event" } }, async ({ step }) => { await step.invoke("compute-square-value", { function: computeSquare, data: { number: 4 }, // input data is typed, requiring input if it's needed }); return `Square of 4 is ${square.result}.`; // square.result is typed as number } ); ``` ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/errors" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "send-delayed-email", }, inngestgo.EventTrigger("app/user.signup", nil), // handler function func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // Invoke another function and wait for its result result, err := step.Invoke[any]( ctx, "invoke-email-function", step.InvokeOpts{ FunctionID: "send-welcome-email", // Pass data to the invoked function Data: map[string]any{ "user_id": input.Event.Data["user_id"], "email": input.Event.Data["email"], }, // Optional: Set a concurrency limit Concurrency: step.ConcurrencyOpts{ Limit: 5, Key: "user-{{event.data.user_id}}", }, }, ) if err != nil { return nil, err } return result, nil }, ) ``` ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="fn-1", trigger=inngest.TriggerEvent(event="app/fn-1"), ) async def fn_1(ctx: inngest.Context) -> None: return "Hello!" @inngest_client.create_function( fn_id="fn-2", trigger=inngest.TriggerEvent(event="app/fn-2"), ) async def fn_2(ctx: inngest.Context) -> None: output = await ctx.step.invoke( "invoke", function=fn_1, ) # Prints "Hello!" print(output) ``` ### step.sendEvent() / step.send_event() This method sends events to Inngest to invoke functions with a matching event. Use `sendEvent()` when you want to trigger other functions, but you do not need to return the result. It is useful for example in [fan-out functions](/docs/guides/fan-out-jobs). ```typescript export default inngest.createFunction( { id: "user-onboarding", triggers: { event: "app/user.signup" } }, async ({ event, step }) => { // Do something await step.sendEvent("send-activation-event", { name: "app/user.activated", data: { userId: event.data.userId }, }); // Do something else } ); ``` Go SDK does not have a dedicated `step.sendEvent()` method. Use the Inngest client's `Send()` method within a `step.Run()` instead. ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn(ctx: inngest.Context) -> list[str]: return await ctx.step.send_event("send", inngest.Event(name="foo")) ``` ## Further reading - [Quick Start](/docs/getting-started/nextjs-quick-start?ref=docs-inngest-steps): learn how to build complex workflows. - ["How Inngest functions are executed"](/docs/learn/how-functions-are-executed): Learn more about Inngest's execution model, including how steps are handled. - Docs guide: ["Multi-step functions"](/docs/learn/inngest-steps). # Security Source: https://www.inngest.com/docs/learn/security Description: How Inngest secures communication with your servers: request signing, signing key rotation, environment isolation, and secure deployment best practices. metaTitle = "Inngest Security | Signing Keys & Best Practices" Security is a primary consideration when moving systems into production. In this section we'll dive into how Inngest handles security, including endpoint security, encryption, standard practices, and how to add SAML authentication to your account. Learn more about * [Inngest platform security](#inngest-platform-security) * [Best practices](#security-best-practices) that you can apply ## Inngest platform security ### Compliance, audits, and reports Inngest is [SOC 2 Type II compliant](/blog/soc2-compliant?ref=docs-security). Our company and platform is regularly audited to adhere to the standards of SOC 2. This ensures that we have the necessary controls in place to protect our customers' data and ensure the security and privacy of their information. Our platform and SDKs undergo periodic independent security assessments including penetration testing and red-team simulated attacks. For more information on our security practices, or to request a copy of our SOC 2 report, please visit our [trust center](https://trust.inngest.com/). ### End to end encryption All data in Inngest databases is encrypted at rest and encrypted in transit, including between Inngest's servers and your servers. For an added layer of encryption and control of your data, install [encryption middleware](#encryption-middleware) and bring your own encryption key. ### Signing keys and SDK security In addition to TLS encryption, all requests between the Inngest platforms and your servers are signed with a [signing key](/docs/platform/signing-keys). The signing key is a pre-shared key which is unique to each environment. The Inngest SDKs all automatically verify the signature via the `serve` endpoint adapter. Every request includes a signature with an embedded timestamp as to reject old requests to prevent replay attacks. **It's important that the signing key is kept secret.** If your signing key is exposed, it puts the security of your endpoints at risk. Note that it's possible to [rotate signing keys](/docs/platform/manage/rotating-keys) with zero downtime. Signing keys are also leveraged as an authentication mechanism to interact with the Inngest API for apps (e.g. checkpointing runs) as well as enabling [`connect` workers](/docs/setup/connect) to establish a connection with Inngest servers. ### API Keys For programmatic access to [the Inngest REST API](https://api-docs.inngest.com/), you can create [API keys](/docs/platform/api-keys), scoping them to a specific environment. We recommend using API keys if or when you are writing custom scripts, tooling, or performing actions in CI/CD pipelines. ### SAML Enterprise users can enable SAML authentication to access their account. In order to enable SAML, you must: 1. Reach out to your account manager and request a SAML integration. 2. From there, we'll request configuration related to your SAML provider. This differs depending on your provider, and may include: 1. A metadata URL; an SSO URL; An IdP entity ID; an IdP x.509 certificate, and so on. 3. Your account manager will then send you the ACS and Metadata URL used to configure your account. 4. Your account manager will work with you to correctly map attributes to ensure fully functioning sign in. It's important to note that once SAML is enabled, users **must** sign in via SAML. To learn more about enterprise plans for Inngest, [contact our team here](/contact?ref=docs-security). ### App syncing & function registration Functions are defined within your codebase and run on your own infrastructure. Inngest must "[sync](/docs/apps/cloud)" your application to read the current function configurations. For apps that use `serve` on public HTTP endpoints, Inngest syncs your application using one of two methods: * **Direct sync**: Inngest sends a signed `PUT` request to your application's endpoint, your application's configuration including all function config is returned synchronously back to Inngest. This is the default method as of these SDK versions: TS [v3.31.0](https://github.com/inngest/inngest-js/releases/tag/inngest%403.31.0), Python [0.4.18](https://github.com/inngest/inngest-py/releases/tag/0.4.18). * **Indirect sync**: Prior to direct sync, indirect syncs were the default. They can still be used to initiate a sync from your server. The handshake would be initiated by sending a `PUT` request to your endpoint which would then serialize and send the app configuration to the Inngest API, authenticating with the signing key set in your application. The SDK only sends requests to `https://api.inngest.com` unless configured to work with a self-hosted Inngest server. For `connect` workers, app configuration is synced upon startup, directly with the Inngest API. ## Security best practices ### Encryption middleware Inngest runs functions automatically, based off of event data that you send to Inngest. Additionally, Inngest runs steps transactionally, and stores the output of each `step.run` within function state. This may contain regulated, sensitive data. **If you process sensitive data, we _strongly_ recommend, and sometimes require, end-to-end encryption enabled in our SDKs**. [End-to-end encryption middleware](/docs/features/middleware/encryption-middleware) intercepts requests, responses, and SDK logic on your own servers. With end to end encryption, data is encrypted on your servers with a key that only you have access to. The following applies: - All data in `event.data.encrypted` is encrypted _before_ it leaves your servers. Inngest can never read data in this object. - All step output and function output is encrypted _before_ it leaves your servers. Inngest only receives the encrypted values, and can never read this data. Function state is sent fully encrypted to the SDKs. The SDKs decrypt data on your servers and then resume as usual. - Middleware automatically decrypts the data as it's received within your application. With this enabled, even in the case of unexpected issues your data is encrypted and secure. This greatly improves the security posture for sensitive data. ### Firewall allowlist with Inngest IP addresses Inngest's servers make outbound requests to your application and it's advised to add the Inngest IPs to your firewall allowlist. For security and networking purposes, you may need to know the IP addresses that Inngest uses for outbound requests to your functions and webhooks. These IP addresses are used by Inngest's infrastructure to make authenticated requests to your endpoints. You can find the current list of IP addresses at: - [IPv4 addresses](https://www.inngest.com/ips-v4) - `https://www.inngest.com/ips-v4` - [IPv6 addresses](https://www.inngest.com/ips-v6) - `https://www.inngest.com/ips-v6` These IP ranges are used for all Inngest function invocations and webhook deliveries. If you need to whitelist these IPs in your firewall or security groups, please use the complete ranges listed on these pages. ### Key rotation Signing, event, and API keys can all be rotated using built in tools. If you need to rotate signing and event keys for your app, follow the steps in [this guide here](/docs/platform/manage/rotating-keys). # Serving Inngest functions Source: https://www.inngest.com/docs/learn/serving-inngest-functions metaTitle = "Serve Inngest Functions via HTTP | Framework Setup" description = `Set up the Inngest serve handler in Next.js, Express, Python, or any supported framework to expose your functions as an HTTP endpoint Inngest can call remotely.` hidePageSidebar = true; To let Inngest invoke your functions, your application needs a secure connection back to Inngest. Most web apps do this by serving an HTTP endpoint, usually at `/api/inngest`, that exposes the functions defined with the SDK. Use this page to choose between `serve()` and `connect()`, set up the right framework handler, and confirm the deployment requirements for your app. _Last updated: June 16, 2026._ There are two ways to connect your app to Inngest:

Serve your Inngest functions by creating an HTTP endpoint in your application.

**Ideal for**:

  • Serverless platforms like Vercel, Lambda, etc.
  • Adding Inngest to an existing API.
  • Deploying with your existing CI/CD pipeline.

Connect to Inngest's servers using an outbound WebSocket connection.

**Ideal for**:

  • Container runtimes (Kubernetes, Docker, etc.)
  • Latency sensitive applications
  • Horizontal scaling with workers
Inngest functions are portable, so you can migrate between `serve()` and `connect()` as well as cloud providers. ## Serving Inngest functions Inngest provides a `serve()` handler which adds an API endpoint to your router. You expose your functions to Inngest through this HTTP endpoint. To make automated deploys much easier, **the endpoint needs to be defined at `/api/inngest`** (though you can [change the API path](/docs/reference/typescript/v4/serve#serve-client-functions-options)). ```ts {{ title: "./api/inngest.ts" }} // All serve handlers have the same arguments: serve({ client: inngest, // a client created with new Inngest() functions: [fnA, fnB], // an array of Inngest functions to serve, created with inngest.createFunction() /* Optional extra configuration */ }); ``` ## Supported frameworks and platforms
* [Astro](#framework-astro) * [AWS Lambda](#framework-aws-lambda) * [Bun](#bun-serve) * [Cloudflare Pages](#framework-cloudflare-pages-functions) * [Cloudflare Workers](#framework-cloudflare-workers) * [DigitalOcean Functions](#framework-digital-ocean-functions) * [ElysiaJS](#framework-elysia-js) * [Express](#framework-express) * [Fastify](#framework-fastify) * [Fresh (Deno)](#framework-fresh-deno) * [Google Cloud Run Functions](#framework-google-cloud-run-functions) * [Firebase Cloud functions](#framework-firebase-cloud-functions) * [H3](#framework-h3) * [Hono](#framework-hono) * [Koa](#framework-koa) * [NestJS](#framework-nest-js) * [Next.js](#framework-next-js) * [Nitro](#framework-nitro) * [Nuxt](#framework-nuxt) * [Redwood](#framework-redwood) * [Remix](#framework-remix) * [Supabase Edge Functions](#framework-supabase-edge-functions) * [SvelteKit](#framework-svelte-kit) * [Tanstack Start](#framework-tanstack-start)
You can also create a custom serve handler for any framework or platform not listed here - [read more here](#custom-frameworks). Want us to add support for another framework? Open an issue on [GitHub](https://github.com/inngest/website) or tell us about it on our [Discord](/discord). ### Framework: Astro Add the following to `./src/pages/api/inngest.ts`: ```ts { GET, POST, PUT } = serve({ client: inngest, functions, }); ``` See the [Astro example](https://github.com/inngest/inngest-js/tree/main/examples/framework-astro) for more information. ### Framework: AWS Lambda We recommend using [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) to trigger your functions, as these require no other configuration or cost. Alternatively, you can use an API Gateway to route requests to your Lambda. The handler supports [API Gateway V1](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) and [API Gateway V2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). If you are running API Gateway behind a proxy or have some other configuration, you may have to specify the `serveOrigin` and `servePath` options when calling `serve()` to ensure Inngest knows the URL where you are serving your functions. See [Configuring the API path](/docs/reference/typescript/v4/serve#serve-client-functions-options) for more details. ```ts // Your own function handler = serve({ client: inngest, functions: [fnA], }); ``` ### Bun.serve() You can use the `inngest/bun` handler with `Bun.serve()` for a lightweight Inngest server: ```ts {{ title: "index.ts" }} Bun.serve({ port: 3000, routes: { // ...other routes... "/api/inngest": serve({ client: inngest, functions }), }, }); ``` See the [Bun example](https://github.com/inngest/inngest-js/tree/main/examples/bun) for more information. ### Framework: Cloudflare Pages Functions You can import the Inngest API server when using Cloudflare pages functions within `/functions/api/inngest.js`: ```ts // Your own function onRequest = serve({ client: inngest, functions: [fnA], }); ``` Cloudflare Pages Functions run on the Workers runtime. Enable Node.js compatibility so Inngest can use `AsyncLocalStorage` during function execution. ### Framework: Cloudflare Workers You can export `"inngest/cloudflare"`'s `serve()` as your Cloudflare Worker: ```ts export default { fetch: serve({ client: inngest, functions: [fnA], // We suggest explicitly defining the path to serve Inngest functions servePath: "/api/inngest", }), }; ``` Enable Node.js compatibility so Inngest can use `AsyncLocalStorage` during function execution. Add `nodejs_compat` to your compatibility flags and use a compatibility date of `2024-09-23` or later. To automatically pass environment variables defined with Wrangler to Inngest function handlers, use the [Cloudflare Workers bindings middleware](/docs/examples/middleware/cloudflare-workers-environment-variables). #### Local development with Wrangler When developing locally with Wrangler and the `--remote` flag, your code is deployed and run remotely. To use this with a local Inngest Dev Server, you must use a tool such as [ngrok](https://ngrok.com/) or [localtunnel](https://theboroer.github.io/localtunnel-www/) to allow access to the Dev Server from the internet. ```sh ngrok http 8288 ``` ```toml {{ title: "wrangler.toml" }} [vars] # The URL of your tunnel. This enables the "cloud" worker to access the local Dev Server INNGEST_DEV = "https://YOUR_TUNNEL_URL.ngrok.app" # This may be needed: # The URL of your local server. This enables the Dev Server to access the app at this local URL # You may have to change this URL to match your local server if running on a different port. # Without this, the "cloud" worker may attempt to redirect Inngest to the wrong URL. INNGEST_SERVE_ORIGIN = "http://localhost:8787" ``` See an example of this in the [Hono framework example on GitHub](https://github.com/inngest/inngest-js/tree/main/examples/framework-hono). ### Framework: DigitalOcean Functions The DigitalOcean serve function allows you to deploy Inngest to DigitalOcean serverless functions. Because DigitalOcean does not provide the request URL in its function arguments, you **must** include the function URL and path when configuring your handler: ```ts // Your own function serve({ client: inngest, functions: [fnA], // Your digitalocean hostname. This is required otherwise your functions won't work. serveOrigin: "https://faas-sfo3-your-url.doserverless.co", // And your DO path, also required. servePath: "/api/v1/web/fn-your-uuid/inngest", }); // IMPORTANT: Makes the function available as a module in the project. // This is required for any functions that require external dependencies. module.exports.main = main; ``` Inngest functions can also be deployed to [DigitalOcean's App Platform or Droplets](/docs/deploy/digital-ocean). ### Framework: ElysiaJS For [deployment options](https://elysiajs.com/patterns/deploy.html), Elysia can compile to a binary or to JavaScript, or you can deploy with Docker or Railway. ```ts {{ title: "src/index.ts" }} serve({ client: inngest, functions, }); new Elysia().all("/api/inngest", ({ request }) => handler(request) ); // register the handler with Elysia new Elysia() .use(inngestHandler) ``` Elysia's `use` function expects a single argument. We make use of the `all` method for the inngest api route to handle the expected methods and then get the request off of the context object passed to elysia handlers. See the [ElysiaJS example](https://github.com/inngest/inngest-js/tree/main/examples/framework-elysiajs) for more information. ### Framework: Express You can serve Inngest functions within your existing Express app, deployed to any hosting provider like Render, Fly, AWS, K8S, and others: ```ts // Your own function // Important: ensure you add JSON middleware to process incoming JSON POST payloads. app.use(express.json()); app.use( // Expose the middleware on our recommended path at `/api/inngest`. "/api/inngest", serve({ client: inngest, functions: [fnA] }) ); ``` You must ensure you're using the `express.json()` middleware otherwise your functions won't be executed. **Note** - You may need to set [`express.json()`'s `limit` option](https://expressjs.com/en/5x/api.html#express.json) to something higher than the default `100kb` to support larger event payloads and function state. See the [Express example](https://github.com/inngest/inngest-js/tree/main/examples/framework-express) for more information. #### Streaming Express can also stream responses back to Inngest, potentially allowing much longer timeouts. To enable this, add the `streaming: true` option to your serve handler: ```ts serve({ client: inngest, functions: [...fns], streaming: true, }); ``` For more information, check out the [Streaming](/docs/streaming) page. ### Framework: Fastify You can serve Inngest functions within your existing Fastify app. We recommend using the exported `inngestFastify` plugin, though we also expose a generic `serve()` function if you'd like to manually create a route. ```ts {{ title: "Plugin" }} Fastify(); fastify.register(fastifyPlugin, { client: inngest, functions: [fnA], options: {}, }); fastify.listen({ port: 3000 }, function (err, address) { if (err) { fastify.log.error(err); process.exit(1); } }); ``` ```ts {{ title: "Custom route" }} Fastify(); fastify.route({ method: ["GET", "POST", "PUT"], handler: serve({ client: inngest, functions: [fnA] }), url: "/api/inngest", }); fastify.listen({ port: 3000 }, function (err, address) { if (err) { fastify.log.error(err); process.exit(1); } }); ``` See the [Fastify example](https://github.com/inngest/inngest-js/tree/main/examples/framework-fastify) for more information. ### Framework: Fresh (Deno) Inngest works with Deno's Fresh framework via the `esm.sh` CDN. Add the serve handler to `./api/inngest.ts` as follows: ```ts // Your own function handler = serve({ client: inngest, functions: [fnA], }); ``` ### Framework: Google Cloud Run Functions Google's [Functions Framework](https://github.com/GoogleCloudPlatform/functions-framework-nodejs) has an Express-compatible API which enables you to use the Express serve handler to deploy your Inngest functions to Google Cloud Run. This is an example of a function: ```ts // Your own function ff.http( "inngest", serve({ client: inngest, functions: [fnA], servePath: "/", }) ); ``` You can run this locally with `npx @google-cloud/functions-framework --target=inngest` which will serve your Inngest functions on port `8080`. See the [Google Cloud Functions example](https://github.com/inngest/inngest-js/tree/main/examples/framework-google-functions-framework) for more information. 1st generation Cloud Run Functions are not officially supported. Using one may result in a signature verification error. ### Framework: Firebase Cloud Functions Based on the Google Cloud Function architecture, the Firebase Cloud Functions provide a different API to serve functions using `onRequest`: ```typescript inngest = onRequest( serve({ client: inngestClient, functions: [/* ...functions... */], }) ); ``` Firebase Cloud Functions require configuring `INNGEST_SERVE_PATH` with the custom function path. For example, for a project named `inngest-firebase-functions` deployed on the `us-central1` region, the `INNGEST_SERVE_PATH` value will be as follows: ``` /inngest-firebase-functions/us-central1/inngest/ ``` To serve your Firebase Cloud Function locally, use the following command: ```bash firebase emulators:start ``` Please note that you'll need to start your Inngest Local Dev Server with the `-u` flag to match our Firebase Cloud Function's custom path as follows: ```bash npx --ignore-scripts=false inngest-cli@latest dev -u http://127.0.0.1:5001/inngest-firebase-functions/us-central1/inngest ``` _The above command example features a project named `inngest-firebase-functions` deployed on the `us-central1` region_. ### Framework: H3 Inngest supports [H3](https://github.com/unjs/h3) and frameworks built upon it. Here's a simple H3 server that hosts serves an Inngest function. ```ts createApp(); app.use( "/api/inngest", eventHandler( serve({ client: inngest, functions: [fnA], }) ) ); createServer(toNodeListener(app)).listen(process.env.PORT || 3000); ``` See the [github.com/unjs/h3](https://github.com/unjs/h3) repository for more information about how to host an H3 endpoint. ### Framework: Hono Inngest supports the [Hono](https://hono.dev/) framework which is popularly deployed to Cloudflare Workers. Add the following to `./src/index.ts`: ```ts new Hono(); app.on( ["GET", "PUT", "POST"], "/api/inngest", serve({ client: inngest, functions, }) ); export default app; ``` To automatically pass environment variables defined with Wrangler to Inngest function handlers, use the [Hono bindings middleware](/docs/examples/middleware/cloudflare-workers-environment-variables). If you're using Hono with Cloudflare's Wrangler CLI in "_cloud_" mode, follow [the documentation above](#local-development-with-wrangler) for Cloudflare Workers. See the [Hono example](https://github.com/inngest/inngest-js/blob/main/examples/framework-hono) for more information. ### Framework: Koa Add the following to your routing file: ```ts new Koa(); app.use(bodyParser()); // make sure we're parsing incoming JSON serve({ client: inngest, functions, }); app.use((ctx) => { if (ctx.request.path === "/api/inngest") { return handler(ctx); } }); ``` See the [Koa example](https://github.com/inngest/inngest-js/tree/main/examples/framework-koa) for more information. ### Framework: NestJS Add the following to `./src/main.ts`: ```ts async function bootstrap() { await NestFactory.create(AppModule, { bodyParser: true, }); // Setup inngest app.useBodyParser('json', { limit: '10mb' }); // Inject Dependencies into inngest functions app.get(Logger); app.get(AppService); // Pass dependencies into this function getInngestFunctions({ appService, logger, }); // Register inngest endpoint app.use( '/api/inngest', serve({ client: inngest, functions: inngestFunctions, }), ); // Start listening for http requests await app.listen(3000); } bootstrap(); ``` See the [NestJS example](https://github.com/inngest/inngest-js/tree/main/examples/framework-nestjs) for more information. ### Framework: Next.js Inngest has first class support for Next.js API routes, allowing you to easily create the Inngest API. Both the App Router and the Pages Router are supported. For the App Router, Inngest requires `GET`, `POST`, and `PUT` methods. ```typescript {{ title: "App Router" }} // src/app/api/inngest/route.ts // Your own functions { GET, POST, PUT } = serve({ client: inngest, functions: [fnA], }); ``` ```typescript {{ title: "Pages Router" }} // pages/api/inngest.ts // Your own function export default serve({ client: inngest, functions: [fnA], }); ``` #### Streaming Next.js Functions hosted on [Vercel](/docs/deploy/vercel) with Fluid compute can stream responses back to Inngest which can help you reach the maximum duration of 800s (13m20s) provided you are on a paid Vercel plan. To enable this, add the `streaming: true` option to your serve handler: **Next.js 13+ on Fluid compute** ```ts { GET, POST, PUT } = serve({ client: inngest, functions: [...fns], streaming: true, }); ```
**Edge runtime**
If you are not using Vercel Fluid compute, you can also stream responses to Inngest by running on their [edge runtime](https://vercel.com/docs/functions/runtimes/edge). To enable this, set your runtime to `"edge"` and add the `streaming: true` option to your serve handler: **Next.js 13+** ```ts runtime = "edge"; { GET, POST, PUT } = serve({ client: inngest, functions: [...fns], streaming: true, }); ```
**Older versions (Next.js 12)** ```ts config = { runtime: "edge", }; serve({ client: inngest, functions: [...fns], streaming: true, }); ```
For more information, check out the [Streaming](/docs/streaming) page. ### Framework: Nitro Add the following to `./server/routes/api/inngest.ts`: ```ts // Your own function export default eventHandler( serve({ client: inngest, functions: [fnA], }) ); ``` See the [Nitro example](https://github.com/inngest/inngest-js/tree/main/examples/framework-nitro) for more information. ### Framework: Nuxt Inngest has first class support for [Nuxt server routes](https://nuxt.com/docs/guide/directory-structure/server#server-routes), allowing you to easily create the Inngest API. Add the following within `./server/api/inngest.ts`: ```ts // Your own function export default defineEventHandler( serve({ client: inngest, functions: [fnA], }) ); ``` See the [Nuxt example](https://github.com/inngest/inngest-js/tree/main/examples/framework-nuxt) for more information. ### Framework: Redwood Add the following to `api/src/functions/inngest.ts`: ```ts // Your own function handler = serve({ client: inngest, functions: [fnA], servePath: "/api/inngest", }); ``` You should also update your `redwood.toml` to add `apiUrl = "/api"`, ensuring your API is served at the `/api` root. ### Framework: Remix Add the following to `./app/routes/api.inngest.ts`: ```ts // app/routes/api.inngest.ts serve({ client: inngest, functions: [fnA], }); export { handler as action, handler as loader }; ``` See the [Remix example](https://github.com/inngest/inngest-js/tree/main/examples/framework-remix) for more information. #### Streaming Remix Edge Functions hosted on [Vercel](/docs/deploy/vercel) can also stream responses back to Inngest, giving you a much higher request timeout of 15 minutes (up from 10 seconds on the Vercel Hobby plan!). To enable this, set your runtime to `"edge"` (see [Quickstart for Using Edge Functions | Vercel Docs](https://vercel.com/docs/concepts/functions/edge-functions/quickstart)) and add the `streaming: true` option to your serve handler: ```ts config = { runtime: "edge", }; serve({ client: inngest, functions: [...fns], streaming: true, }); ``` For more information, check out the [Streaming](/docs/streaming) page. ### Framework: Supabase Edge Functions Supabase Edge Functions can use our `inngest/edge` package. ```ts // Your own function Deno.serve(serve({ client: inngest, functions: [fnA], servePath: "/functions/v1/your-function-name", })); ``` Ensure that `servePath` matches your Supabase Edge Function name. Alternatively, you can set this with the `INNGEST_SERVE_PATH` environment variable. This is necessary because Supabase Edge Functions rewrite the request path. ### Framework: Firebase Cloud Functions Based on the Google Cloud Function architecture, the Firebase Cloud Functions provide a different API to serve functions using `onRequest`: ```typescript inngest = onRequest( serve({ client: inngestClient, functions: [/* ...functions... */], }) ); ``` ### Framework: SvelteKit Add the following to `./src/routes/api/inngest/+server.ts`: ```ts serve({ client: inngest, functions }); GET = inngestServe.GET; POST = inngestServe.POST; PUT = inngestServe.PUT; ``` See the [SvelteKit example](https://github.com/inngest/inngest-js/tree/main/examples/framework-sveltekit) for more information. ### Framework: Tanstack Start Add the following to `./src/routes/api/inngest.ts`: ```ts serve({ client: inngest, functions }); Route = createFileRoute("/api/inngest")({ server: { handlers: { GET: async ({ request }) => handler(request), POST: async ({ request }) => handler(request), PUT: async ({ request }) => handler(request), }, }, }); ``` See the [Tanstack Start example](https://github.com/inngest/inngest-js/tree/main/examples/framework-tanstack-start) for more information. ### Custom frameworks If the framework that your application uses is not included in the above list of first-party supported frameworks, you can create a custom `serve` handler. To create your own handler, check out the [example handler](https://github.com/inngest/inngest-js/blob/main/packages/inngest/src/test/functions/handler.ts) in our SDK's open source repository to understand how it works. Here's an example of a custom handler being created and used: ```ts (options: ServeHandlerOptions) => { new InngestCommHandler({ frameworkName: "edge", fetch: fetch.bind(globalThis), ...options, handler: (req: Request) => { return { body: () => req.json(), headers: (key) => req.headers.get(key), method: () => req.method, url: () => new URL(req.url, `https://${req.headers.get("host") || ""}`), transformResponse: ({ body, status, headers }) => { return new Response(body, { status, headers }); }, }; }, }); return handler.createHandler(); }; new Inngest({ id: "example-edge-app" }); inngest.createFunction( { id: "hello-world", triggers: { event: "test/hello.world" } }, () => "Hello, World!" ); export default serve({ client: inngest, functions: [fn] }); ```
Inngest enables you to create a HTTP handler for your functions. This handler will be used to serve your functions over HTTP (compatible with `net/http`). ```go {{ title: "Go (HTTP)" }} package main import ( "context" "fmt" "net/http" "time" "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) func main() { client, err := inngestgo.NewClient(inngestgo.ClientOpts{ AppID: "core", }) if err != nil { panic(err) } _, err = inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "account-created", Name: "Account creation flow", }, // Run on every api/account.created event. inngestgo.EventTrigger("api/account.created", nil), AccountCreated, ) if err != nil { panic(err) } http.ListenAndServe(":8080", client.Serve()) } ``` You expose your functions to Inngest through this HTTP endpoint. Inngest provides integrations with Flask and FastAPI. ```python {{ title: "Python (Flask)" }} import logging import inngest from src.flask import app import inngest.flask logger = logging.getLogger(f"{app.logger.name}.inngest") logger.setLevel(logging.DEBUG) inngest_client = inngest.Inngest(app_id="flask_example", logger=logger) @inngest_client.create_function( fn_id="hello-world", trigger=inngest.TriggerEvent(event="say-hello"), ) def hello(ctx: inngest.ContextSync) -> str: inngest.flask.serve( app, inngest_client, [hello], ) app.run(port=8000) ``` ```python {{ title: "Python (FastAPI)" }} import logging import inngest import fastapi import inngest.fast_api logger = logging.getLogger("uvicorn.inngest") logger.setLevel(logging.DEBUG) inngest_client = inngest.Inngest(app_id="fast_api_example", logger=logger) @inngest_client.create_function( fn_id="hello-world", trigger=inngest.TriggerEvent(event="say-hello"), ) async def hello(ctx: inngest.Context) -> str: return "Hello world!" app = fastapi.FastAPI() inngest.fast_api.serve( app, inngest_client, [hello], ) ```
### Signing key You'll need to assign your [signing key](/docs/platform/signing-keys) to an [`INNGEST_SIGNING_KEY`](/docs/sdk/environment-variables#inngest-signing-key) environment variable in your hosting provider or `.env` file locally, which lets the SDK securely communicate with Inngest. If you can't use an environment variable, you can pass the signing key directly to the Inngest client constructor via the `signingKey` option. [Read the reference for more information](/docs/reference/typescript/v4/serve#reference). ### Other configuration When using `serve`, allow requests up to 4 MB in size. This is the maximum request size that Inngest will send to your app. Configuring maximum request size is framework-specific, so check the documentation for your framework for more information. ## Next steps After your app serves Inngest functions, choose the next step that matches where you are in the setup: * Start from a framework guide, such as the [Next.js quick start](/docs/getting-started/nextjs-quick-start?ref=docs-serving-inngest-functions), [Express quick start](/docs/getting-started/express-quick-start?ref=docs-serving-inngest-functions), or [Python quick start](/docs/getting-started/python-quick-start?ref=docs-serving-inngest-functions). * Configure production secrets with [signing keys](/docs/platform/signing-keys?ref=docs-serving-inngest-functions) and [event keys](/docs/events/creating-an-event-key?ref=docs-serving-inngest-functions). * Deploy on Vercel with the [Vercel deployment guide](/docs/deploy/vercel?ref=docs-serving-inngest-functions), including `maxDuration` and streaming guidance for long-running work. * Use [Connect](/docs/setup/connect?ref=docs-serving-inngest-functions) instead of `serve()` when you want worker-style execution over an outbound WebSocket connection. ## Reference For more information about the `serve` handler, read the [the reference guide](/docs/reference/typescript/v4/serve), which includes: * [`serve()` configuration options](/docs/reference/typescript/v4/serve#serve-client-functions-options) * [How the serve handler works](/docs/reference/typescript/v4/serve#how-the-serve-api-handler-works) # Versioning and Function Evolution Source: https://www.inngest.com/docs/learn/versioning Description: Safely update Inngest functions while runs are in-flight using step memoization. Understand how new deploys interact with paused and sleeping function runs. metaTitle = "Function Versioning | Evolve Long-Running Inngest Functions" Long-running functions inevitably change over time. Inngest enables developers to deploy changes to functions without explicit version markers or complex migration logic. This guide explains how Inngest handles versioning and the strategies you can use to evolve functions safely. ## How Inngest handles versioning Inngest uses **step-based memoization** and a graceful deterministic execution model. This is consistent across all language SDKs. Unlike systems that require explicit version annotations in your code, Inngest tracks function state through step identifiers, allowing you to modify functions while they're running. ### Step-based memoization Each [step](/docs/learn/inngest-steps) in a function has a unique string identifier. When a function executes, the SDK: 1. Hashes the step's identifier along with a counter (enabling steps inside loops) 2. Checks if this hash exists in the function's stored state 3. If found, returns the memoized result without re-executing 4. If not found, executes the step and stores the result This means that completed steps are never re-executed, even across deployments. The SDK determines what to run based on the step identifiers in your code, not version numbers. For more details on this execution model, see [How Inngest functions are executed](/docs/learn/how-functions-are-executed). ### Graceful determinism by default The SDK handles changes **gracefully** by default: - **New steps are executed when discovered** — If you add a step to a function, in-progress runs will execute it when they encounter it - **Warnings, not failures** — If step execution order changes, the SDK logs a warning rather than failing the function This approach lets you extend and improve functions over time without worrying about in-progress runs failing. ## Evolving functions over time There are several strategies for evolving functions, depending on the type of change you're making. ### Adding new steps Adding new steps to a function is generally safe. New steps will execute when discovered by in-progress runs. This is useful for adding logging, analytics, notifications, or other additive functionality. ```ts {{ title: "Before" }} inngest.createFunction( { id: "user-signup", triggers: { event: "user/created" } }, async ({ event, step }) => { await step.run("send-welcome-email", async () => { return sendWelcomeEmail(event.data.email); }); await step.run("sync-to-crm", async () => { return crm.contacts.create(event.data); }); await step.run("schedule-followup", async () => { return scheduleFollowupEmail(event.data.userId, "3 days"); }); } ); ``` ```ts {{ title: "After: Added analytics tracking" }} inngest.createFunction( { id: "user-signup", triggers: { event: "user/created" } }, async ({ event, step }) => { await step.run("send-welcome-email", async () => { return sendWelcomeEmail(event.data.email); }); // New step - executes when discovered, even if later steps already completed await step.run("track-signup-analytics", async () => { return analytics.track("user_signup_complete", { userId: event.data.userId, }); }); await step.run("sync-to-crm", async () => { return crm.contacts.create(event.data); }); await step.run("schedule-followup", async () => { return scheduleFollowupEmail(event.data.userId, "3 days"); }); } ); ``` When you deploy this change, an in-progress run that has already completed `send-welcome-email` and `sync-to-crm` will: 1. Skip `send-welcome-email` (memoized) 2. Execute `track-signup-analytics` (new step) 3. Skip `sync-to-crm` if already completed (memoized) 4. Execute `schedule-followup` (new step) New steps must not depend on data from steps that haven't executed yet. In the example above, `track-signup-analytics` only uses data from the triggering event, so it's safe to add at any point. ### Modifying existing steps **Changing step logic with the same ID** is safe. If you modify the code inside a step but keep the same step ID, in-progress runs that have already completed that step will use the memoized result. New runs will execute the updated logic. **Changing step IDs** forces re-execution. If you need to re-run a step with new logic for in-progress runs, change the step ID: ```ts {{ title: "Before" }} await step.run("calculate-risk-score", async () => { return calculateRiskScore(user.profile); }); ``` ```ts {{ title: "After: Changed ID to force re-execution" }} // Changed ID means in-progress runs will re-calculate // even if they already ran "calculate-risk-score" await step.run("calculate-risk-score-v2", async () => { return calculateRiskScoreWithNewModel(user.profile); }); ``` The SDK logs a warning when step execution order changes. This is expected behavior when intentionally forcing re-execution. ### Removing steps When you remove a step, in-progress runs that have already completed it will continue normally. The memoized data for the removed step remains in state but is simply ignored. ### Reordering steps Reordering steps triggers a warning because the execution order differs from the stored state. The SDK handles this gracefully—memoized steps return their stored results regardless of their position in code, and new steps execute when encountered. ## Major logic changes For complete rewrites where the new logic is incompatible with in-progress runs, use the **new function pattern** with timestamp-based routing. This pattern uses two functions that subscribe to the same event, with [`if` expressions](/docs/reference/typescript/v4/functions/create#trigger) to route events based on timestamp: ```ts {{ title: "Original function with timestamp filter" }} // Handle events BEFORE the cutover timestamp 1704067200000; // Jan 1, 2024 00:00:00 UTC processUploadV1 = inngest.createFunction( { id: "process-upload", triggers: { event: "file/uploaded", if: `event.ts < ${CUTOVER_TS}`, }, }, async ({ event, step }) => { // Original logic - continues for in-progress runs await step.run("process-file", async () => { return legacyProcessor(event.data.fileId); }); } ); ``` ```ts {{ title: "New function for events after cutover" }} // Handle events AFTER the cutover timestamp 1704067200000; processUploadV2 = inngest.createFunction( { id: "process-upload-v2", triggers: { event: "file/uploaded", if: `event.ts >= ${CUTOVER_TS}`, }, }, async ({ event, step }) => { // Completely rewritten workflow await step.run("extract-metadata", async () => { return extractMetadata(event.data.fileId); }); await step.run("validate-content", async () => { return validateContent(metadata); }); await step.run("process-modern", async () => { return modernProcessor(event.data.fileId, metadata); }); } ); ``` This approach ensures: - In-progress runs complete with the original logic - New events trigger the updated function - No data loss or unexpected failures This creates a new function in your Inngest dashboard. Once all v1 runs complete, you can remove the original function. ### Using event versions You can also use the event's `v` [version field](/docs/features/events-triggers/event-format#event-payload-format) in combination with the `if` pattern above if you choose to control the ```typescript await inngest.send({ name: "file/uploaded", data: {/* ... */}, v: "2026-03-11" // Use the version to route to the right function }) ``` ```typescript // v1 processUploadV1 = inngest.createFunction( { id: "process-upload", triggers: { event: "file/uploaded", if: `event.v != "2026-03-11"`, // or you already are using versions, set that explicitly }, }, //... }); // v2 processUploadV2 = inngest.createFunction( { id: "process-upload-v2", triggers: { event: "file/uploaded", if: `event.v == "2026-03-11"`, }, }, //... }); ``` ## Best practices ### Step ID naming Choose step IDs that are: - **Descriptive**: `"charge-customer-payment"` not `"step-1"` - **Stable**: Avoid IDs that encode values that might change - **Unique**: Each step in a function needs a distinct ID ```ts // Good - descriptive and stable await step.run("send-order-confirmation", async () => { ... }); // Avoid - generic and likely to conflict await step.run("send", async () => { ... }); ``` ### Testing version changes locally Use the [Inngest Dev Server](/docs/local-development) to test how changes affect in-progress runs: 1. Start a function with a `step.sleep()` to pause execution 2. Modify the function code while it's sleeping 3. Observe how the function handles changes when it resumes This helps you understand the impact of changes before deploying to production. ## Further reading - [How Inngest functions are executed](/docs/learn/how-functions-are-executed) — Detailed explanation of durable execution - [Inngest steps](/docs/learn/inngest-steps) — Step methods and patterns # Background jobs Source: https://www.inngest.com/docs/guides/background-jobs Description: Run background jobs with automatic retries in a few lines of code. No queues, no workers, no infrastructure. Works on serverless platforms out of the box. metaTitle = "Background Jobs with Inngest | Setup Guide" This guide will walk you through creating background jobs with retries in a few minutes. By running background tasks in Inngest: - You don't need to create queues, workers, or subscriptions. - You can run background jobs on serverless functions without setting up infrastructure. - You can enqueue jobs to run in the future, similar to a task queue, without any configuration. ## How to create background jobs Background jobs in Inngest are executed in response to a trigger (an event or cron). The example below shows a background job that uses an event (here called `app/user.created`) to send an email to new signups. It consists of two parts: creating the function that runs in the background and triggering the function. ### 1. Create a function that runs in the background Let's walk through the code step by step: 1. We [create a new Inngest function](/docs/reference/typescript/v4/functions/create), which will run in the background any time the `app/user.created` event is sent to Inngest. 2. We send an email reliably using the [`step.run()`](/docs/reference/typescript/v4/functions/step-run) method. Every [Inngest step](/docs/learn/inngest-steps) is automatically retried upon failure. 3. We pause the execution of the function until a specific date using [`step.sleepUntil()`](/docs/reference/typescript/v4/functions/step-sleep-until). The function will be resumed automatically, across server restarts or serverless functions. You don't have to worry about scale, memory leaks, connections, or restarts. 4. We resume execution and perform other tasks. ```ts new Inngest({ id: "signup-flow" }); sendSignUpEmail = inngest.createFunction( { id: "send-signup-email", triggers: { event: "app/user.created" } }, ({ event, step }) => { await step.run("send-the-user-a-signup-email", async () => { await sesclient.clientsendEmail({ to: event.data.user_email, subject: "Welcome to Inngest!" message: "...", }); }); await step.sleepUntil("wait-for-the-future", "2023-02-01T16:30:00"); await step.run("do-some-work-in-the-future", async () => { // Code here runs in the future automatically. }); } ); ``` ### 2. Trigger the function Your `sendSignUpEmail` function will be triggered whenever Inngest receives an event called `app/user.created`. is received. You send this event to Inngest like so: ```ts await inngest.send({ name: "app/user.created", // This matches the event used in `createFunction` data: { email: "test@example.com", // any data you want to send }, }); ``` Let's walk through the code step by step: 1. We [create a new Inngest function](https://pkg.go.dev/github.com/inngest/inngestgo#CreateFunction), which will run in the background any time the `app/user.created` event is sent to Inngest. 2. We send an email reliably using the [`step.Run()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Run) method. Every [Inngest step](/docs/learn/inngest-steps) is automatically retried upon failure. 3. We pause the execution of the function for 4 hours using [`step.Sleep()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Sleep). The function will be resumed automatically, across server restarts or serverless functions. You don't have to worry about scale, memory leaks, connections, or restarts. 4. We resume execution and perform other tasks. ```go !snippet:path=snippets/go/docs/functions/delayed_function.go ``` ### 2. Trigger the function Your `sendSignUpEmail` function will be triggered whenever Inngest receives an event called `app/user.created`. is received. You send this event to Inngest like so: ```go !snippet:path=snippets/go/docs/events/user_created.go ``` Let's walk through the code step by step: 1. We [create a new Inngest function](/docs/reference/python/functions/create), which will run in the background any time the `app/user.created` event is sent to Inngest. 2. We send an email reliably using the [`step.run()`](/docs/reference/python/steps/run) method. Every [Inngest step](/docs/learn/inngest-steps) is automatically retried upon failure. 3. We pause the execution of the function until a specific date using [`step.sleep_until()`](/docs/reference/python/steps/sleep-until). The function will be resumed automatically, across server restarts or serverless functions. You don't have to worry about scale, memory leaks, connections, or restarts. 4. We resume execution and perform other tasks. ```python import inngest inngest_client = inngest.Inngest( app_id="my-app", ) @inngest_client.create_function( fn_id="send-signup-email", trigger=inngest.TriggerEvent(event="app/user.created") ) async def send_signup_email(ctx: inngest.Context): async def send_email(): await sesclient.send_email( to=ctx.event.data["user_email"], subject="Welcome to Inngest!", message="..." ) await ctx.step.run("send-the-user-a-signup-email", send_email) await ctx.step.sleep_until("wait-for-the-future", "2023-02-01T16:30:00") async def future_work(): # Code here runs in the future automatically pass await ctx.step.run("do-some-work-in-the-future", future_work) ``` ### 2. Trigger the function Your `sendSignUpEmail` function will be triggered whenever Inngest receives an event called `app/user.created`. is received. You send this event to Inngest like so: ```python from src.inngest.client import inngest_client await inngest_client.send( name="app/user.created", # This matches the event used in `create_function` data={ "email": "test@example.com", # any data you want to send } ) ``` When you send an event to Inngest, it automatically finds any functions that are triggered by the event ID and automatically runs those functions in the background. The entire JSON object you pass in to `inngest.send()` will be available to your functions. 💡 Tip: You can create many functions which listen to the same event, and all of them will run in the background. Learn more about this pattern in our ["Fan out" guide](/docs/guides/fan-out-jobs). ## Further reading More information on background jobs: - [Email sequence examples](/docs/examples/email-sequence) implemented with Inngest. - [Customer story: Soundcloud](/customers/soundcloud): building scalable video pipelines with Inngest to streamline dynamic video generation. - [Customer story: GitBook](/customers/gitbook): how GitBook scaled background job processing with Inngest. - [Customer story: Fey](/customers/fey): how Fey cut execution time and costs by 50x in data-intensive processes. - Blog post: [building Truckload](/blog/mux-migrating-video-collections), a tool for heavy video migration between hosting platforms, from Mux. - Blog post: building _banger.show_'s [video rendering pipeline](/blog/banger-video-rendering-pipeline). # Batching events Source: https://www.inngest.com/docs/guides/batching Description: Process high volumes of events in batches to reduce function invocations and enable bulk operations. Configure batch size, timeout, and key-based grouping. metaTitle = "Batch Processing Events" Batching allows a function to process multiple events in a single run. This is useful for high load systems where it's more efficient to handle a batch of events together rather than handling each event individually. Some use cases for batching include: * Reducing the number of requests to an external API that supports batch operations. * Creating a batch of database writes to reduce the number of transactions. * Reducing the number of requests to your [Inngest app](/docs/apps) to improve performance or serverless costs. ## How to configure batching {/* NOTE - This should be moved to an example and we can make this more succinct */} ```ts {{ title: "TypeScript"}} inngest.createFunction( { id: "record-api-calls", batchEvents: { maxSize: 100, timeout: "5s", key: "event.data.user_id", // Optional: batch events by user ID if: "event.data.account_type == \"free\"", // Optional: Only batch events from free accounts }, triggers: { event: "log/api.call" }, }, async ({ events, step }) => { // NOTE: Use the `events` argument, which is an array of event payloads events.map((evt) => { return { user_id: evt.data.user_id, endpoint: evt.data.endpoint, timestamp: toDateTime(evt.ts), account_type: evt.data.account_type, }; }); await step.run("record-data-to-db", async () => { return db.bulkWrite(attrs); }); return { success: true, recorded: result.length }; } ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "record-api-calls", BatchEvents: &inngestgo.ConfigBatchEvents{ MaxSize: 100, Timeout: 5 * time.Second, Key: inngestgo.StrPtr("event.data.user_id"), // Optional: batch events by user ID If: inngestgo.StrPtr("event.data.account_type == \"free\""), // Optional: Only batch events from free accounts }, }, inngestgo.EventTrigger("log/api.call", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // NOTE: Use the events argument, which is an array of event payloads events := input.Events attrs := make([]interface{}, len(events)) for i, evt := range events { attrs[i] = map[string]interface{}{ "user_id": evt.Data["user_id"], "endpoint": evt.Data["endpoint"], "timestamp": toDateTime(evt.Timestamp), "account_type": evt.Data["account_type"], } } _, err := step.Run(ctx, "record-data-to-db", func(ctx context.Context) (interface{}, error) { return db.BulkWrite(attrs) }) if err != nil { return nil, err } return map[string]interface{}{ "success": true, "recorded": len(attrs), }, nil }, ) ``` ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="record-api-calls", trigger=inngest.TriggerEvent(event="log/api.call"), batch_events=inngest.Batch( max_size=100, timeout=datetime.timedelta(seconds=5), key="event.data.user_id", # Optional: batch events by user ID if_exp="event.data.account_type == \"free\"", # Optional: Only batch events from free accounts ), ) async def record_api_calls(ctx: inngest.Context): # NOTE: Use the events from ctx, which is an array of event payloads attrs = [ { "user_id": evt.data["user_id"], "endpoint": evt.data["endpoint"], "timestamp": to_datetime(evt.ts), "account_type": evt.data["account_type"] } for evt in ctx.events ] async def record_data(): return await db.bulk_write(attrs) result = await ctx.step.run("record-data-to-db", record_data) return {"success": True, "recorded": len(result)} ``` ### Configuration reference * `maxSize` - The maximum number of events to add to a single batch. * `timeout` - The duration of time to wait to add events to a batch. If the batch is not full after this time, the function will be invoked with whatever events are in the current batch, regardless of size. * `key` - An optional [expression](/docs/guides/writing-expressions) using event data to batch events by. Each unique value of the `key` will receive its own batch, enabling you to batch events by any particular key, like a user ID. * `if` - An optional [boolean expression](/docs/guides/writing-expressions) using event data to conditionally batch events that evaluate to true on this expression. It is recommended to consider the overall batch size that you will need to process including the typical event payload size. Processing large batches can lead to memory or performance issues in your application. For system safety purposes, We also enforce a 10 MiB size limit for a batch, meaning if the size of the total number of events exceeds 10 MiB, the batch will start execution even if it's not full or has reached a timeout. This limit cannot be changed at the moment. ## How batching works When batching is enabled, Inngest creates a new batch when the first event is received. The batch is filled with events until the `maxSize` is reached _or_ the `timeout` is up. The function is then invoked with the full list of events in the batch. When `key` is set, Inngest will maintain a batch for each unique key, which allows you to batch events belonging to a single entity, for example a customer. Depending on your SDK, the `events` argument will contain the full list of events within a batch. This allows you to operate on all of them within a single function. ### Conditional Batching Conditional Batching can be enabled by providing a boolean expression in `if`. If the expression cannot be evaluated to a boolean value or if the expression evaluates to `false`, batching will be skipped for this event and the event will be scheduled for execution immediately. ## Combining with other flow control methods Batching does not work with all other flow control features. ### Batching with Concurrency limits You can combine batching with [concurrency](/docs/guides/concurrency) limits. For example, setting `concurrency: { limit: 1 }` will process one batch at a time. ### Batching with Custom Concurrency keys When a concurrency limit has a `key`, **the key is evaluated against the first event in the batch**. This means: - If your batch contains events with different key values, only the first event's key value is used for the concurrency check - This can lead to unintuitive behavior if events with different key values end up in the same batch, which can happen when there is no batch key or when the batch key is different from the concurrency key. For predictable behavior, **use the same key expression for both batching and concurrency**. When the batch `key` and the concurrency `key` match, batches are naturally grouped by key value, so the concurrency limit applies per key as expected — allowing up to `n` concurrent batches per unique key. ### Incompatible flow control features You _cannot_ use batching with [idempotency](/docs/guides/handling-idempotency), [rate limiting](/docs/guides/rate-limiting), [cancellation events](/docs/guides/cancel-running-functions#cancel-with-events), or [priority](/docs/guides/priority). ## Limitations * Check our [pricing page](https://www.inngest.com/pricing) to verify the batch size limits for each plan. ## Further reference * [TypeScript SDK Reference](/docs/reference/typescript/v4/functions/create#batchEvents) * [Python SDK Reference](/docs/reference/python/functions/create#batch_events) # Bulk Cancellation Source: https://www.inngest.com/docs/guides/cancel-running-functions Description: Cancel multiple in-flight Inngest function runs at once using the REST API. Filter by function ID, event name, or time range to scope the cancellation. metaTitle = "Bulk Cancel Running Function Runs" {/* TODO - Link the sleeps and waits to guides when we move those from references to guides */} With Inngest, your functions can be running or paused for long periods of time. You may have function with hundreds of steps, or you may be using [`step.sleep`](/docs/reference/typescript/v4/functions/step-sleep), [`step.sleepUntil`](/docs/reference/typescript/v4/functions/step-sleep-until), or [`step.waitForEvent`](/docs/reference/typescript/v4/functions/step-wait-for-event). Sometimes, things happen in your system that make it no longer necessary to complete running the function, which is when cancelling is necessary. Inngest provides both a Bulk Cancellation API and UI. The Bulk Cancellation API offers more flexibility with the support of event expression matching while the [Bulk Cancellation UI](/docs/platform/manage/bulk-cancellation), available from the Platform, provides a quick way to cancel unwanted Function runs. {/* TODO ## Cancel in the Inngest dashboard */} ## Bulk cancel via the REST API You can also cancel functions in bulk via the [REST API](https://api-docs.inngest.com). This is useful if you have a large number of functions within a specific range that you need to cancel. With the `POST /cancellations` endpoint, you can cancel functions by specifying the `app_id`, `function_id`, and a `started_after` and `started_before` timestamp range. You can also optionally specify an `if` statement to only cancel functions that match a [given expression](/docs/guides/writing-expressions). ```bash {{ title: 'cURL' }} curl -X POST https://api.inngest.com/v1/cancellations \ -H 'Authorization: Bearer signkey-prod-' \ -H 'Content-Type: application/json' \ --data '{ "app_id": "acme-app", "function_id": "schedule-reminder", "started_after": "2024-01-21T18:23:12.000Z", "started_before": "2024-01-22T14:22:42.130Z", "if": "event.data.userId == 'user_o9235hf84hf'" }' ``` When successful, the response will be returned with the cancellation ID and the cancellation job data: ```json {{ title: 'Response' }} { "id": "01HMRMPE5ZQ4AMNJ3S2N79QGRZ", "environment_id": "e03843e1-d2df-419e-9b7b-678b03f7398f", "function_id": "schedule-reminder", "started_after": "2024-01-21T18:23:12.000Z", "started_before": "2024-01-22T14:22:42.130Z", "if": "event.data.userId == 'user_o9235hf84hf'" } ``` To learn more, read the full [REST API reference](https://api-docs.inngest.com). # Concurrency management Source: https://www.inngest.com/docs/guides/concurrency Description: Limit the number of steps executing concurrently across function runs. Set global or key-scoped concurrency limits to protect downstream services and databases. metaTitle = "Concurrency Control in Inngest | Limit Parallel Step Execution" Limiting concurrency in systems is an important tool for correctly managing computing resources and scaling workloads. Inngest's concurrency control enables you to manage the number of _steps_ that concurrently execute. **Important:** Concurrency limits the number of _steps_ executing at a single time, **not** the number of function runs. A function run that is sleeping, waiting for an event, or paused between steps does **not** count against your concurrency limit. Only steps that are actively executing code count toward the limit. This means you may have many more function runs in progress than your concurrency limit suggests, because most of those runs are likely paused or waiting between steps. {/* TODO - Link to updated keys section */} Step concurrency can be optionally configured using "keys" which applies the limit to each unique value of the key (ex. user id). This creates separate queue groups that provide [best-effort fairness in multi-tenant systems](/docs/guides/multi-tenancy?ref=docs-concurrency). The concurrency option can also be applied to different "scopes" which allows a concurrency limit to be shared across _multiple_ functions. As compared to traditional queue and worker systems, Inngest manages the concurrency within the system so you do not need to implement additional worker-level logic or state. ## When to use concurrency Concurrency is most useful when you want to constrain your function for a set of resources. Some use cases include: - **Limiting in multi-tenant systems** - Prevent a single account, user, or tenant from consuming too many resources and creating a backlog for others. See: [Concurrency keys (Multi-tenant concurrency)](#concurrency-keys-multi-tenant-concurrency). - **Limiting throughput for database operations** - Prevent potentially high volume jobs from overwhelming a database or similar resource. See: [Sharing limits across functions (scope)](#sharing-limits-across-functions-scope). - **Basic concurrent operations limits** - Limit the capacity dedicated to processing a certain job, for example an import pipeline. See: [Basic concurrency](#basic-concurrency). - **Combining multiple of the above** - Multiple concurrency limits can be added per function. See: [Combining multiple concurrency limits](#combining-multiple-concurrency-limits) If you need to limit a function to a certain rate of processing, for example with a third party API rate limit, you might need [throttling](/docs/guides/throttling) instead. Throttling is applied at the function level, compared to concurrency which is at the step level. ## How to configure concurrency One or more concurrency limits can be configured for each function. * [Basic concurrency](#basic-concurrency) * [Concurrency keys (Multi-tenant concurrency)](#concurrency-keys-multi-tenant-concurrency) * [Sharing limits across functions (scope)](#sharing-limits-across-functions-scope) * [Combining multiple concurrency limits](#combining-multiple-concurrency-limits) ### Basic concurrency The most basic concurrency limit is a single `limit` set to an integer value of the maximum number of concurrently executing steps. When concurrency limit is reached, new steps will continue to be queued and create a backlog to be processed. ```ts inngest.createFunction( { id: "generate-ai-summary", concurrency: 10, triggers: { event: "ai/summary.requested" }, }, async ({ event, step }) => { // Your function handler here } ); ``` ```go !snippet:path=snippets/go/v0_11/concurrency/basic.go ``` ```python !snippet:path=snippets/py/v0_5/concurrency/basic.py ``` ### Concurrency keys (Multi-tenant concurrency) Use a concurrency `key` expression to apply the `limit` to each unique value of key received. Within the Inngest system, this creates a **virtual queue** for every unique value and limits concurrency to each. ```ts inngest.createFunction( { id: "generate-ai-summary", concurrency: [ { key: "event.data.account_id", limit: 10, }, ], triggers: { event: "ai/summary.requested" }, }, async ({ event, step }) => { } ); ``` ```go !snippet:path=snippets/go/v0_11/concurrency/keys.go ``` ```python !snippet:path=snippets/py/v0_5/concurrency/keys.py ``` Concurrency keys are great for creating fair, multi-tenant systems. This can help prevent the noisy neighbor issue where one user triggers a lot of jobs and consumes far more resources that slow down your other users. [Learn how flow control keys reduce head-of-line blocking.](/docs/guides/multi-tenancy?ref=docs-concurrency) ### Sharing limits across functions (scope) Using the `scope` option, limits can be set across your entire Inngest account, shared across multiple functions. Here is an example of setting an `"account"` level limit for a _static_ `key` equal to `"openai"`. This will create a virtual queue using `"openai"` as the key. Any other functions using this same `"openai"` key will consume from this same limit. {/* TODO - Link to the detail section on how this works */} ```ts inngest.createFunction( { id: "generate-ai-summary", concurrency: [ { scope: "account", key: `"openai"`, limit: 60, }, ], triggers: { event: "ai/summary.requested" }, }, async ({ event, step }) => { } ); ``` ```go !snippet:path=snippets/go/v0_11/concurrency/across_fns.go ``` ```python !snippet:path=snippets/py/v0_5/concurrency/across_fns.py ``` ### Combining multiple concurrency limits Each SDK's concurrency option supports up to two limits. This is the most beneficial when combining limits, each with a different `scope`. Here is an example that combines two limits, one on the `"account"` scope and another on the `"fn"` level. Combining limits will create multiple virtual queues to limit concurrency. In the below function: - If there are 10 steps executing under the 'openai' key's virtual queue, any future runs will be blocked and will wait for existing runs to finish before executing. - If there are 5 steps executing under the 'openai' key and a single `event.data.account_id` enqueues 2 runs, the second run is limited by the `event.data.account_id` virtual queue and will wait before executing. ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "unique-function-id", concurrency: [ { // Use an account-level concurrency limit for this function, using the // "openai" key as a virtual queue. Any other function which // runs using the same "openai"` key counts towards this limit. scope: "account", key: `"openai"`, limit: 10, }, { // Create another virtual concurrency queue for this function only. This // limits all accounts to a single executing step for this function, based off // of the `event.data.account_id` field. // NOTE - "fn" is the default scope, so we could omit this field. scope: "fn", key: "event.data.account_id", limit: 1, }, ], triggers: { event: "ai/summary.requested" }, }, async ({ event, step }) => { } ); ``` ```go {{ title: "Go" }} !snippet:path=snippets/go/v0_11/concurrency/multiple.go ``` ```py {{ title: "Python" }} !snippet:path=snippets/py/v0_5/concurrency/multiple.py ``` It's worth it to note that the `"fn"` scope is the default and is optional to include. ## How concurrency works **Concurrency works by limiting the number of steps executing at a single time.** Within Inngest, execution is defined as "an SDK running code". **Calling **`step.sleep`**, **`step.sleepUntil`**, **`step.waitForEvent`**, or **`step.invoke`** does not count towards capacity limits**, as the SDK doesn't execute code while those steps wait. ### Understanding step execution vs. function runs Because sleeping or waiting is common, concurrency _does not_ limit the number of functions in progress. Instead, it limits the number of steps executing at any single time. The animation below shows how concurrency works in Inngest. You can see how different jobs queue up and flow through the system. When a limit is set, only that number of steps can execute at any given time. As steps complete, the next queued steps start executing. The key insight is that your concurrency limit applies to _active execution_, not to the number of function runs in progress. Consider a function with a `concurrency` limit of `10`: - You could have **hundreds** of function runs in progress - But only **10 steps** can be actively executing code at once - When a function run calls `step.sleep("wait", "1h")`, it releases its execution slot - That slot becomes available for other steps to use **What counts against concurrency:** - `step.run()` - while the step's code is executing **What does NOT count against concurrency:** - `step.sleep()` / `step.sleepUntil()` - while sleeping - `step.waitForEvent()` - while waiting for an event - `step.invoke()` - while waiting for the invoked function to complete - Time between steps - when Inngest is coordinating the next step ### Queue ordering Within the same function and flow control key, queues use **best-effort [FIFO](https://en.wikipedia.org/wiki/FIFO) ordering** from oldest to newest jobs. Ordering across different keys or functions is not guaranteed because the scheduler also considers capacity and fairness. This means Inngest generally prioritizes older work within a key while preventing one key's backlog from blocking other keys. If you change a key expression, existing jobs retain the key that was evaluated when they entered the queue. Only new jobs are grouped using the new expression. Learn more about [multi-tenancy and flow control keys](/docs/guides/multi-tenancy?ref=docs-concurrency#ordering-within-each-key). ### Additional information - The order of keys does not matter. Concurrency is limited by any key that reaches its limits. - You can specify multiple keys for the same scope, as long as the resulting `key` evaluates to a different string. ## Concurrency control across specific steps in a function You might need to set a different concurrency limit for a single step in a function. For example, within an AI flow you may have 10 pre-processing steps which can run with higher limits, and a single AI call with much lower limits. To control concurrency on individual steps, extract the step into a new function with its _own_ concurrency controls, and invoke the new function using `step.invoke`. This lets you combine concurrency controls and manage "flow control" in a clean, composable manner. ## How global limits work While two functions can share different `account` scoped limits, we strongly recommend that you use a global const with a single shared limit. You may write two functions that define different levels for an 'account' scoped concurrency limit. For example, function A may limit the "ai" capacity to 5, while function B limits the "ai" capacity to 50: ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "func-a", concurrency: { scope: "account", key: `"openai"`, limit: 5, }, triggers: { event: "ai/summary.requested" }, }, async ({ event, step }) => { } ); inngest.createFunction( { id: "func-b", concurrency: { scope: "account", key: `"openai"`, limit: 50, }, triggers: { event: "ai/summary.requested" }, }, async ({ event, step }) => { } ); ``` ```go {{ title: "Go" }} !snippet:path=snippets/go/v0_11/concurrency/global.go ``` ```py {{ title: "Python" }} !snippet:path=snippets/py/v0_5/concurrency/global.py ``` This works in Inngest and is *not* a conflict. Instead, function A is limited any time there are 5 or more functions running in the 'openai' queue. Function B, however, is limited when there are 50 or more items in the queue. This means that function B has more capacity than function A, though both are limited and compete on the same virtual queue. Because functions are FIFO, function runs are more likely to be worked on the older their jobs get (as the backlog grows). If function A's jobs stay in the backlog longer than function B's jobs, it's likely that their jobs will be worked on as soon as capacity is free. That said, function B will almost always have capacity before function A and may block function A's work. **While this works we strongly recommend that you use global constants for `env` or `account` level scopes, giving functions the same limit.** ## Limitations - Concurrency limits the number of steps executing at a single time. It does not _yet_ perform rate limiting over a given period of time. - Functions can specify up to 2 concurrency constraints at once - The maximum concurrency limit is defined by your account's plan - Ordering within the same function and flow control key is best-effort FIFO (with the exception of retries). - Ordering across different keys or functions is not guaranteed. The scheduler also considers capacity and fairness. ## Concurrency reference The maximum number of concurrently running steps. A value of `0` or `undefined` is the equivalent of not setting a limit. The maximum value is dictated by your account's plan. The scope for the concurrency limit, which impacts whether concurrency is managed on an individual function, across an environment, or across your entire account. * `fn` (default): only the runs of this function affects the concurrency limit * `env`: all runs within the same environment that share the same evaluated key value will affect the concurrency limit. This requires setting a `key` which evaluates to a virtual queue name. * `account`: every run that shares the same evaluated key value will affect the concurrency limit, across every environment. This requires setting a `key` which evaluates to a virtual queue name. Each SDK exposes these enums in the idiomatic manner of a given language, though the meanings of the enums are the same across all languages. An expression which evaluates to a string given the triggering event. The string returned from the expression is used as the concurrency queue name. A key is required when setting an `env` or `account` level scope. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Limit concurrency to `n` (via `limit`) per customer id: `'event.data.customer_id'` * Limit concurrency to `n` per user, per import id: `'event.data.user_id + "-" + event.data.import_id'` * Limit globally using a specific string: `'"global-quoted-key"'` (wrapped in quotes, as the expression is evaluated as a language) ## Further examples ### Restricting parallel import jobs for a customer id In this hypothetical system, customers can upload `.csv` files which each need to be processed and imported. We want to limit each customer to only one import job at a time so no two jobs are writing to a customer's data at a given time. We do this by setting a `limit: 1` and a concurrency `key` to the `customerId` which is included in every single event payload. Inngest ensures that the concurrency (`1`) applies to each unique value for `event.data.customerId`. This allows different customers to have steps executing at the same exact time, but no given customer can have two steps executing at once! ```ts {{ title: "TypeScript" }} send = inngest.createFunction( { name: "Process customer csv import", id: "process-customer-csv-import", concurrency: { limit: 1, key: `event.data.customerId`, // You can use any piece of data from the event payload }, triggers: { event: "csv/file.uploaded" }, }, async ({ event, step }) => { await step.run("process-file", async () => { await bucket.fetch(event.data.fileURI); // ... }); return { message: "success" }; } ); ``` ```go {{ title: "Go" }} !snippet:path=snippets/go/v0_11/concurrency/customer_id.go ``` ```py {{ title: "Python" }} !snippet:path=snippets/py/v0_5/concurrency/customer_id.py ``` ## Tips * Configure [start timeouts](/docs/features/inngest-functions/cancellation/cancel-on-timeouts) to prevent large backlogs with concurrency # Debounce Source: https://www.inngest.com/docs/guides/debounce Description: Collapse rapid event sequences into one function run using a sliding time window. Avoids redundant work when a function might be triggered in quick succession. metaTitle = "Debounce | Deduplicate Events Over a Time Window" Debounce delays function execution until a series of events are no longer received. This is useful for preventing wasted work when a function might be triggered in quick succession. Use cases for debounce include: * Preventing wasted work when handling events from user input that may change multiple times in a short time period. * Delaying processing of noisy webhook events until they are no longer received. * Ensuring that functions use the latest event within a series of updates (for example, synchronization). ## How to configure debounce ```ts {{ title: "TypeScript" }} !snippet:path=snippets/ts/v4/debounce/basic.ts ``` ```go {{ title: "Go" }} !snippet:path=snippets/go/v0_11/debounce/basic.go ``` ```py {{ title: "Python" }} !snippet:path=snippets/py/v0_5/debounce/basic.py ``` ### Configuration reference * `period` - The time delay to delay execution. The period begins when the first matching event is received. * `key` - An optional [expression](/docs/guides/writing-expressions) using event data to apply each limit too. Each unique value of the `key` has its own limit, enabling you to rate limit function runs by any particular key, like a user ID. * `timeout` - Optional. The maximum time that a debounce can be extended before running. ## How it works When a function is triggered, the debounce `period` begins. If another event is received that matches the function's trigger, the debounce `period` is reset. This continues until no events are received for the debounce `period`. Once the `period` has passed without any new events, the function is executed using the last event received. If a `timeout` is provided, the function will always run after the `timeout` has passed even if new events are received. This ensures that the function does not continue to be debounced indefinitely if events continue to debounce the function. [IMAGE] ### Using a `key` When a `key` is added, a separate debounce period is applied for each unique value of the `key` expression. For example, if your `key` is set to `event.data.customer_id`, each customer would have their individual debounce period applied to functions run. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more information. ## Comparison to rate limiting If you prefer to execute a function for the _first_ event received, consider using [rate limiting](/docs/guides/rate-limiting) instead. Rate limiting ensures that a function runs once for each `key` the *first* time an event is received, while debounce uses the *last* event during a specified period. ## Combining with idempotency Debounce can be combined with [idempotency](/docs/guides/handling-idempotency#at-the-function-level-the-consumer) to ensure that once the debounced function has run, it does not run again. ## Limitations * The maximum debounce `period` is 7 days (168 hours). * The minimum debounce `period` is 1 second. * Debounce does not work with [batched functions](/docs/guides/batching). ## Further reference * [TypeScript SDK Reference](/docs/reference/typescript/v4/functions/debounce) * [Python SDK Reference](/docs/reference/python/functions/create#configuration) # Debug a function run from your terminal Source: https://www.inngest.com/docs/guides/debug-with-cli Description: Use the Inngest CLI to inspect function runs, send test events, and debug failures locally without needing the cloud dashboard. metaTitle = "Debug Inngest Functions with the CLI" This guide walks you through finding a failed run, pulling its trace, and identifying which step broke. You do this entirely from the terminal using the Inngest CLI. ## Prerequisites - Inngest CLI: `npx inngest-cli@latest` - An API key set as `INNGEST_API_KEY` (create one in the [Inngest dashboard](https://app.inngest.com) under Settings > API Keys) --- ## 1. Get the run summary You have a run ID from a log, alert, or the dashboard. Fetch the summary: ```bash npx inngest-cli@latest api --prod get-function-run 01KTCTWT8XDEGWDMVX3Q9M69ND ``` The response shows the run's status, function, timing, and trigger: ```json { "data": { "id": "01KTCTWT8XDEGWDMVX3Q9M69ND", "status": "FAILED", "function": { "name": "process-order", "slug": "my-app-process-order" }, "queuedAt": "2026-06-05T21:26:44.765Z", "endedAt": "2026-06-05T21:26:45.954Z", "durationMs": "1096" } } ``` The run failed. Pull the trace to see which step broke. --- ## 2. Fetch the trace The trace shows every step in the run with its status, timing, and output: ```bash npx inngest-cli@latest api --prod get-function-trace 01KTCTWT8XDEGWDMVX3Q9M69ND --include-output ``` The response is a tree of spans. Each span is a step. Look for the one with a failed status: ```json { "data": { "runId": "01KTCTWT8XDEGWDMVX3Q9M69ND", "rootSpan": { "name": "process-order", "status": "FAILED", "children": [ { "name": "validate-input", "status": "COMPLETED", "stepOp": "RUN", "durationMs": "12" }, { "name": "charge-card", "status": "COMPLETED", "stepOp": "RUN", "durationMs": "340" }, { "name": "create-shipment", "status": "FAILED", "stepOp": "RUN", "durationMs": "744", "output": { "error": "shipping API returned 503" } } ] } } } ``` The `create-shipment` step failed with a 503 from the shipping API. The `validate-input` and `charge-card` steps completed. You know exactly where it broke and what the error was. --- ## 3. Pipe to jq for quick filtering Extract just the failed steps: ```bash npx inngest-cli@latest api --prod get-function-trace 01KTCTWT8XDEGWDMVX3Q9M69ND --include-output \ | jq '[.data.rootSpan.children[] | select(.status == "FAILED")]' ``` --- ## 4. Test a fix locally Once you identify the issue, fix the code and test against your local dev server. The CLI targets the dev server by default: ```bash # Invoke the function locally npx inngest-cli@latest api invoke-function my-app process-order \ --data '{"orderId": "test-123", "address": "123 Main St"}' # Check the run npx inngest-cli@latest api get-event-runs --include-output ``` No `--prod` flag means it hits `http://localhost:8288`. The dev server must be running. --- ## Next steps - [Inngest CLI reference](/docs/cli) for all commands and auth options - [Traces](/docs/platform/monitor/traces) for the dashboard trace view - [Error handling](/docs/guides/error-handling) for retry and failure handler patterns # Delayed Functions Source: https://www.inngest.com/docs/guides/delayed-functions Description: Schedule a function to run at a future time using event timestamps or step.sleepUntil(). Ideal for reminders, follow-ups, and time-delayed processing. metaTitle = "Delayed & Scheduled One-Off Functions" You can easily run functions in the future with Inngest. There are three ways to delay function execution: - [**Sleep for a duration**](#sleep-for-a-duration) — Pause mid-function for a set amount of time using `step.sleep()` - [**Sleep until a specific time**](#sleep-until-a-specific-time) — Pause mid-function until a date/time using `step.sleepUntil()` - [**Schedule a function for later**](#schedule-a-function-for-later) — Delay when a function is first invoked by setting the event's `ts` field Delays can be up to a year (up to seven days on the free plan). There are some benefits to delaying functions using Inngest: - It works across any provider or platform - Delays are durable and work across server restarts, serverless functions, and redeploys - You can schedule functions into the far future - Serverless functions are fully supported on all platforms - Our SDK bypasses serverless function timeouts on all platforms - You never need to manage queues or backlogs ### Platform support **This works across all providers and platforms**, whether you run serverless functions or use servers like express. **It also bypasses serverless function timeouts** on all platforms, so you can sleep for a longer time than your provider supports. ## Sleep for a duration You can pause a function for a set amount of time using the [`step.sleep()`](/docs/reference/typescript/v4/functions/step-sleep) method: ```ts new Inngest({ id: "signup-flow" }); fn = inngest.createFunction( { id: "send-signup-email", triggers: { event: "app/user.created" } }, async ({ event, step }) => { await step.sleep("wait-a-moment", "1 hour"); await step.run("do-some-work-in-the-future", async () => { // This runs after 1 hour }); } ); ``` For more information on `step.sleep()` read [the reference](/docs/reference/typescript/v4/functions/step-sleep). ## Sleep until a specific time You can pause a function until a specific time using the [`step.sleepUntil()`](/docs/reference/typescript/v4/functions/step-sleep-until) method: ```ts new Inngest({ id: "signup-flow" }); fn = inngest.createFunction( { id: "send-signup-email", triggers: { event: "app/user.created" } }, async ({ event, step }) => { await step.sleepUntil("wait-for-iso-string", "2023-04-01T12:30:00"); // You can also sleep until a timestamp within the event data. This lets you // pass in a time for you to run the job: await step.sleepUntil("wait-for-timestamp", event.data.run_at); // Assuming event.data.run_at is a timestamp. await step.run("do-some-work-in-the-future", async () => { // This runs at the specified time. }); } ); ``` For more information on `step.sleepUntil()` [read the reference](/docs/reference/typescript/v4/functions/step-sleep-until). You can pause a function for a set amount of time using the [`step.Sleep()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Sleep) method: ```go !snippet:path=snippets/go/docs/functions/delayed_function.go ``` For more information on `step.sleep()` read [the reference](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Sleep). You can pause a function for a set amount of time using the [`step.sleep()`](http://localhost:3001/docs/reference/python/steps/sleep) method: ```python import inngest from src.inngest.client import inngest_client from datetime import timedelta @inngest_client.create_function( fn_id="send-signup-email", trigger=inngest.TriggerEvent(event="app/user.created") ) async def send_signup_email(ctx: inngest.Context): await ctx.step.sleep("wait-for-the-future", timedelta(hours=4)) async def future_work(): # Code here runs in the future automatically pass await ctx.step.run("do-some-work-in-the-future", future_work) ``` For more information on `step.sleep()` read [the reference](/docs/reference/typescript/v4/functions/step-sleep). ## Sleep until a specific time You can pause a function until a specific time using the [`step.sleep_until()`](/docs/reference/python/steps/sleep-until) method: ```python import inngest from src.inngest.client import inngest_client inngest_client = inngest.Inngest( app_id="my-app", ) @inngest_client.create_function( fn_id="send-signup-email", trigger=inngest.TriggerEvent(event="app/user.created") ) async def send_signup_email(ctx: inngest.Context): async def send_email(): await sesclient.send_email( to=ctx.event.data["user_email"], subject="Welcome to Inngest!", message="..." ) await ctx.step.run("send-the-user-a-signup-email", send_email) await ctx.step.sleep_until("wait-for-the-future", "2023-02-01T16:30:00") async def future_work(): # Code here runs in the future automatically pass await ctx.step.run("do-some-work-in-the-future", future_work) ``` For more information on `step.sleep_until()` [read the reference](/docs/reference/python/steps/sleep-until). ## Schedule a function for later You can also delay when a function starts by setting the `ts` field in the [event payload](/docs/events#event-payload-format) to a future timestamp (milliseconds since the Unix epoch). Unlike `step.sleep()` or `step.sleepUntil()` which add delays _within_ a function, the `ts` field delays the _start_ of the function run itself. For example, to schedule a function to run 5 minutes from now: ```typescript await inngest.send({ name: "notifications/reminder.scheduled", data: { user: { email: "johnny.utah@fbi.gov" }, message: "Don't forget to catch the wave at 3pm", }, // Include the timestamp for 5 minutes in the future: ts: Date.now() + 5 * 60 * 1000, }); ``` For a complete walkthrough, see the [Scheduling a one-off function](/docs/examples/scheduling-one-off-function) example. ## How it works {/* TODO - Revisit this section after we write a How Inngest Works explainer */} ### `step.sleep()` and `step.sleepUntil()` With `step.sleep()` and `step.sleepUntil()`, **the function controls when it runs**. You control the flow of your code by calling `sleep` or `sleepUntil` within your function directly, instead of using the queue to manage your code's timing. This keeps your logic together and makes your code easier to modify. Inngest *stops the function from running* for whatever time is specified. When you call `step.sleep` or `step.sleepUntil` the function automatically stops running any future work. The function then tells the Inngest executor that it should be re-invoked at a future time. We re-call the function at the next step, skipping any previous work. This is how we bypass serverless function time limits and work across server restarts or redeploys. ### Event `ts` field When you set the `ts` field on an event to a future Unix timestamp, Inngest delays invoking the function until that time. The function itself runs normally — the delay happens before it starts, not within it. This is useful when the delay is known at the time the event is sent and you don't need any logic to run before the wait. # Error handling and retries in Inngest Source: https://www.inngest.com/docs/guides/error-handling Description: Inngest functions automatically retry on failure. Customize retry counts, handle per-step errors, define onFailure handlers, and implement step-level rollbacks. metaTitle = "Error Handling & Retries in Inngest" structuredData = { "@type": "FAQPage", mainEntity: [ { "@type": "Question", name: "What causes an Inngest step to retry?", acceptedAnswer: { "@type": "Answer", text: "An error causes an Inngest step to retry. If the step exhausts all retry attempts, that step fails and will not be attempted again for that run.", }, }, { "@type": "Question", name: "What happens when an Inngest function fails?", acceptedAnswer: { "@type": "Answer", text: "An unhandled error causes the function to fail. The run is marked as failed in the Inngest UI and future executions are cancelled.", }, }, { "@type": "Question", name: "Why should retried steps be idempotent?", acceptedAnswer: { "@type": "Answer", text: "Retries re-run step code, so retried steps should be idempotent and safe to execute multiple times without unintended side effects.", }, }, ], }; Inngest functions automatically retry failed work, persist step state, and let you decide what should happen when a retry cannot recover. This means transient errors like network timeouts, API outages, database locks, and deploy interruptions can be retried without re-running work that already completed. Use Inngest error handling in four layers: - **Automatic retries** retry failed functions and steps with backoff. - **Step-level error handling** lets you catch failed steps with native language features such as `try`/`catch`. - **Failure handlers** run after a function exhausts all retries. - **Rollbacks and idempotency** keep side effects safe when work is retried. Each `step.run()` has its own retry counter. When a step succeeds, its result is saved and reused on later executions, so retries continue from the failed step instead of replaying every completed operation. ## Error handling options } href={'/docs/features/inngest-functions/error-retries/retries'}> Configure how many times Inngest retries a function or step after it throws. } href={'/docs/features/inngest-functions/error-retries/failure-handlers'}> Run cleanup, alerts, or compensating logic after all retries are exhausted. } href={'/docs/features/inngest-functions/error-retries/rollbacks'}> Catch a failed step and run fallback or rollback logic without failing the entire workflow. ## How retries work By default, Inngest retries a function or step up to four times in addition to the initial attempt. You can customize this with the `retries` option on your function, or set `retries: 0` when a function should not retry. Retries happen at the step boundary: - If code inside a `step.run()` throws, Inngest retries that step. - If the retry succeeds, the function continues from that point. - If the step exhausts retries, it fails and throws back into your function. - If previous steps already succeeded, their saved results are reused and not re-run. Throw a [non-retriable error](/docs/features/inngest-functions/error-retries/inngest-errors#non-retriable-error) when an error is permanent and should bypass remaining retries, such as invalid user input or a missing record that will not become available later. ## Errors vs. failures Inngest helps you handle both **errors** and **failures**: - An **error** is an exception thrown by your function or step. Errors cause retries while attempts remain. - A **failed step** is a step that has exhausted all retry attempts. You can catch it with native language error handling. - A **failed function** is a function run that has exhausted retries or received an unhandled failed step. It is marked as failed in the Inngest UI. Use [failure handlers](/docs/features/inngest-functions/error-retries/failure-handlers) when you need a final callback after every retry is exhausted. Use [rollbacks](/docs/features/inngest-functions/error-retries/rollbacks) when you want to handle one failed step and continue or compensate inside the same workflow. ## Keep retried work idempotent Retried code should be idempotent, which means running it more than once does not create duplicate or inconsistent side effects. For example, inserting a new user can create duplicates if the first write succeeded but the response timed out. [Upserting a user](https://www.cockroachlabs.com/blog/sql-upsert/), using deterministic IDs, or checking for existing records before writing are safer retry patterns. Learn how to write retriable steps safely in the [handling idempotency guide](/docs/guides/handling-idempotency). ## FAQ ### Does Inngest retry functions automatically? Yes. Inngest retries failed functions and steps by default. The default is four retries after the first attempt, and you can change the retry count in the function configuration. ### Are retries applied to the whole function or each step? Retries are applied to each step independently. A failed `step.run()` can retry without re-running earlier successful steps because Inngest persists each completed step result. ### How do I handle a step that fails after all retries? Wrap the step in native language error handling such as `try`/`catch`, then run fallback logic, a rollback step, or a different provider. See [step-level rollbacks](/docs/features/inngest-functions/error-retries/rollbacks). ### How do I run code after a function fails permanently? Use a function-level `onFailure` handler, or listen for the [`inngest/function.failed`](/docs/reference/system-events/inngest-function-failed) system event to centralize failure handling across an environment. # Fan-out (one-to-many) Source: https://www.inngest.com/docs/guides/fan-out-jobs Description: Trigger multiple Inngest functions from a single event to run work in parallel. Fan-out decouples producers from consumers for scalable event-driven systems. metaTitle = "Fan-Out Pattern | One Event, Many Functions" The fan-out pattern enables you to send a single event and trigger multiple functions in parallel (one-to-many). The key benefits of this approach are: * **Reliability**: Logic from each function runs independently, meaning an issue with one function will not affect the other(s). * **Performance**: As functions area run in parallel, all of the work will execute faster than running in sequence. A use case for fan-out is, for example, when a user signs up for your product. In this scenario, you may want to: 1. Send a welcome email 2. Start a trial in Stripe 3. Add the user to your CRM 4. Add the user's email to your mailing list {/* TODO - Link to future distributed systems guide*/} The fan-out pattern is also useful in distributed systems where a single event is consumed by functions running in different applications. ## How to fan-out to multiple functions Since Inngest is powered by events, implementing fan-out is as straightforward as defining multiple functions that use the same event trigger. Let's take the above example of user signup and implement it in Inngest. First, set up a `/signup` route handler to send an event to Inngest when a user signs up: ```ts {{ filename: "app/routes/signup/route.ts" }} export async function POST(request: Request) { // NOTE - this code is simplified for the example: await request.json(); await createUser({ email, password }); await createSession(user.id); // Send an event to Inngest await inngest.send({ name: 'app/user.signup', data: { user: { id: user.id, email: user.email, }, }, }); redirect('https://myapp.com/dashboard'); } ``` Now, with this event, any function using `"app/user.signup"` as its event trigger will be automatically invoked. Next, define two functions: `sendWelcomeEmail` and `startStripeTrial`. As you can see below, both functions use the same event trigger, but perform different work. ```ts {{ filename: "inngest/functions.ts" }} inngest.createFunction( { id: 'send-welcome-email', triggers: { event: 'app/user.signup' } }, async ({ event, step }) => { await step.run('send-email', async () => { await sendEmail({ email: event.data.user.email, template: 'welcome'); }); } ) inngest.createFunction( { id: 'start-stripe-trial', triggers: { event: 'app/user.signup' } }, async ({ event }) => { await step.run('create-customer', async () => { return await stripe.customers.create({ email: event.data.user.email }); }); await step.run('create-subscription', async () => { return await stripe.subscriptions.create({ customer: customer.id, items: [{ price: 'price_1MowQULkdIwHu7ixraBm864M' }], trial_period_days: 14, }); }); } ) ``` You've now successfully implemented fan-out in our application. Each function will run independently and in parallel. If one function fails, the others will not be disrupted. Other benefits of fan-out include: * **Bulk Replay**: If a third-party API goes down for a period of time (for example, your email provider), you can use [Replay](/docs/platform/replay) to selectively re-run all functions that failed, without having to re-run all sign-up flow functions. * **Testing**: Each function can be tested in isolation, without having to run the entire sign-up flow. * **New features or refactors**: As each function is independent, you can add new functions or refactor existing ones without having to edit unrelated code. * **Trigger functions in different codebases**: If you have multiple codebases, even using different programming languages (for example [Python](/docs/reference/python) or [Go](https://pkg.go.dev/github.com/inngest/inngestgo)), you can trigger functions in both codebases from a single event. Since Inngest is powered by events, implementing fan-out is as straightforward as defining multiple functions that use the same event trigger. Let's take the above example of user signup and implement it in Inngest. First, set up a `/signup` route handler to send an event to Inngest when a user signs up: ```go {{ filename: "main.go" }} !snippet:path=snippets/go/docs/examples/fan_out_jobs/main.go ``` Now, with this event, any function using `"app/user.signup"` as its event trigger will be automatically invoked. Next, define two functions: `sendWelcomeEmail` and `startStripeTrial`. As you can see below, both functions use the same event trigger, but perform different work. ```go {{ filename: "inngest/functions.go" }} !snippet:path=snippets/go/docs/functions/fan_out_functions.go ``` You've now successfully implemented fan-out in our application. Each function will run independently and in parallel. If one function fails, the others will not be disrupted. Other benefits of fan-out include: * **Bulk Replay**: If a third-party API goes down for a period of time (for example, your email provider), you can use [Replay](/docs/platform/replay) to selectively re-run all functions that failed, without having to re-run all sign-up flow functions. * **Testing**: Each function can be tested in isolation, without having to run the entire sign-up flow. * **New features or refactors**: As each function is independent, you can add new functions or refactor existing ones without having to edit unrelated code. * **Trigger functions in different codebases**: If you have multiple codebases, even using different programming languages (for example [TypeScript](/docs/reference/typescript/intro) or [Python](/docs/reference/python)), you can trigger functions in both codebases from a single event. Since Inngest is powered by events, implementing fan-out is as straightforward as defining multiple functions that use the same event trigger. Let's take the above example of user signup and implement it in Inngest. First, set up a `/signup` route handler to send an event to Inngest when a user signs up: ```py {{ title: "Flask route" }} !snippet:path=snippets/py/v0_5/fan_out_jobs/flask_route.py ``` ```py {{ title: "FastAPI route" }} !snippet:path=snippets/py/v0_5/fan_out_jobs/fast_api_route.py ``` Now, with this event, any function using `"app/user.signup"` as its event trigger will be automatically invoked. Next, define two functions: `sendWelcomeEmail` and `startStripeTrial`. As you can see below, both functions use the same event trigger, but perform different work. ```py {{ filename: "inngest/functions.py" }} !snippet:path=snippets/py/v0_5/fan_out_jobs/functions.py ``` You've now successfully implemented fan-out in our application. Each function will run independently and in parallel. If one function fails, the others will not be disrupted. Other benefits of fan-out include: * **Bulk Replay**: If a third-party API goes down for a period of time (for example, your email provider), you can use [Replay](/docs/platform/replay) to selectively re-run all functions that failed, without having to re-run all sign-up flow functions. * **Testing**: Each function can be tested in isolation, without having to run the entire sign-up flow. * **New features or refactors**: As each function is independent, you can add new functions or refactor existing ones without having to edit unrelated code. * **Trigger functions in different codebases**: If you have multiple codebases, even using different programming languages (for example [TypeScript](/docs/reference/typescript/intro) or [Go](https://pkg.go.dev/github.com/inngest/inngestgo)), you can trigger functions in both codebases from a single event. ## Further reading * [Sending events](/docs/events) * [Invoking functions from within functions](/docs/guides/invoking-functions-directly) * [Sending events from functions](/docs/guides/sending-events-from-functions) # Flow Control Source: https://www.inngest.com/docs/guides/flow-control Description: Manage function execution with flow control: concurrency limits, throttling, rate limiting, event batching, priority, debounce, and singleton runs. import { RiGitPullRequestFill, RiGroupLine, RiSlowDownFill, RiSkipRightFill, } from "@remixicon/react"; metaTitle = "Flow Control in Inngest | Concurrency, Throttling & More" Flow control is a critical part of building robust applications. It allows you to manage the flow of data and events through your application which can help you manage resources, prevent overloading systems, and ensure that your application is responsive and reliable. There are several methods to manage flow control for each Inngest function. Learn about each method and how to use them in your functions: } href={'/docs/guides/multi-tenancy?ref=docs-flow-control'}> Group queued work by tenant or resource to reduce head-of-line blocking and provide best-effort fairness. } href={'/docs/guides/concurrency'}> Limit the number of executing steps across your function runs. Ideal for limiting concurrent workloads by user, resource, or in general. } href={'/docs/guides/throttling'}> Limit the throughput of function execution over a period of time. Ideal for working around third-party API rate limits. } href={'/docs/guides/rate-limiting'}> Prevent excessive function runs over a given time period by _skipping_ events beyond a specific limit. Ideal for protecting against abuse. } href={'/docs/guides/debounce'}> Avoid unnecessary function invocations by de-duplicating events over a sliding time window. Ideal for preventing wasted work when a function might be triggered in quick succession. } href={'/docs/guides/priority'}> Dynamically adjust the execution order of functions based on any data. Ideal for pushing critical work to the front of the queue. {/* NOTE - Should we include 'delaying for flow control - e.g. using the ts' */} # Handling idempotency Source: https://www.inngest.com/docs/guides/handling-idempotency Description: Ensure Inngest functions run exactly once per event using idempotency keys. Prevent duplicate processing when events are retried or delivered more than once. metaTitle = "Idempotency in Inngest | Prevent Duplicate Runs" Ensuring that your code is idempotent is foundational to building reliable systems. Within Inngest, there are multiple ways to ensure that your functions are idempotent. ## What is idempotency? Idempotency, by definition, describes an operation that can occur multiple times without changing the result beyond the initial execution. In the world of software, this means that a functions can be executed multiple times, but it will always have the same effect as being called once. An example of this is an "upsert." ## How to handle idempotency with Inngest It should always be the aim to write code that is idempotent itself within your system or your Inngest functions, but there are also some features within Inngest that can help you ensure idempotency. As Inngest functions are triggered by events, there are two main ways to ensure idempotency: * [at the event level (_the producer_)](#at-the-event-level-the-producer) and/or * [at the function level (_the consumer_)](#at-the-function-level-the-consumer) {/*TODO - New graphic in similar design style ![Relay graphic](/assets/docs/platform/replay/featured-image.png) */} ## At the event level (the producer) Each event that is received by Inngest will trigger any functions with that matching trigger. If an event is sent twice, Inngest will trigger the function twice. This is the default behavior as Inngest does not know if the event is the same event or a new event. **Example:** Using an e-commerce store as an example, a user can add the same t-shirt to their cart twice because they want to buy two (_2 unique events_). That same user may check out and pay for all items in their cart but click the "pay" button twice (_2 duplicate events_). To prevent an event from being handled twice, you can set a unique event `id` when [sending the event](/docs/reference/typescript/v4/events/send#inngest-send-event-payload-event-payload-promise). This `id` acts as an idempotency key **over a 24 hour period** and Inngest will check to see if that event has already been received before triggering another function. ```ts 'CGo5Q5ekAxilN92d27asEoDO'; await inngest.send({ id: `checkout-completed-${cartId}`, // <-- This is the idempotency key name: 'cart/checkout.completed', data: { email: 'taylor@example.com', cartId: cartId } }) ``` ```go {{ title: "Go" }} cart_id := "CGo5Q5ekAxilN92d27asEoDO" inngest.Send(context.Background(), inngestgo.Event{ ID: fmt.Sprintf("checkout-completed-%s", cart_id), // <-- This is the idempotency key Name: "cart/checkout.completed", Data: map[string]any{"email": "taylor@example.com", "cart_id": cart_id}, }) ``` ```python {{ title: "Python" }} cart_id = 'CGo5Q5ekAxilN92d27asEoDO' await inngest.send({ id: f'checkout-completed-{cart_id}', // <-- This is the idempotency key name: 'cart/checkout.completed', data: { email: 'taylor@example.com', cart_id: cart_id } }) ``` | Event ID | Timestamp | Function | | -------- | --------- | -------- | | `checkout-completed-CGo5Q5ekAxilN92d27asEoDO` | 08:00:00.000 | ✅ Functions are triggered | | `checkout-completed-CGo5Q5ekAxilN92d27asEoDO` | 08:00:00.248 | ❌ Nothing is triggered | As you can see in the above example, setting the `id` allows you to prevent duplicate execution on the producer side, where the event originates. Some other key points to note: * Event IDs will only be used to prevent duplicate execution for a 24 hour period. After 24 hours, the event will be treated as a new event and will trigger any functions with that trigger. * Inngest will store the second event and it will be visible in your event history, but it will _not_ trigger any functions. * Events that fan-out to multiple functions will trigger each function as they normally would. {/* TODO - Link to revised fan-out guide above when complete */} **Tip** - If you are using Inngest's [webhook transforms](/docs/platform/webhooks#defining-a-transform-function), you can set the `id` in the transform to ensure that the event is idempotent. Event idempotency is ignored by some features: - Debouncing - Event batching - Function pausing. While a function is paused, event idempotency is ignored. So if a replay is created after unpausing, it may have "skipped" runs that ignored event idempotency. {/*### Uniqueness of event IDs TODO - Highlight the importance of the uniqueness of the event ID across events that may have different names*/} ## At the function level (the consumer) You might prefer to ensure idempotency at the function level or you may not be able to control the event that is being sent (from a webhook). The [function's `idempotency` config option](/docs/reference/typescript/v4/functions/create#inngest-create-function-configuration-trigger-handler-inngest-function) allows you to do this. Each function's `idempotency` key is defined as a [CEL expression](/docs/guides/writing-expressions) that is evaluated with the event payload's data. The expression is used to generate a unique string key which idempotently prevents duplicate execution of the function. Each unique expression will only trigger one function execution **per 24 hour period**. After 24 hours, a new event that generates the same unique expression will trigger another function execution. ### Example We'll use the same example of an e-commerce store to demonstrate how this works. We have an event here with no `id` set ([see above](#at-the-event-level-the-producer)), but we want to ensure that the `send-checkout-email` function is only triggered once for each `cartId` to prevent duplicate emails being sent. ```json {{ title: "Event payload"}} { "name": "cart/checkout.completed", "data": { "email": "blake@example.com", "cartId": "s6CIMNqIaxt503I1gVEICfwp" }, "ts": 1703275661157 } ``` ```ts {{ title: "Function definition with idempotency key"}} sendEmail = inngest.createFunction( { id: 'send-checkout-email', // This is the idempotency key idempotency: 'event.data.cartId', // Evaluates to: "s6CIMNqIaxt503I1gVEICfwp" // for the given event payload triggers: { event: 'cart/checkout.completed' }, }, async ({ event, step }) => { /* ... */ } }) ``` ### Writing CEL expressions While CEL can do many things, we'll focus on how to use it to generate a unique string key for idempotency. The key things to know are: * You can access any of the event payload's data using the `event` variable and dot-notation for nested properties. * You can use the `+` operator to concatenate strings together. Combining two or more properties together is a good way to ensure the level of uniqueness that you need. Here are couple of examples: * **User signup:** You only want to send a welcome email once per user, so you'd set `idempotency` to `event.data.userId` in case there your API sends duplicate events. * **Organization team invite:** A user may be part of multiple organizations in your app. You only want to send a team invite email once per user/organization combination, so you'd set `idempotency` to `event.data.userId + "-" + event.data.organizationId`. For more information on writing CEL expressions, read [our guide](/docs/guides/writing-expressions). 💡 If you want to control when a function is executed over a period of time you might prefer: * [`rateLimit`](/docs/reference/typescript/v4/functions/rate-limit) - Limit the number of function executions per period of time * [`debounce`](/docs/reference/typescript/v4/functions/debounce) - Delay function execution for duplicate events over a period of time ### Idempotency keys and fan-out {/* TODO - This should link to a future iteration of the fan-out guide which is 1 event, n functions */} One reason why you might want to use `idempotency` at the function level is if you have an `event` that fans-out to multiple functions. Let's take the following fan-out example: | Function | Event trigger | How often | | -------- | ------------- | --------- | | Track requests | `ai/generation.requested` | Every time | | Run generation | `ai/generation.requested` | Once per request | In this case, you would want to set `idempotency` on the "Run generation" function to ensure that it runs once, for example, for every unique prompt that is sent. You may want to do this as you don't want to re-run the same exact prompt and waste compute resources/credits. However, you still might want to track the number of requests that each user submitted, so you would not want to set `idempotency` on the "Track requests" function. You can see the code for both functions below.
**View the function code** Both functions use the same event trigger, `ai/generation.requested` which contains a `promptHash` and a `userId` in the event payload. ```ts {{ title: "Track requests function" }} inngest.createFunction( { id: 'track-requests', triggers: { event: 'ai/generation.requested' } }, async ({ event, step }) => { // Track the request } ) ``` ```ts {{ title: "Run generation function" }} inngest.createFunction( { id: 'run-generation', // Given the event payload sends a hash of the prompt, // this will only run once per unique prompt per user // every 24 hours: idempotency: `event.data.promptHash + "-" + event.data.userId`, triggers: { event: 'ai/generation.requested' }, }, async ({ event, step }) => { // Track the request } ) ```
# Guides Source: https://www.inngest.com/docs/guides/index Description: In-depth guides for building with Inngest: flow control, error handling, scheduled jobs, webhook integrations, fan-out patterns, idempotency, and more. metaTitle = "Inngest Guides | Patterns, Integrations & How-Tos" hidePageSidebar = true; Learn how to build with Inngest: ## Patterns # Instrumenting GraphQL Source: https://www.inngest.com/docs/guides/instrumenting-graphql Description: Add Inngest background jobs and durable workflows to a GraphQL API. Trigger functions from mutations and handle long-running work outside the request cycle. metaTitle = "Instrument GraphQL with Inngest" {/* Intro discussing what instrumenting GraphQL means */} When building with GraphQL, you can give your event-driven application a kick-start by instrumenting every query and mutation, sending events when one is successfully executed. {/* Describe that we can use a plugin for this, and how it's compatible */} {/* Mention Redwood */} We can do this using an [Envelop](https://envelop.dev/) plugin, `useInngest`, for [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) and servers or frameworks powered by Yoga, such as [RedwoodJS](https://www.redwoodjs.com/). {/* Discuss the benefits of this */} By instrumenting with the `useInngest` plugin: - Get an immediate set of events to react to that automatically grows with your GraphQL API. - No changes to your existing resolvers are ever needed. - Utilise fine-grained control over what events are sent such as operations (queries, mutations, or subscriptions), introspection events, when GraphQL errors occur, if result data should be included, type and schema coordinate denylists, and more. - Automatically capture context such as user data. {/* Show code adding this to a simple Yoga schema */} ## Getting Started ```sh npm install envelop-plugin-inngest # or yarn add ``` ### Usage example Using `useInngest` just requires that you have an Inngest client (see the [Quick start](/docs/getting-started/nextjs-quick-start)) set up with an appropriate event key (see [Creating an event key](https://www.inngest.com/docs/events/creating-an-event-key)). Here's a single-file example of how to add the plugin. ```ts new Inngest({ id: "my-app" }); // Provide your schema createYoga({ schema: createSchema({ typeDefs: /* GraphQL */ ` type Query { greetings: String! } `, resolvers: { Query: { greetings: () => "Hello World!", }, }, }), // Add the plugin to the server. RedwoodJS users can use the // `extraPlugins` option instead. plugins: [useInngest({ inngestClient: inngest })], }); // Start the server and explore http://localhost:4000/graphql createServer(yoga); server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql"); }); ``` {/* Discuss and show screenshots of example events */} ### Output events Once the plugin is installed, an event will be sent for all successful GraphQL operations, resulting in a ready-to-use set of events that you can react to immediately. Here's an example event sent from a mutation to create a new item in a user's cart: ```json { "name": "graphql/create-cart-item.mutation", "data": { "identifiers": [ { "id": 27, "typename": "CartItem" } ], "operation": { "id": "create-cart-item", "name": "CreateCartItem", "type": "mutation" }, "result": { "data": { "createCartItem": { "id": 27, "productId": "123" } } }, "types": [ "CartItem" ], "variables": {} }, "id": "01GXXAQ1M0A1SFVGEHACRF4K1C" } ``` ### Reacting to events We can react to this event by creating a new Inngest function with the event as the trigger. ```ts inngest.createFunction( { id: "send-cart-alert", triggers: { event: "graphql/create-cart-item.mutation" } }, async ({ event }) => { await sendSlackMessage( "#marketing", `Someone added product #${event.data.identifiers[0].id} to their cart!` ); } ); ``` {/* To get more info, see the `envelop-plugin-inngest` repo */} For more info on how to customize the events sent check out the [envelop-plugin-inngest](https://github.com/inngest/envelop-plugin-inngest) repository, or see [Writing functions](https://www.inngest.com/docs/learn/inngest-functions) to learn how to react to these events in different ways. # Invoking functions directly Source: https://www.inngest.com/docs/guides/invoking-functions-directly Description: Call Inngest functions directly from other functions using step.invoke() and wait for their return value. Compose workflows across multiple durable functions. metaTitle = "Invoke Inngest Functions Directly | step.invoke() Guide" Inngest's `step.invoke()` function provides a powerful tool for calling functions directly within your event-driven system. It differs from traditional event-driven triggers, offering a more direct, RPC-like approach. This encourages a few key benefits: - Allows functions to call and receive the result of other functions - Naturally separates your system into reusable functions that can spread across process boundaries - Allows use of synchronous interaction between functions in an otherwise-asynchronous event-driven architecture, making it much easier to manage functions that require immediate outcomes - Enables [synchronous sub-agent delegation](/docs/ai-patterns/sub-agent-delegation#delegate-synchronously-with-stepinvoke) in AI agent systems ## Invoking another function ### When should I invoke? Use `step.invoke()` in tasks that need specific settings like concurrency limits. Because it runs with its own configuration, distinct from the invoker's, you can provide a tailored configuration for each function. If you don't need to define granular configuration or if your function won't be reused across app boundaries, use `step.run()` for simplicity. ```ts // Some function we'll call inngest.createFunction( { id: "compute-square", triggers: { event: "calculate/square" } }, async ({ event }) => { return { result: event.data.number * event.data.number }; // Result typed as { result: number } } ); // In this function, we'll call `computeSquare` inngest.createFunction( { id: "main-function", triggers: { event: "main/event" } }, async ({ step }) => { await step.invoke("compute-square-value", { function: computeSquare, data: { number: 4 }, // input data is typed, requiring input if it's needed }); return `Square of 4 is ${square.result}.`; // square.result is typed as number } ); ``` In the above example, our `mainFunction` calls `computeSquare` to retrieve the resulting value. `computeSquare` can now be called from here or any other process connected to Inngest. ## Referencing another Inngest function If a function exists in another app, you can create a reference that can be invoked in the same manner as the local `computeSquare` function above. ```ts // @/inngest/computeSquare.ts // Create a reference to a function in another application. computeSquare = referenceFunction({ appId: "my-python-app", functionId: "compute-square", // Schemas are optional, but provide types for your call if specified schemas: { data: z.object({ number: z.number(), }), return: z.object({ result: z.number(), }), }, }); ``` ```ts // square.result is typed as a number await step.invoke("compute-square-value", { function: computeSquare, data: { number: 4 }, // input data is typed, requiring input if it's needed }); ``` References can also be used to invoke local functions without needing to import them (and their dependencies) directly. This can be useful for frameworks like Next.js where edge and serverless handlers can be mixed together and require different sets of dependencies. ```ts // Import only the type inngest.createFunction( { id: "main-function", triggers: { event: "main/event" } }, async ({ step }) => { await step.invoke("compute-square-value", { function: referenceFunction({ functionId: "compute-square", }), data: { number: 4 }, // input data is still typed }); return `Square of 4 is ${square.result}.`; // square.result is typed as number } ); ``` For more information on referencing functions, see [TypeScript -> Referencing Functions](/docs/functions/references). ### When should I invoke? Use `step.Invoke()` in tasks that need specific settings like concurrency limits. Because it runs with its own configuration, distinct from the invoker's, you can provide a tailored configuration for each function. If you don't need to define granular configuration or if your function won't be reused across app boundaries, use `step.Run()` for simplicity. ```go import ( "context" "fmt" "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) // Some function we'll call inngestgo.CreateFunction( client, inngestgo.FunctionOpts{Name: "compute-square"}, inngestgo.EventTrigger("calculate/square", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { number, ok := input.Event.Data["number"].(float64) if !ok { return nil, fmt.Errorf("invalid number") } return map[string]any{ "result": int(number * number), }, nil }, ) // In this function, we'll call the compute-square function inngestgo.CreateFunction( client, inngestgo.FunctionOpts{Name: "main-function"}, inngestgo.EventTrigger("main/event", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { square, err := step.Invoke(ctx, "compute-square-value", &inngestgo.InvokeOpts{ Function: "compute-square", Data: map[string]any{ "number": 4, }, }) if err != nil { return nil, err } result := square.Data["result"].(int) return fmt.Sprintf("Square of 4 is %d.", result), nil }, ) ``` In the above example, our `mainFunction` calls `computeSquare` to retrieve the resulting value. `computeSquare` can now be called from here or any other process connected to Inngest. ### When should I invoke? Use `step.invoke()` in tasks that need specific settings like concurrency limits. Because it runs with its own configuration, distinct from the invoker's, you can provide a tailored configuration for each function. If you don't need to define granular configuration or if your function won't be reused across app boundaries, use `step.run()` for simplicity. ```py import inngest from src.inngest.client import inngest_client # Some function we'll call @inngest_client.create_function( fn_id="compute-square", trigger=inngest.TriggerEvent(event="calculate/square") ) async def compute_square(ctx: inngest.Context): return {"result": ctx.event.data["number"] * ctx.event.data["number"]} # Result typed as { result: number } # In this function, we'll call compute_square @inngest_client.create_function( fn_id="main-function", trigger=inngest.TriggerEvent(event="main/event") ) async def main_function(ctx: inngest.Context): square = await ctx.step.invoke( "compute-square-value", function=compute_square, data={"number": 4} # input data is typed, requiring input if it's needed ) return f"Square of 4 is {square['result']}." # square.result is typed as number ``` In the above example, our `mainFunction` calls `compute_square` to retrieve the resulting value. `compute_square` can now be called from here or any other process connected to Inngest. ## Creating a distributed system You can invoke Inngest functions written in any language, hosted on different clouds. For example, a TypeScript function on Vercel can invoke a Python function hosted in AWS. By starting to define these blocks of functionality, you're creating a smart, distributed system with all of the benefits of event-driven architecture and without any of the hassle. ## Similar pattern: Fan-Out A similar pattern to invoking functions directly is that of fan-out - [check out the guide here](/docs/guides/fan-out-jobs). Here are some key differences: - Fan-out will trigger multiple functions simultaneously, whereas invocation will only trigger one - Unlike invocation, fan-out will not receive the result of the invoked function - Choose fan-out for parallel processing of independent tasks and invocation for coordinated, interdependent functions # Logging in Inngest Source: https://www.inngest.com/docs/guides/logging Description: Add structured logging to your Inngest functions using the built-in logger. Logs appear in the Inngest dashboard alongside step traces for each function run. metaTitle = "Logging Inside Inngest Functions" Log handling can have some caveats when working with serverless runtimes. One of the main problems is due to how serverless providers terminate after a function exits. There might not be enough time for a logger to finish flushing, which results in logs being lost. Another (opposite) problem is due to how Inngest handles memoization and code execution via HTTP calls to the SDK. A log statement outside of `step` function could end up running multiple times, resulting in duplicated deliveries. ```ts {{ title: "example-fn.ts" }} async ({ event, step }) => { console.log("something") // this can be run three times await step.run("fn", () => { console.log("something else") // this will always be run once }) await step.run(...) } ``` We provide a thin wrapper over existing logging tools, and export it to Inngest functions in order to mitigate these problems, so you, as the user, don't need to deal with them and things should work as you expect. ## Usage A `logger` object is available within all Inngest functions as a handler argument. You can use it with the logger of your choice, or if absent, `logger` will default to use `console`. The SDK uses **Pino-style object-first** logging, where structured data is passed before the message string: ```ts inngest.createFunction( { id: "my-awesome-function", triggers: { event: "func/awesome" } }, async ({ event, step, logger }) => { logger.info({ eventId: event.data.id }, "Starting function"); await step.run("do-something", () => { if (somethingBadHappens) logger.warn("something bad happened"); }); return { success: true, event }; } ); ``` We recommend using a structured logger like [Pino](https://github.com/pinojs/pino) that supports a child logger `.child()` implementation, which automatically adds function runtime metadata to your logs. Read more about [enriched logs with function metadata](#enriched-logs-with-function-metadata) for more details. ## Using your preferred logger While the default `ConsoleLogger` may be good enough for local development, structured logging libraries provide more features that are suitable for production use. Pass a logger to the `logger` option on the Inngest client to make it available as `ctx.logger` in all functions. ```ts {{ title: "Pino" }} pino({ level: "debug" }); inngest = new Inngest({ id: "my-awesome-app", logger: logger, }); inngest.createFunction( { id: 'my-fn', }, ({ event, step, logger }) => { logger.info({ hello: "world" }, "this uses my pino logger"); } ); ``` ```ts {{ title: "Winston" }} winston.createLogger({ level: "info", format: winston.format.json(), transports: [new winston.transports.Console()], }); // Winston uses string-first logging, so wrap it for compatibility inngest = new Inngest({ id: "my-awesome-app", logger: wrapStringFirstLogger(logger), }); inngest.createFunction( { id: 'my-fn', }, ({ event, step, logger }) => { logger.info("this uses my winston logger", { hello: "world" }); } ); ``` ### Object-first vs string-first loggers The SDK expects **object-first** loggers (like [Pino][pino]), where structured data comes before the message: ```ts // Object-first (Pino style) - works out of the box logger.info({ userId: "abc" }, "User created"); ``` Some loggers like [Winston][winston] use **string-first** conventions, where the message comes first. For these loggers, use `wrapStringFirstLogger` to adapt them: ```ts wrapStringFirstLogger(winstonLogger); ``` See the [Logging reference](/docs/reference/typescript/v4/logging) for more details on logger configuration, the `ConsoleLogger`, and the `internalLogger` option. ## Enriched logs with function metadata If the logger library supports a child logger `.child()` implementation, the built-in middleware will utilize it to add function runtime metadata to your logs automatically: - Function name - Event name - Run ID ```ts {{ title: "Example usage with Pino logger" }} await step.run("summarize-content", async ({ step, logger }) => { logger.info({ max_tokens: 1000 }, "Calling Claude"); }); ``` ```json {{ title: "Example log output" }} {"eventName":"inngest/function.invoked","functionName":"Summarize content via GPT-4", "level":"info","max_tokens":1000,"message":"Calling Claude", "runID":"01KB7YQXYNPEX3XB257A3RQDRX"} ``` ## Loggers supported The following is a list of loggers we're aware of that work, but is not an exhaustive list: - [Pino][pino] child logger support - [Winston][winston] child logger support (requires `wrapStringFirstLogger`) - [Bunyan](https://github.com/trentm/node-bunyan) child logger support - [Roarr](https://github.com/gajus/roarr) child logger support - [LogLevel](https://github.com/pimterry/loglevel) - [Log4js](https://github.com/log4js-node/log4js-node) - [npmlog](https://github.com/npm/npmlog) (doesn't have `.debug()` but has a way to add custom levels) - [Tracer](https://github.com/baryon/tracer) - [Signale](https://github.com/klaudiosinani/signale) ## Customizing the logger The built-in logger is implemented using [middleware](/docs/features/middleware). You can create your own middleware to customize the logger to your needs. See the [logging middleware example](/docs/reference/typescript/v4/middleware/examples#logging) for more details. ## Further reading - [Logging reference](/docs/reference/typescript/v4/logging) - Full details on logger configuration, `ConsoleLogger`, `internalLogger`, and `wrapStringFirstLogger`. - [Traces](/docs/platform/monitor/traces?ref=guides-logging) - View detailed execution traces for your functions in the Inngest dashboard, including step-by-step breakdowns and timing information. [pino]: https://github.com/pinojs/pino [winston]: https://github.com/winstonjs/winston # Multi-tenancy and flow control Source: https://www.inngest.com/docs/guides/multi-tenancy Description: Learn how Inngest groups queued work by flow control keys to reduce head-of-line blocking and provide best-effort fairness between tenants. metaTitle = "Multi-tenant Flow Control with Keys" In a multi-tenant system, one tenant can create much more work than another. If every item waits in a single queue, a busy tenant can delay other tenants even when their work is ready to run. This is **head-of-line blocking**, often described as the noisy neighbor problem. Inngest reduces head-of-line blocking by grouping queued work with flow control **keys**. Each unique key value gets its own application of the configured flow control limit. These per-key groups are sometimes called **key queues**. **Availability:** The key queue scheduling system described on this page is currently available to Enterprise customers on request. [Contact our team](/contact?ref=docs-multi-tenancy) to request access. We plan to make key queues more broadly available in the future. ## Why group work by key Consider a function serving two tenants: | Tenant | Queued work | Current state | | --- | ---: | --- | | Tenant A | 1,000 items | At its flow control limit | | Tenant B | 1 item | Ready to run | In a single FIFO queue, Tenant B's item can sit behind hundreds of items from Tenant A. Because Tenant A is already at its limit, repeatedly finding its items does not produce runnable work. The chance of reaching Tenant B's item is extremely low until the queue advances far enough. With a key such as `event.data.tenant_id`, Inngest groups Tenant A and Tenant B into separate key queues. The scheduler can consider Tenant B's ready queue without first moving through Tenant A's backlog. This provides **best-effort fairness** between keys while continuing to enforce each tenant's limit. Key queues are part of the scheduling and execution system described in the [Inngest Cloud architecture](/docs/architecture?ref=docs-multi-tenancy) overview. You configure the key in your function; Inngest manages the queue grouping and scheduling. ## How flow control keys work Flow control features such as [concurrency](/docs/guides/concurrency?ref=docs-multi-tenancy) and [throttling](/docs/guides/throttling?ref=docs-multi-tenancy) accept an optional key expression. Inngest evaluates the expression for each item and applies the limit independently to each resulting value. For example, this function uses the tenant ID for both limits: ```ts inngest.createFunction( { id: "sync-tenant-data", triggers: { event: "app/tenant.sync.requested" }, concurrency: { key: "event.data.tenant_id", limit: 5, }, throttle: { key: "event.data.tenant_id", limit: 100, period: "1m", }, }, async ({ event, step }) => { await step.run("sync-data", async () => { await syncTenantData(event.data.tenant_id); }); } ); ``` If events contain `tenant_id` values of `tenant-a` and `tenant-b`, each tenant receives its own concurrency limit of five executing steps and its own throttle limit of 100 run starts per minute. A backlog for one tenant does not consume the other tenant's per-key limit. Choose a stable key that represents the resource or tenant you want to isolate, such as an account ID, workspace ID, or user ID. Keys are [expressions evaluated from event data](/docs/guides/writing-expressions?ref=docs-multi-tenancy), so the same function can create groups dynamically without you provisioning queues. ## Ordering within each key Inngest applies [best-effort FIFO ordering](/docs/guides/concurrency?ref=docs-multi-tenancy#queue-ordering) within each key queue. Older work for a key is generally selected before newer work for that same key. Scheduling across different keys favors fairness rather than a single global FIFO order. If you change a function's key expression, already queued items remain grouped by the key value that was evaluated when they entered the queue. Their relative ordering stays stable. Only newly queued items are grouped using the new key expression. Best-effort fairness and FIFO ordering are scheduling goals, not strict execution-order guarantees. Retries, available capacity, and other flow control constraints can affect which item runs next. ## Next steps - [Configure multi-tenant concurrency](/docs/guides/concurrency?ref=docs-multi-tenancy#concurrency-keys-multi-tenant-concurrency) - [Configure throttling by key](/docs/guides/throttling?ref=docs-multi-tenancy#how-to-configure-throttling) - [Learn how queue items are scheduled and executed](/docs/architecture?ref=docs-multi-tenancy#from-event-to-function-execution) # Multiple triggers & wildcards Source: https://www.inngest.com/docs/guides/multiple-triggers Description: Trigger a single Inngest function from multiple events or use wildcard patterns to match a family of events. Reduce code duplication across similar workflows. metaTitle = "Multiple Triggers & Event Wildcards" Inngest functions can be configured to trigger on multiple events or schedules. Using multiple triggers is useful for running the same logic for a wide array of events, or ensuring something also runs on a schedule, for example running an integrity check every morning, or when requested using an event. Multiple triggers can be configured using an [list of triggers](#multiple-triggers), or [wildcard event triggers](#wildcard-event-triggers). ## Multiple triggers Functions support up to 10 unique triggers. This allows you to explicitly match multiple events, or schedules. Multiple schedules that overlap will be de-duplicated - Learn more about [overlapping crons](#overlapping-crons). ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "resync-user-data", triggers: [ { event: "user.created" }, { event: "user.updated" }, { cron: "0 5 * * *" }, // Every morning at 5am ], }, async ({ event, step }) => { // ... }, ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( client, inngestgo.FunctionOpts{Name: "resync-user-data"}, inngestgo.MultipleTriggers{ inngestgo.EventTrigger("user.created", nil), inngestgo.EventTrigger("user.updated", nil), inngestgo.CronTrigger("0 5 * * *"), }, func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // ... return nil, nil }, ) ``` ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="resync-user-data", trigger=[ inngest.TriggerEvent(event="user.created"), inngest.TriggerEvent(event="user.updated"), inngest.TriggerCron(cron="0 5 * * *") ], ) def my_handler(ctx: inngest.Context) -> None: # ... ``` ## Wildcard event triggers Event triggers can be configured using wildcards to match multiple events. This is useful for matching entire groups of events for cases like forwarding events to another system, like an real-time ETL. Wildcards can be used after any `/` or `.` character to match entire groups of events. Here are some examples: * `app/*` matches any event with the `app` prefix, like `app/user.created` and `app/blog.post.published`. * `app/user.*` matches any event with the `app/user.` prefix, like `app/user.created` and `app/user.updated`. * `app/blog.post.*` matches any event with the `app/blog.post.` prefix, like `app/blog.post.published`. Wildcards cannot be used following any characters other than `/` and `.` or in the middle of a pattern, so mid-word wildcards like `app/user.update*` and `app/blog.*.published` are not supported. ### Defining types for wildcard triggers To define types for wildcard triggers, you need to explicitly define ```ts eventType({ name: "app/blog.post.created", data: {} as { postId: string; authorId: string; createdAt: string; }, }); eventType({ name: "app/blog.post.published", data: {} as { postId: string; authorId: string; publishedAt: string; }, }); new Inngest({ id: "my-app", }); inngest.createFunction( { id: "blog-updates-to-slack", triggers: [{ event: "app/blog.post.*" }] }, async ({ event, step }) => { // ... }, ); ``` ## Determining event types In the handler for a function with multiple triggers, the event that triggered the function can be determined using the `event.name` property. ```ts {{ title: "TypeScript" }} async ({ event }) => { // ^? type event: EventA | EventB | InngestScheduledEvent | InngestFnInvoked if (event.name === "a") { // `event` is type narrowed to only the `a` event } else if (event.name === "b") { // `event` is type narrowed to only the `b` event } else { // `event` is type narrowed to only the `inngest/function.invoked` event } } ``` ```go {{ title: "Go" }} func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { switch input.Event.Name { case "a": // Handle event A case "b": // Handle event B case "inngest/function.invoked": // Handle function invoked event } return nil, nil }, ``` Note that batches of `events` can contain many different events; you will need to assert the shape of each event in a batch individually. ## Overlapping crons If your function defines multiple cron triggers, the schedules may sometimes overlap. For example, a cron `0 * * * *` that runs every hour and a cron `*/30 * * * *` that runs every half hour would overlap at the start of each hour. Only one cron job will be run for each given second, so in this case above, one cron would run every half hour. {/* There's a place for wildcards here if we want to talk about them. They're pretty undocumented... */} # Function Pausing Source: https://www.inngest.com/docs/guides/pause-functions Description: Temporarily pause an Inngest function to stop new runs from starting, then resume it when ready. Useful during incidents or planned maintenance windows. metaTitle = "Pause & Resume Inngest Functions" Inngest allows you to pause a function indefinitely. This is a powerful feature that can be useful, for example, if you are planning a maintenance window or if a user reports a bug and you want to stop processing events until you've fixed it. ## When a function is paused It's important to understand what happens when a function is paused. **No data will be lost.** Inngest will continue receiving and storing your events, but the events will not trigger the paused function. The function will be marked as "skipped" on the event's page in Inngest Cloud. **You can resume the function at any time.** No deployment or sync is required to resume your function. After you resume the function, new events will trigger it as usual. **Events received while the function was paused will not be reprocessed automatically** after you resume the function. Use [Replay](/docs/platform/replay) to process events that were received while the function was paused. **Paused functions do not count toward your plan's concurrency limit.** Note that events received while a function is paused are still subject to your plan's history limit. ## How to pause a function Navigate to the function's dashboard in Inngest Cloud and select the "Pause" option in the "All actions" menu from a function's dashboard. ![The Pause option within the "All actions" menu on a function's dashboard.](/assets/docs/platform/function-dashboard-actions-2025-05-05.png) ### Handling running invocations When you pause a function, no new events will trigger it. You can choose what to do with any currently-running invocations of the function: - **"Pause immediately, then cancel after 7 days:"** No further steps will be executed while the function is paused. If you resume the function within 7 days, these invocations will continue with the next step. Otherwise, they will be canceled. (This is the default behavior.) - **"Cancel immediately:"** All currently-running invocations of the function will be canceled. They will not continue running or restart when you resume the function. In both cases, all running invocations will complete their current step before being paused or canceled. Inngest cannot interrupt your function mid-step.
![Options for handling running invocations when pausing a function.](/assets/docs/platform/pause/pause-modal-2025-05-05.png)
## Resuming a function To resume a paused function, navigate to the function's page in Inngest Cloud and select the "Resume" option in the "All actions" menu from a function's dashboard. ![The Resume option within the "All actions" menu on a function's dashboard.](/assets/docs/platform/pause/resume-function-2025-05-05.png) The function will immediately begin processing events received after you resume it. ## Replaying skipped events After resuming a paused function, you may wish to replay the runs for that function that would otherwise have run while it was paused. To do so: 1. Navigate to the function's dashboard 2. Select the "Replay" option in the "All actions" menu. 3. Select an appropriate date window and enable the "Skipped" status. 4. If you wish to replay runs that were canceled when you paused the function, select the "Canceled" status as well. You'll see a preview of the number of runs to be replayed: ![Creating a replay for skipped function runs.](/assets/docs/platform/pause/replay-modal-2025-05-05.png) Remember that your plan's history limit still applies to events received while a function is paused. This means that if, for example, you're using Inngest's free plan, you will only be able to replay events from the last 3 days, regardless of how long your function was paused. See our [Replay guide](/docs/platform/replay) for more information. # Priority Source: https://www.inngest.com/docs/guides/priority Description: Dynamically rank Inngest function runs using a CEL expression over event data. Ensure high-priority work executes first without managing separate queues. metaTitle = "Function Run Priority | Push Critical Work First" Priority allows you to dynamically execute some runs ahead or behind others based on any data. This allows you to prioritize some jobs ahead of others without the need for a separate queue. Some use cases for priority include: * Giving higher priority based on a user's subscription level, for example, free vs. paid users. * Ensuring that critical work is executed before other work in the queue. * Prioritizing certain jobs during onboarding to give the user a better first-run experience. ## How to configure priority ```ts export default inngest.createFunction( { id: "ai-generate-summary", priority: { // For enterprise accounts, a given function run will be prioritized // ahead of functions that were enqueued up to 120 seconds ago. // For all other accounts, the function will run with no priority. run: "event.data.account_type == 'enterprise' ? 120 : 0", }, triggers: { event: "ai/summary.requested" }, }, async ({ event, step }) => { // This function will be prioritized based on the account type } ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "ai-generate-summary", Priority: &inngest.ConfigPriority{ Run: inngestgo.StrPtr("event.data.account_type == 'enterprise' ? 120 : 0"), }, }, inngestgo.EventTrigger("ai/summary.requested", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // This function will be prioritized based on the account type return nil, nil }, ) ``` ```py {{ title: "Python" }} @inngest.create_function( id="ai-generate-summary", priority=inngest.Priority( run="event.data.account_type == 'enterprise' ? 120 : 0", ), trigger=inngest.Trigger(event="ai/summary.requested") ) async def ai_generate_summary(ctx: inngest.Context): # This function will be prioritized based on the account type ``` ### Configuration reference * `run` - A dynamic factor [expression](/docs/guides/writing-expressions), that evaluates to seconds, to prioritize the function by. Returning a positive number will increase that priority ahead of other jobs already in the queue. Returning a negative number will delay the function run's jobs by the given value in seconds. ## How priority works Functions are scheduled in a priority queue based on the time they should run. By default, all functions are enqueued at the current time (a factor of `0`). If a function has `priority` configured, Inngest evaluates the `run` expression for each new function run based on the input event's data. The `run` expression should return a factor, in seconds, (positive or negative) to adjust the priority of the function run. Expressions that return a **positive** number will **increase the priority** of the function run ahead of other jobs already in the queue by the given value in seconds. The function will be run ahead of other jobs that were enqueued up to that many seconds ago. For example, if a function run is scheduled with a factor of `120`, it will run ahead of any jobs enqueued in the last 120 seconds, given that they are still in the queue and have not completed. Expressions that return a **negative** number will **delay** the function run by the given value in seconds. ### Practical example Given we have three jobs in the queue, each which was enqueued at the following times: ```plaintext Jobs: [A, B, C ] Priority/Time: [12:00:10, 12:00:40, 12:02:10] ``` If the current time is `12:02:30`, and two new jobs are enqueued with the following `run` factors: ```plaintext - Job X: factor 0 - Job Y: factor 120 ``` Then Job Y will run ahead of Job X. Job Y will also run before any jobs scheduled 120 seconds beforehand. The queue will look like this: ```plaintext Jobs: [A, Y, B, C, X ] Priority/Time: [12:00:10, 12:00:30, 12:00:40, 12:02:10, 12:02:30] │ │ └ 12:02:30 - 120s = 12:00:30 └ 12:02:30 - 0s = 12:02:30 ``` Job Y was successfully prioritized by a factor of `120` seconds ahead of other jobs in the queue. ## Combining with concurrency Prioritization is most useful when combined with a flow control option that limits throughput, such as [concurrency](/docs/guides/concurrency). Jobs often wait in the queue when limiting throughput, so prioritization allows you to control the order in which jobs are executed in that backlog. ## Limitations * The highest priority is `600` (seconds). * The lowest priority is `-600` (seconds). * Not compatible with [batching](/docs/guides/batching). ## Further reference * [TypeScript SDK Reference](/docs/reference/typescript/v4/functions/run-priority) * [Python SDK Reference](/docs/reference/python/functions/create#configuration) # Rate limiting Source: https://www.inngest.com/docs/guides/rate-limiting Description: Prevent function runs from exceeding a rate over a time window by skipping events beyond the limit. Protect against abuse or third-party API quota exhaustion. metaTitle = "Rate Limiting Inngest Functions | Skip Excess Events" Rate limiting is a _hard limit_ on how many function runs can start within a time period. Events that exceed the rate limit are _skipped_ and do not trigger functions to start. This prevents excessive function runs over a given time period. Some use cases for rate limiting include: * Preventing abuse of your system. * Reducing frequency of data synchronization functions. * Skipping noisy or duplicate [webhook](/docs/platform/webhooks) events. ## How to configure rate limiting ```ts {{ title: "TypeScript" }} export default inngest.createFunction( { id: "synchronize-data", rateLimit: { limit: 1, period: "4h", key: "event.data.company_id", }, triggers: { event: "intercom/company.updated" }, }, async ({ event, step }) => { // This function will be rate limited // It will only run once per 4 hours for a given event payload with matching company_id } ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "synchronize-data", RateLimit: &inngestgo.ConfigRateLimit{ Limit: 1, Period: 4 * time.Hour, Key: inngestgo.StrPtr("event.data.company_id"), }, }, inngestgo.EventTrigger("intercom/company.updated", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // This function will be rate limited to 1 run per 4 hours for a given event payload with matching company_id return nil, nil }, ) ``` ```py {{ title: "Python" }} @inngest.create_function( id="synchronize-data", rate_limit=inngest.RateLimit( limit=1, period=datetime.timedelta(hours=4), key="event.data.company_id", ), trigger=inngest.Trigger(event="intercom/company.updated") ) async def synchronize_data(ctx: inngest.Context): # This function will be rate limited to 1 run per 4 hours for a given event payload with matching company_id ``` ### Configuration reference * `limit` - The maximum number of functions to run in the given time period. * `period` - The time period of which to set the limit. The period begins when the first matching event is received. * `key` - An optional [expression](/docs/guides/writing-expressions) using event data to apply each limit too. Each unique value of the `key` has its own limit, enabling you to rate limit function runs by any particular key, like a user ID. Any events received in excess of your `limit` are ignored. This means this is not the right approach if you need to process every single event sent to Inngest. Consider using [throttle](/docs/guides/throttling) instead. ## How rate limiting works Each time an event is received that matches your function's trigger, it is evaluated prior to executing your function. If `rateLimit` is configured, Inngest uses the `limit` and `period` options to only execute a maximum number of functions during that period. Inngest's rate limiting implementation uses the [“Generic Cell Rate Algorithm”](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm) (GCRA). To _overly simplify_ how this works, Inngest will use the `limit` and `period` options to create "buckets" of time in which your function can execute _once_. ``` limit / period = bucket time window ``` For example, this means that for a `limit: 10` and `period: '60m'` (60 minutes), the bucket time window will be 6 minutes. Any event triggering the function "fills up" the bucket for that time window and any additional events are ignored until the bucket's time window is reset. The algorithm (GCRA) is more sophisticated than this, but at the basic level - `rateLimit` ensures that you'll only run the max `limit` number of items over the `period` that you specify. Events that are ignored by the function will continue to be stored by Inngest. **How the rate limit is applied with a consistent rate of events received** [IMAGE] **How the rate limit is applied with sporadic events received** [IMAGE] **How the rate limit is applied when limit is set to 1** [IMAGE] ### Using a `key` When a `key` is added, a separate limit is applied for each unique value of the `key` expression. For example, if your `key` is set to `event.data.customer_id`, each customer would have their individual rate limit applied to functions run meaning different users might have the same function run in same bucket time window, but two runs will not happen for the same `event.data.customer_id`. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more information. **Note** - To prevent duplicate events from triggering your function more than once in a 24 hour period, use [the `idempotency` option](/docs/guides/handling-idempotency#at-the-function-level-the-consumer) which is the equivalent to setting `rateLimit` with a `key`, a `limit` of `1` and `period` of `24hr`. ## Limitations * The maximum rate limit `period` is 24 hours. ## Further reference * [Rate limiting vs Throttling](/docs/guides/throttling#throttling-vs-rate-limiting) * [TypeScript SDK Reference](/docs/reference/typescript/v4/functions/rate-limit) * [Python SDK Reference](/docs/reference/python/functions/create#configuration) # Crons (Scheduled Functions) Source: https://www.inngest.com/docs/guides/scheduled-functions Description: Create recurring background jobs using cron expressions in Inngest. Supports timezone-aware schedules and runs on any platform including serverless. metaTitle = "Scheduled Functions & Cron Jobs" You can create scheduled jobs using cron schedules within Inngest natively. Inngest's cron schedules also support timezones, allowing you to schedule work in whatever timezone you need work to run in. You can create scheduled functions that run in any timezone using the SDK's [`createFunction()`](/docs/reference/typescript/v4/functions/create): ```ts new Inngest({ id: "signup-flow" }); // This weekly digest function will run at 12:00pm on Friday in the Paris timezone prepareWeeklyDigest = inngest.createFunction( { id: "prepare-weekly-digest", triggers: { cron: "TZ=Europe/Paris 0 12 * * 5" } }, async ({ step }) => { // Load all the users from your database: await step.run( "load-users", async () => await db.load("SELECT * FROM users") ); // 💡 Since we want to send a weekly digest to each one of these users // it may take a long time to iterate through each user and send an email. // Instead, we'll use this scheduled function to send an event to Inngest // for each user then handle the actual sending of the email in a separate // function triggered by that event. // ✨ This is known as a "fan-out" pattern ✨ // 1️⃣ First, we'll create an event object for every user return in the query: users.map((user) => { return { name: "app/send.weekly.digest", data: { user_id: user.id, email: user.email, }, }; }); // 2️⃣ Now, we'll send all events in a single batch: await step.sendEvent("send-digest-events", events); // This function can now quickly finish and the rest of the logic will // be handled in the function below ⬇️ } ); // This is a regular Inngest function that will send the actual email for // every event that is received (see the above function's inngest.send()) // Since we are "fanning out" with events, these functions can all run in parallel sendWeeklyDigest = inngest.createFunction( { id: "send-weekly-digest-email", triggers: { event: "app/send.weekly.digest" } }, async ({ event }) => { // 3️⃣ We can now grab the email and user id from the event payload event.data; // 4️⃣ Finally, we send the email itself: await email.send("weekly_digest", email, user_id); // 🎇 That's it! - We've used two functions to reliably perform a scheduled // task for a large list of users! } ); ``` You can create scheduled functions that run in any timezone using the SDK's [`CreateFunction()`](https://pkg.go.dev/github.com/inngest/inngestgo#CreateFunction): ```go package main import ( "context" "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) func init() { // This weekly digest function will run at 12:00pm on Friday in the Paris timezone inngestgo.CreateFunction( client, inngestgo.FunctionOpts{Name: "prepare-weekly-digest"}, inngestgo.CronTrigger("TZ=Europe/Paris 0 12 * * 5"), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // Load all the users from your database: users, err := step.Run(ctx, "load-users", func(ctx context.Context) ([]*User, error) { return loadUsers() }) if err != nil { return nil, err } // 💡 Since we want to send a weekly digest to each one of these users // it may take a long time to iterate through each user and send an email. // Instead, we'll use this scheduled function to send an event to Inngest // for each user then handle the actual sending of the email in a separate // function triggered by that event. // ✨ This is known as a "fan-out" pattern ✨ // 1️⃣ First, we'll create an event object for every user return in the query: events := make([]inngestgo.Event, len(users)) for i, user := range users { events[i] = inngestgo.Event{ Name: "app/send.weekly.digest", Data: map[string]interface{}{ "user_id": user.ID, "email": user.Email, }, } } // 2️⃣ Now, we'll send all events in a single batch: err = step.SendMany(ctx, "send-digest-events", events) if err != nil { return nil, err } // This function can now quickly finish and the rest of the logic will // be handled in the function below ⬇️ return nil, nil }, ) // This is a regular Inngest function that will send the actual email for // every event that is received (see the above function's inngest.send()) // Since we are "fanning out" with events, these functions can all run in parallel inngestgo.CreateFunction( client, inngestgo.FunctionOpts{Name: "send-weekly-digest-email"}, inngestgo.EventTrigger("app/send.weekly.digest", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // 3️⃣ We can now grab the email and user id from the event payload email := input.Event.Data["email"].(string) userID := input.Event.Data["user_id"].(string) // 4️⃣ Finally, we send the email itself: err := email.Send("weekly_digest", email, userID) if err != nil { return nil, err } // 🎇 That's it! - We've used two functions to reliably perform a scheduled // task for a large list of users! return nil, nil }, ) } ``` You can create scheduled functions that run in any timezone using the SDK's [`create_function()`](/docs/reference/python/functions/create): ```py from inngest import Inngest inngest_client = Inngest(app_id="signup-flow") # This weekly digest function will run at 12:00pm on Friday in the Paris timezone @inngest_client.create_function( fn_id="prepare-weekly-digest", trigger=inngest.TriggerCron(cron="TZ=Europe/Paris 0 12 * * 5") ) async def prepare_weekly_digest(ctx: inngest.Context) -> None: # Load all the users from your database: users = await ctx.step.run( "load-users", lambda: db.load("SELECT * FROM users") ) # 💡 Since we want to send a weekly digest to each one of these users # it may take a long time to iterate through each user and send an email. # Instead, we'll use this scheduled function to send an event to Inngest # for each user then handle the actual sending of the email in a separate # function triggered by that event. # ✨ This is known as a "fan-out" pattern ✨ # 1️⃣ First, we'll create an event object for every user return in the query: events = [ { "name": "app/send.weekly.digest", "data": { "user_id": user.id, "email": user.email, } } for user in users ] # 2️⃣ Now, we'll send all events in a single batch: await ctx.step.send_event("send-digest-events", events) # This function can now quickly finish and the rest of the logic will # be handled in the function below ⬇️ # This is a regular Inngest function that will send the actual email for # every event that is received (see the above function's inngest.send()) # Since we are "fanning out" with events, these functions can all run in parallel @inngest_client.create_function( fn_id="send-weekly-digest-email", trigger=inngest.TriggerEvent(event="app/send.weekly.digest") ) async def send_weekly_digest(ctx: inngest.Context) -> None: # 3️⃣ We can now grab the email and user id from the event payload email = ctx.event.data["email"] user_id = ctx.event.data["user_id"] # 4️⃣ Finally, we send the email itself: await email.send("weekly_digest", email, user_id) # 🎇 That's it! - We've used two functions to reliably perform a scheduled # task for a large list of users! ``` 👉 Note: You'll need to [serve these functions in your Inngest API](/docs/learn/serving-inngest-functions) for the functions to be available to Inngest. On the free plan, if your function fails 20 times consecutively it will automatically be paused. ⚠️ **Daylight Saving Time (DST) disclaimer:** Schedules near DST transition times can behave unexpectedly in local timezones. Depending on the timezone and the exact schedule, a cron may run zero, one, or two times in a day when clocks change. Inngest's cron behavior follows the underlying cron library and does not apply special DST correction. To reduce risk, avoid transition-hour schedules (such as `2:00 AM` in many US regions, or `12:00 AM` in some other regions), and prefer `TZ=UTC` when you need consistent execution timing. ## Adding jitter By default, cron functions fire at the exact scheduled time. If you have many cron functions on the same schedule, they all fire at once which can create load spikes on your system or on third-party APIs. You can add an optional `jitter` to spread out the execution. When jitter is set, each cron occurrence fires at a random time within the jitter window after the scheduled boundary. Jitter must be between 1 second and 5 minutes. ```ts inngest.createFunction( { id: "hourly-sync", triggers: [{ cron: "0 * * * *", jitter: "5m" }], }, async ({ step }) => { // Fires at a random time within 5 minutes after each hour } ); ``` ```go inngestgo.CreateFunction( client, inngestgo.FunctionOpts{Name: "hourly-sync"}, inngestgo.CronTriggerWithJitter("0 * * * *", 5*time.Minute), func(ctx context.Context, input inngestgo.Input[any]) (any, error) { // Fires at a random time within 5 minutes after each hour return nil, nil }, ) ``` ```python @inngest_client.create_function( fn_id="hourly-sync", trigger=inngest.TriggerCron(cron="0 * * * *", jitter="5m"), ) async def hourly_sync(ctx: inngest.Context) -> None: # Fires at a random time within 5 minutes after each hour pass ``` # Sending events from functions Source: https://www.inngest.com/docs/guides/sending-events-from-functions Description: Use step.sendEvent() inside an Inngest function to trigger other functions or fan-out work in parallel. Events are sent reliably as part of the step lifecycle. metaTitle = "Send Events from Inside a Function" In some workflows or pipeline functions, you may want to broadcast events from within your function to trigger _other_ functions. This pattern is useful when: * You want to decouple logic into separate functions that can be re-used across your system * You want to send an event to [fan-out](/docs/guides/fan-out-jobs) to multiple other functions * Your function is handling many items that you want to process in parallel functions * You want to [delegate tasks to sub-agents asynchronously](/docs/ai-patterns/sub-agent-delegation#delegate-asynchronously-with-stepsendevent) in an AI agent system * You want to [cancel](/docs/guides/cancel-running-functions) another function * You want to send data to another function [waiting for an event](/docs/reference/typescript/v4/functions/step-wait-for-event) If your function needs to handle the result of another function, or wait until that other function has completed, you should use [direct function invocation](/docs/guides/invoking-functions-directly) instead. ## How to send events from functions To send events from within functions, you will use [`step.sendEvent()`](/docs/reference/typescript/v4/functions/step-send-event). This method takes a single event, or an array of events. The example below uses an array of events. This is an example of a [scheduled function](/docs/guides/scheduled-functions) that sends a weekly activity email to all users. First, the function fetches all users, then it maps over all users to create a `"app/weekly-email-activity.send"` event for each user, and finally it sends all events to Inngest. ```ts new Inngest({ id: "signup-flow" }); type Events = GetEvents; loadCron = inngest.createFunction( { id: "weekly-activity-load-users", triggers: { cron: "0 12 * * 5" } }, async ({ event, step }) => { // Fetch all users await step.run("fetch-users", async () => { return fetchUsers(); }); // For each user, send us an event. Inngest supports batches of events // as long as the entire payload is less than 512KB. users.map( (user) => { return { name: "app/weekly-email-activity.send", data: { ...user, }, user, }; } ); // Send all events to Inngest, which triggers any functions listening to // the given event names. await step.sendEvent("fan-out-weekly-emails", events); // Return the number of users triggered. return { count: users.length }; } ); ``` Next, create a function that listens for the `"app/weekly-email-activity.send"` event. This function will be triggered for each user that was sent an event in the previous function. ```ts sendReminder = inngest.createFunction( { id: "weekly-activity-send-email", triggers: { event: "app/weekly-email-activity.send" } }, async ({ event, step }) => { await step.run("load-user-data", async () => { return loadUserData(event.data.user.id); }); await step.run("email-user", async () => { return sendEmail(event.data.user, data); }); } ); ``` Each of these functions will run in parallel and individually retry on error, resulting in a faster, more reliable system. 💡 **Tip**: When triggering lots of functions to run in parallel, you will likely want to configure `concurrency` limits to prevent overloading your system. See our [concurrency guide](/docs/guides/concurrency) for more information. ### Why `step.sendEvent()` vs. `inngest.send()`? By using [`step.sendEvent()`](/docs/reference/typescript/v4/functions/step-send-event) Inngest's SDK can automatically add context and tracing which ties events to the current function run. If you use [`inngest.send()`](/docs/reference/typescript/v4/events/send), the context around the function run is not present. To send events from within functions, you will use [`step.Send()`](https://pkg.go.dev/github.com/inngest/inngestgo/step#Send) or [`step.SendMany()`](https://pkg.go.dev/github.com/inngest/inngestgo/step#SendMany) This is an example of a [scheduled function](/docs/guides/scheduled-functions) that sends a weekly activity email to all users. First, the function fetches all users, then it maps over all users to create a `"app/weekly-email-activity.send"` event for each user, and finally it sends all events to Inngest. ```go !snippet:path=snippets/go/docs/functions/sending_events_from_functions_part1.go ``` Next, create a function that listens for the `"app/weekly-email-activity.send"` event. This function will be triggered for each user that was sent an event in the previous function. ```go !snippet:path=snippets/go/docs/functions/sending_events_from_functions_part2.go ``` Each of these functions will run in parallel and individually retry on error, resulting in a faster, more reliable system. 💡 **Tip**: When triggering lots of functions to run in parallel, you will likely want to configure `concurrency` limits to prevent overloading your system. See our [concurrency guide](/docs/guides/concurrency) for more information. To send events from within functions, you will use [`step.send_event()`](/docs/reference/python/steps/send-event). This method takes a single event, or an array of events. The example below uses an array of events. This is an example of a [scheduled function](/docs/guides/scheduled-functions) that sends a weekly activity email to all users. First, the function fetches all users, then it maps over all users to create a `"app/weekly-email-activity.send"` event for each user, and finally it sends all events to Inngest. ```py import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="weekly-activity-load-users", trigger=inngest.TriggerCron(cron="0 12 * * 5") ) async def load_cron(ctx: inngest.Context): # Fetch all users async def fetch(): return await fetch_users() users = await ctx.step.run("fetch-users", fetch) # For each user, send us an event. Inngest supports batches of events # as long as the entire payload is less than 512KB. events = [] for user in users: events.append( inngest.Event( name="app/weekly-email-activity.send", data={ **user, "user": user } ) ) # Send all events to Inngest, which triggers any functions listening to # the given event names. await ctx.step.send_event("fan-out-weekly-emails", events) # Return the number of users triggered. return {"count": len(users)} ``` Next, create a function that listens for the `"app/weekly-email-activity.send"` event. This function will be triggered for each user that was sent an event in the previous function. ```py @inngest_client.create_function( fn_id="weekly-activity-send-email", trigger=inngest.TriggerEvent(event="app/weekly-email-activity.send") ) async def send_reminder(ctx: inngest.Context): async def load_data(): return await load_user_data(ctx.event.data["user"]["id"]) data = await ctx.step.run("load-user-data", load_data) async def send(): return await send_email(ctx.event.data["user"], data) await ctx.step.run("email-user", send) ``` Each of these functions will run in parallel and individually retry on error, resulting in a faster, more reliable system. 💡 **Tip**: When triggering lots of functions to run in parallel, you will likely want to configure `concurrency` limits to prevent overloading your system. See our [concurrency guide](/docs/guides/concurrency) for more information. ### Why `step.send_event()` vs. `inngest.send()`? By using [`step.send_event()`](/docs/reference/python/steps/send-event) Inngest's SDK can automatically add context and tracing which ties events to the current function run. If you use [`inngest.send()`](/docs/reference/python/client/send), the context around the function run is not present. ## Parallel functions vs. parallel steps Another technique similar is running multiple steps in parallel (read the [step parallelism guide](/docs/guides/step-parallelism)). Here are the key differences: * Both patterns run code in parallel * With parallel steps, you can access the output of each step, whereas with the above example, you cannot * Parallel steps have limit of 1,000 steps, though you can trigger as many functions as you'd like using the send event pattern * Decoupled functions can be tested and [replayed](/docs/platform/replay) separately, whereas parallel steps can only be tested as a whole * You can retry individual functions easily if they permanently fail, whereas if a step permanently fails (after retrying) the function itself will fail and terminate. ## Sending events vs. invoking A related pattern is invoking external functions directly instead of just triggering them with an event. See the [Invoking functions directly](/docs/guides/invoking-functions-directly) guide. Here are some key differences: * Sending events from functions is better suited for parallel processing of independent tasks and invocation is better for coordinated, interdependent functions * Sending events can be done in bulk, whereas invoke can only invoke one function at a time. * Sending events can be combined with [fan-out](/docs/guides/fan-out-jobs) to trigger multiple functions from a single event * Unlike invocation, sending events will not receive the result of the invoked function # Singleton Functions Source: https://www.inngest.com/docs/guides/singleton Description: Prevent concurrent runs of an Inngest function using the singleton configuration. Queue or skip additional invocations while one run is already in progress. metaTitle = "Singleton Functions | Ensure Only One Run at a Time" Singleton Functions enable you to ensure that only a single run of your function (_or a set of specific function runs, based on specific event properties_) is happening at a time.

Singleton Functions are available in the TypeScript SDK starting from version 3.39.0. ## When to use Singleton Functions Singleton Functions are useful when you want to ensure that only a single instance of a function is running at a time, for example: - A third-party data synchronization workflow - A compute- or time-intensive function that should not be run multiple times at the same time (ex: AI processing) ### Singleton compared to concurrency: While [Concurrency](/docs/guides/concurrency) set to `1` ensures that only a single step of a given function is running at a time, Singleton Functions ensure that only a single run of a given function is happening at a time. ### Singleton compared to Rate Limiting: [Rate Limiting](/docs/guides/rate-limiting) is similar to Singleton Functions, but it is designed to limit the number of runs started within a time period, whereas Singleton Functions are designed to ensure that only a single run of a function occurs over a given time window. Rate Limiting is useful for controlling the rate of execution of a function, while Singleton Functions are useful for ensuring that only a single run of a function occurs over a given time window. ## How it works Singleton Functions are configured using the `singleton` property in the function definition. The following `data-sync` function demonstrates singleton behavior scoped to individual users. Depending on the `mode`, new runs will either be skipped or will cancel the existing run: ```ts inngest.createFunction({ id: "data-sync", singleton: { key: "event.data.user_id", mode: "skip", }, triggers: { event: "data-sync.start" }, }, async ({ event }) => { // ... }, ); ``` Refer to the [reference documentation](/docs/reference/typescript/v4/functions/singleton) for more details. ### Using a `key` When a `key` is added, the unique runs rule is applied for each unique value of the `key` expression. For example, if your `key` is set to `event.data.user_id`, each user would have their individual singleton rule applied to functions runs, ensuring that only a single run of the function is happening at a time for each user. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more information. ### Two modes: Skip vs Cancel Singleton Functions can be configured to either skip the new run or cancel the existing run and start a new one. The `mode` property configures the behavior of the Singleton Function: - `"skip"` - Skips the new run if another run is already executing. - `"cancel"` - Cancels the existing run and starts the new one. **Cancel mode behavior**: Triggering multiple function runs with the same key in very rapid succession may result in some runs being skipped rather than cancelled, similar to a debounce effect. This prevents excessive cancellation overhead when events are triggered in quick bursts. #### When should I use "cancel" mode vs "skip" mode? Use `"skip"` mode when you want to prevent duplicate work and preserve the currently running function. Use `"cancel"` mode when you want to ensure the most recent event is always processed, even if it means cancelling an in-progress run. ```ts {{ title: "Skip mode" }} inngest.createFunction({ id: "data-sync", singleton: { key: "event.data.user_id", mode: "skip", }, triggers: { event: "data-sync.start" }, }, async ({ event }) => { event.data.user_id; // This long-running sync process will not be interrupted // If another sync is triggered for this user, it will be skipped await syncUserDataFromExternalAPI(userId); await processLargeDataset(data); await updateDatabase(processed); }, ); ``` ```ts {{ title: "Cancel mode" }} inngest.createFunction({ id: "latest-data-sync", singleton: { key: "event.data.user_id", mode: "cancel", }, triggers: { event: "data-sync.start" }, }, async ({ event }) => { event.data.user_id; // If a newer sync is triggered, this run will be cancelled // ensuring only the most recent data is processed await fetchLatestUserData(userId); await applyRealTimeUpdates(payload); }, ); ``` ## Compatibility with other flow control features Singleton Functions can be combined with other flow control features, with the following considerations: | Flow control | Compatibility | Considerations | | --- | --- | --- | | [Debounce](/docs/guides/debounce) | ✅ | Can be used together without issues. | | [Rate limiting](/docs/guides/rate-limiting) | ✅ | Similar functionality but rate limiting operates over a predefined time window rather than function execution duration. | | [Throttling](/docs/guides/throttling) | ✅ | Similar functionality but throttling enqueues events over time rather than discarding/canceling them. | | [Concurrency](/docs/guides/concurrency) | ❌ | Singleton functions implicitly have a concurrency of 1. A concurrency setting can be set but should be used with caution. | | [Batching](/docs/guides/batching) | ❌ | Singleton isn't compatible with batching; function registration will fail if both are set. | ## FAQ ### How does Singleton Functions work with retries? If a singleton function fails and is retrying, it should still skip new incoming runs. # Step parallelism Source: https://www.inngest.com/docs/guides/step-parallelism Description: Execute multiple Inngest steps in parallel using Promise.all() to reduce total runtime. Steps run concurrently and results are collected when all complete. metaTitle = "Parallel Steps | Run Multiple Steps Concurrently" - If you’re using a serverless platform to host, code will run in true parallelism similar to multi-threading (without shared state) - Each step will be individually retried ### Platform support **Parallelism works across all providers and platforms**. True parallelism is supported for serverless functions; if you’re using a single Express server you’ll be splitting all parallel jobs amongst a single-threaded node server. ## Running steps in parallel You can run steps in parallel via `Promise.all()`: - Create each step via [`step.run()`](/docs/reference/typescript/v4/functions/step-run) without awaiting, which returns an unresolved promise. - Await all steps via `Promise.all()`. This triggers all steps to run in parallel via separate executions. A common use case is to split work into chunks: ```ts new Inngest({ id: "signup-flow" }); fn = inngest.createFunction( { id: "post-payment-flow", triggers: { event: "stripe/charge.created" } }, async ({ event, step }) => { // These steps are not `awaited` and run in parallel when Promise.all // is invoked. step.run("confirmation-email", async () => { await sendEmail(event.data.email); return emailID; }); step.run("update-user", async () => { return db.updateUserWithCharge(event); }); // Run both steps in parallel. Once complete, Promise.all will return all // parallelized state here. // // This ensures that all steps complete as fast as possible, and we still have // access to each step's data once they're complete. await Promise.all([sendEmail, updateUser]); return { emailID, updates }; } ); ``` When each step is finished, Inngest will aggregate each step's state and re-invoke the function with all state available. ### Step parallelism in Python Inngest supports parallel steps regardless of whether you're using asynchronous or synchronous code. For both approaches, you can use `step.parallel`: #### async - with `inngest.Step` and `await ctx.group.parallel()` ```py @client.create_function( fn_id="my-fn", trigger=inngest.TriggerEvent(event="my-event"), ) async def fn(ctx: inngest.Context) -> None: user_id = ctx.event.data["user_id"] (updated_user, sent_email) = await ctx.group.parallel( ( lambda: step.run("update-user", update_user, user_id), lambda: step.run("send-email", send_email, user_id), ) ) ``` #### sync - with `inngest.StepSync` and `group.parallel()` ```py @client.create_function( fn_id="my-fn", trigger=inngest.TriggerEvent(event="my-event"), ) def fn(ctx: inngest.ContextSync) -> None: user_id = ctx.event.data["user_id"] (updated_user, sent_email) = ctx.group.parallel( ( lambda: ctx.step.run("update-user", update_user, user_id), lambda: ctx.step.run("send-email", send_email, user_id), ) ) ``` ## Optimizing parallel step performance Without optimized parallelism, parallel steps require 2 requests per step to your application. If you have many parallel steps (e.g., hundreds), this can lead to: - High number of HTTP requests to your application - Increased ingress bandwidth - Higher CPU usage from request parsing Optimized parallelism reduces this to just 1 request per parallel step, significantly improving performance for functions with many parallel operations. ### TypeScript You can disable optimized parallelism by setting `optimizeParallelism: false` on your client or function. **Important considerations:** - **`Promise.race` behavior**: With optimized parallelism (the default in v4), `Promise.race` waits for all parallel steps to complete before resolving. If you need early resolution behavior, use `group.parallel()`, though note that early resolution [does not cancel the remaining steps](#racing-steps-are-not-cancelled): ```ts await group.parallel(async () => { return Promise.race([ step.run("a", () => "a"), step.run("b", () => "b"), ]); }); ``` - **Sequential steps in parallel groups**: Steps that run sequentially within different parallel branches may not execute in the order you expect. For example: ```ts []; fn = inngest.createFunction( { id: "fn-1", triggers: { event: "event-1" } }, async ({ step }) => { await Promise.all([ (async () => { await step.run("fast.1", async () => { stepOrder.push("fast.1"); }); await step.run("fast.2", async () => { stepOrder.push("fast.2"); }); })(), (async () => { await step.run("slow.1", async () => { await sleep(1000); stepOrder.push("slow.1"); }); await step.run("slow.2", async () => { await sleep(1000); stepOrder.push("slow.2"); }); })(), ]); // With optimizeParallelism: ['fast.1', 'slow.1', 'fast.2', 'slow.2'] // Without optimizeParallelism: ['fast.1', 'fast.2', 'slow.1', 'slow.2'] } ); ``` ### Python: Optimized by default with opt-out Python always uses optimized parallelism by default, as it doesn't have an equivalent to `Promise.race` to worry about. However, you can opt out at the group level if you need sequential steps within parallel groups to run independently. Use the `parallel_mode` parameter to control this behavior: ```py import inngest import asyncio @inngest_client.create_function( fn_id="my-fn", trigger=inngest.TriggerEvent(event="my-event"), ) async def fn(ctx: inngest.Context) -> None: async def fast_group() -> None: await ctx.step.run("a", lambda: asyncio.sleep(1)) await ctx.step.run("b", lambda: asyncio.sleep(1)) async def slow_group() -> None: await ctx.step.run("x", lambda: asyncio.sleep(10)) await ctx.step.run("y", lambda: asyncio.sleep(10)) # Using RACE mode makes steps run in expected order: a, b, x, y await ctx.group.parallel( (fast_group, slow_group), parallel_mode=inngest.ParallelMode.RACE ) ``` **Without specifying `parallel_mode`**, the steps will run in the order: `a`, `x`, `b`, `y` (optimized mode). Everything is still correct, but step `b` doesn't run until step `x` completes. **With `parallel_mode=inngest.ParallelMode.RACE`**, the steps run in the expected order: `a`, `b`, `x`, `y`, but with the performance trade-off of more requests. ### Racing steps are not cancelled Race mode changes *when* Inngest re-invokes your function: as soon as any step in the group settles, instead of waiting for all of them. The losing steps are *not* cancelled and continue to run. This is most visible with [`step.waitForEvent()`](/docs/features/inngest-functions/steps-workflows/wait-for-event). A losing `waitForEvent` remains an active server-side pause, so the run stays in a `Running` state until that wait reaches its own `timeout`, even though your function code proceeded past the race earlier. Because the losing wait's timeout bounds the run's lifetime, use the tightest `waitForEvent` timeout that works for your use case when racing waits. ## Chunking jobs A common use case is to chunk work. For example, when using OpenAI's APIs you might need to chunk a user's input and run the API on many chunks, then aggregate all data: ```ts new Inngest({ id: "signup-flow" }); fn = inngest.createFunction( { id: "summarize-text", triggers: { event: "app/text.summarize" } }, async ({ event, step }) => { splitTextIntoChunks(event.data.text); await Promise.all( chunks.map((chunk, index) => step.run(`summarize-chunk-${index}`, () => summarizeChunk(chunk)) ) ); await step.run("summarize-summaries", () => summarizeSummaries(summaries)); } ); ``` This allows you to run many independent steps, wait until they're all finished, then fetch the results from all steps within a few lines of code. Doing this in a traditional system would require creating many jobs, polling the status of all jobs, and manually combining state. ## Limitations Currently, the total data returned from **all** steps must be under 4MB (eg. a single step can return a max of. 4MB, or 4 steps can return a max of 1MB each). Functions are also limited to a maximum of 1,000 steps. ## Parallelism vs fan-out Another technique similar to parallelism is fan-out ([read the guide here](/docs/guides/fan-out-jobs)): when one function sends events to trigger other functions. Here are the key differences: - Both patterns run jobs in parallel - You can access the output of steps ran in parallel within your function, whereas with fan-out you cannot - Parallelism has a limit of 1,000 steps, though you can create as many functions as you'd like using fan-out - You can replay events via fan-out, eg. to test functions locally - You can retry individual functions easily if they permanently fail, whereas if a step permanently fails (after retrying) the function itself will fail and terminate. - Fan-out splits functionality into different functions, using step functions keeps all related logic in a single, easy to read function # Throttling Source: https://www.inngest.com/docs/guides/throttling Description: Cap Inngest function throughput to a maximum number of runs per time period. Ideal for respecting third-party API rate limits without dropping or losing events. metaTitle = "Throttling | Limit Function Throughput Over Time" Throttling allows you to specify how many function runs can start within a time period. When the limit is reached, new function runs over the throttling limit will be _enqueued for the future_. Throttling is FIFO (first in first out). Some use cases for throttling include: * Evenly distributing function execution over time to reduce spikes. * Working around third-party API rate limits. ## How to configure throttling ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "unique-function-id", throttle: { limit: 1, period: "5s", burst: 2, key: "event.data.user_id", }, triggers: { event: "ai/summary.requested" }, }, async ({ event, step }) => { }, ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "unique-function-id", Throttle: &inngestgo.ConfigThrottle{ Limit: 1, Period: 5 * time.Second, Key: inngestgo.StrPtr("event.data.user_id"), Burst: 2, }, }, inngestgo.EventTrigger("ai/summary.requested", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { // This function will be throttled to 1 run per 5 seconds for a given event payload with matching user_id, // while the one-time burst allows 2 extra runs to start within 5 seconds, after which no more runs are accepted for 5 seconds. return nil, nil }, ) ``` ```py {{ title: "Python" }} @inngest.create_function( id="unique-function-id", throttle=inngest.Throttle( limit=1, period=datetime.timedelta(seconds=5), key="event.data.user_id", burst=2, ), trigger=inngest.Trigger(event="ai/summary.requested") ) async def synchronize_data(ctx: inngest.Context): # This function will be throttled to 1 run per 5 seconds for a given event payload with matching user_id # while the one-time burst allows 2 extra runs to start within 5 seconds, after which no more runs are accepted for 5 seconds. ``` You can configure throttling on each function using the optional `throttle` parameter. The options directly control the generic cell rate algorithm parameters used within the queue. ### Configuration reference - `limit`: The total number of runs allowed to start within the given `period`. - `period`: The period within the limit will be applied. - `burst`: The number of runs allowed to start in the given window in a single burst on top of `limit`. - `key`: An optional expression which returns a throttling key using event data. This applies the throttle independently to each key value, which can [reduce head-of-line blocking in multi-tenant systems](/docs/guides/multi-tenancy?ref=docs-throttling). GCRA breaks down the provided `period` into smaller windows based on the `limit`. Without bursts, GCRA will admit a single request for every window. Inngest may attempt to start multiple pending function runs in a short time window, so to guarantee maximum throughput, we start `limit + burst` function runs in each window, which allows all requests to start within the configured `period`. This is required as background jobs do not arrive at the same rate as the events triggering them. **Configuration information** - Using throttle ensures that within a window of the given `period`, at most `limit + burst` runs may start. - Period must be between `1s` and `7d`, or between 1 second and 7 days. The minimum granularity is one second. - Throttling is currently applied per function. Two functions with the same key have two separate limits. - Every request is evenly weighted and counts as a single unit in the rate limiter. ## How throttling works Throttling uses the [generic cell rate algorithm (GCRA)](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm) to limit function run *starts* directly in the queue. When you send an event or invoke a function that specifies throttling configuration, Inngest checks the function's throttle limit to see if there's capacity: - If there's capacity, the function run starts as usual. - If there is no capacity, the function run will begin when there's capacity in the future. Note that throttling only applies to function run starts. It does not apply to steps within a function. This allows you to regulate how often functions begin work, *without* worrying about how many steps are in a function, or if steps run in parallel. To limit how many steps can execute at once, use [concurrency controls](/docs/guides/concurrency). Throttling is [FIFO (first in first out)](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)), so the first function run to be enqueued will be the first to start when there's capacity. ## Throttling vs Concurrency **Concurrency** limits the *number of executing steps across your function runs*. This allows you to manage the total capacity of your functions. **Throttling** limits the number of *new function runs* being started. It does not limit the number of executing steps. For example, with a throttling limit of 1 per minute, only one run will start in a single minute. However, that run may execute hundreds of steps, as throttling does not limit steps. ## Throttling vs Rate Limiting Rate limiting also specifies how many functions can start within a time period. However, in Inngest rate limiting ignores function runs over the limit and does not enqueue them for future work. Throttling will enqueue runs over the limit for the future. Rate limiting is *lossy* and provides hard limits on function runs, while throttling delays function runs over the limit until there’s capacity, smoothing spikes. ## Tips * Configure [start timeouts](/docs/features/inngest-functions/cancellation/cancel-on-timeouts) to prevent large backlogs with throttling ## Further reference * [TypeScript SDK Reference](/docs/reference/typescript/v4/functions/create#throttle) * [Python SDK Reference](/docs/reference/python/functions/create#configuration) # Build workflows configurable by your users Source: https://www.inngest.com/docs/guides/user-defined-workflows Description: Let your users configure and run their own workflows with Inngest's Workflow Kit. Build a visual workflow engine backed by durable, event-driven execution. metaTitle = "User-Defined Workflows | Build a Workflow Engine for Your Users" Users today are demanding customization and integrations from every product. Your users may want your product to support custom workflows to automate key user actions. Leverage our [Workflow Kit](/docs/reference/workflow-kit) to add powerful user-defined workflows features to your product. Inngest's Workflow Kit ships as a full-stack package ([`@inngest/workflow-kit`](https://npmjs.com/package/@inngest/workflow-kit)), aiming to simplify the development of user-defined workflows on both the front end and back end: ## Use case: adding AI automation to a Next.js CMS application }> This use case is available a open-source Next.js demo on GitHub. Our Next.js CMS application features the following `blog_posts` table: |Column name|Column type|Description| |-----------|-----------|-----------| id| `bigint`| title | `text`| _The title of the blog post_ subtitle | `text`| _The subtitle of the blog post_ status | `text`| _"draft" or "published"_ markdown | `text`| _The content of the blog post as markdown_ created_at | `timestamp`| You will find a ready-to-use database seed [in the repository](https://github.com/inngest/workflow-kit/blob/main/examples/nextjs-blog-cms/supabase/seed.sql). We would like to provide the following AI automation tasks to our users: **Review tasks** - Add a Table of Contents: _a task leveraging OpenAI to insert a Table of Contents in the blog post_ - Perform a grammar review: _a task leveraging OpenAI to perform some grammar fixes_ **Social content tasks** - Generate LinkedIn posts: _a task leveraging OpenAI to generate some Tweets_ - Generate Twitter posts: _a task leveraging OpenAI to generate a LinkedIn post_ Our users will be able to combine those tasks to build their custom workflows. ### 1. Adding the tasks definition to the application After [installing and setup Inngest](/docs/getting-started/nextjs-quick-start?ref=docs-guide-user-defined-workflows) in our Next.js application, we will create the following [Workflow Actions definition](/docs/reference/workflow-kit/actions) file: ```ts {{ title: "lib/inngest/workflowActions.ts" }} actions: PublicEngineAction[] = [ { kind: "add_ToC", name: "Add a Table of Contents", description: "Add an AI-generated ToC", }, { kind: "grammar_review", name: "Perform a grammar review", description: "Use OpenAI for grammar fixes", }, { kind: "wait_for_approval", name: "Apply changes after approval", description: "Request approval for changes", }, { kind: "apply_changes", name: "Apply changes", description: "Save the AI revisions", }, { kind: "generate_linkedin_posts", name: "Generate LinkedIn posts", description: "Generate LinkedIn posts", }, { kind: "generate_tweet_posts", name: "Generate Twitter posts", description: "Generate Twitter posts", }, ]; ``` Explore how Workflow actions get declared as `PublicEngineAction` and `EngineAction`. ### 2. Updating our database schema To enable our users to configure the workflows, we will create the following `workflows` table. The `workflows` tables stores the [Workflow instance object](/docs/reference/workflow-kit/workflow-instance) containing how the user ordered the different selected [Workflow actions](/docs/reference/workflow-kit/actions). Other columns are added to store extra properties specific to our application such as: the automation name and description, the event triggering the automation and its status (`enabled`). |Colunm name|Column type|Description| |-----------|-----------|-----------| id| `bigint`| name | `text`| _The name of the automation_ description | `text`| _A short description of the automation_ workflow | `jsonb`| _A [Workflow instance object](/docs/reference/workflow-kit/workflow-instance)_ enabled | `boolean`| trigger | `text`| _The name of the [Inngest Event](/docs/features/events-triggers) triggering the workflow_ created_at | `timestamp`| Once the `workflows` table created, we will add two [workflow instances](/docs/reference/workflow-kit/workflow-instance) records: - _"When a blog post is published"_: Getting a review from AI - _"When a blog post is moved to review"_: Actions performed to optimize the distribution of blog posts using the following SQL insert statement: ```sql INSERT INTO "public"."workflows" ("id", "created_at", "workflow", "enabled", "trigger", "description", "name") VALUES (2, '2024-09-14 20:19:41.892865+00', NULL, true, 'blog-post.published', 'Actions performed to optimize the distribution of blog posts', 'When a blog post is published'), (1, '2024-09-14 15:46:53.822922+00', NULL, true, 'blog-post.updated', 'Getting a review from AI', 'When a blog post is moved to review'); ``` You will find a ready-to-use database seed [in the repository](https://github.com/inngest/workflow-kit/blob/main/examples/nextjs-blog-cms/supabase/seed.sql). ### 3. Adding the Workflow Editor page With our workflow actions definition and `workflows` table ready, we will create a new Next.js Page featuring the Workflow Editor. First, we will add a new [Next.js Page](https://nextjs.org/docs/app/building-your-application/routing/pages) to load the worklow and render the Editor: ```tsx {{ title: "app/automation/[id]/page.tsx" }} runtime = "edge"; export default async function Automation({ params, }: { params: { id: string }; }) { createClient(); await supabase .from("workflows") .select("*") .eq("id", params.id!) .single(); if (workflow) { return ; } else { notFound(); } } ``` The `` component is then rendered with the following required properties: - `workflow={}`: [workflow instance](/docs/reference/workflow-kit/workflow-instance) loaded from the database along side - `event={}`: the name of the event triggering the workflow - `availableActions={}`: [actions](/docs/reference/workflow-kit/actions#passing-actions-to-the-react-components-public-engine-action) that the user can select to build its automation ```tsx {{ title: "src/components/automation-editor.ts" }} import "@inngest/workflow-kit/ui/ui.css"; import "@xyflow/react/dist/style.css"; AutomationEditor = ({ workflow }: { workflow: Workflow }) => { useState(workflow); return ( { updateWorkflowDraft({ ...workflowDraft, workflow: updated, }); }} > ); }; ``` [``](/docs/reference/workflow-kit/components-api) is a [Controlled Component](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components), relying on the `workflow={}` object to update its UI. Every change performed by the user will trigger the `onChange={}` callback to be called. This callback should update the object passed to the `workflow={}` prop and can be used to also implement an auto save mechanism. The complete version of the `` is [available on GitHub](https://github.com/inngest/workflow-kit/blob/main/examples/nextjs-blog-cms/components/automation-editor.tsx). Navigating to `/automation/1` renders tht following Workflow Editor UI using our workflow actions: ![workflow-kit-announcement-video-loop.gif](/assets/docs/reference/workflow-kit/workflow-demo.gif) ### 4. Implementing the Workflow Actions handlers Let's now implement the logic our automation tasks by creating a new file in `lib/inngest` and starting with the "Add a Table of Contents" workflow action: ```tsx {{ title: "lib/inngest/workflowActionHandlers.ts" }} actions: EngineAction[] = [ { // Add a Table of Contents ...actionsDefinition[0], handler: async ({ event, step, workflowAction }) => { createClient(); await step.run("load-blog-post", async () => loadBlogPost(event.data.id) ); await step.run("add-toc-to-article", async () => { new OpenAI({ apiKey: process.env["OPENAI_API_KEY"], // This is the default and can be omitted }); ` Please update the below markdown article by adding a Table of Content under the h1 title. Return only the complete updated article in markdown without the wrapping "\`\`\`". Here is the text wrapped with "\`\`\`": \`\`\` ${getAIworkingCopy(workflowAction, blogPost)} \`\`\` `; await openai.chat.completions.create({ model: process.env["OPENAI_MODEL"] || "gpt-3.5-turbo", messages: [ { role: "system", content: "You are an AI that make text editing changes.", }, { role: "user", content: prompt, }, ], }); return response.choices[0]?.message?.content || ""; }); await step.run("save-ai-revision", async () => { await supabase .from("blog_posts") .update({ markdown_ai_revision: aiRevision, status: "under review", }) .eq("id", event.data.id) .select("*"); }); }, } }, ]; ``` This new file adds the `handler` property to the existing _"Add a Table of Contents"_ action. A [workflow action `handler()`](/docs/reference/workflow-kit/actions#handler-function-argument-properties) has a similar signature to Inngest's function handlers, receiving two key arguments: `event` and [`step`](/docs/reference/typescript/v4/functions/create#step). Our _"Add a Table of Contents"_ leverages Inngest's [step API](/docs/reference/typescript/v4/functions/step-run) to create reliable and retriable steps generating and inserting a Table of Contents. The complete implementation of all workflow actions are [available on GitHub](https://github.com/inngest/workflow-kit/blob/main/examples/nextjs-blog-cms/lib/inngest/workflowActionHandlers.ts). ### 5. Creating an Inngest Function With all the workflow action handlers of our automation tasks [implemented](https://github.com/inngest/workflow-kit/blob/main/examples/nextjs-blog-cms/lib/inngest/workflowActionHandlers.ts), we can create a [`Engine`](/docs/reference/workflow-kit/engine) instance and pass it to a dedicated [Inngest Function](/docs/learn/inngest-functions) that will run the automation when the `"blog-post.updated"` and `"blog-post.published"` events will be triggered: ```tsx {{ title: "lib/inngest/workflow.ts" }} new Engine({ actions: actionsWithHandlers, loader: loadWorkflow, }); export default inngest.createFunction( { id: "blog-post-workflow", // Triggers // - When a blog post is set to "review" // - When a blog post is published triggers: [{ event: "blog-post.updated" }, { event: "blog-post.published" }], }, async ({ event, step }) => { // When `run` is called, the loader function is called with access to the event await workflowEngine.run({ event, step }); } ); ``` ### Going further This guide demonstrated how quickly and easily user-defined workflows can be added to your product when using our [Workflow Kit](/docs/reference/workflow-kit). }> This use case is available a open-source Next.js demo on GitHub. # Working with Loops in Inngest Source: https://www.inngest.com/docs/guides/working-with-loops Description: Safely implement loops inside Inngest functions. Avoid common step memoization pitfalls and learn the correct patterns for iterating over dynamic data. metaTitle = "Working with Loops in Inngest Functions | Pitfalls & Patterns" In Inngest each step in your function is executed as a separate HTTP request. This means that for every step in your function, the function is re-entered, starting from the beginning, up to the point where the next step is executed. This [execution model](/docs/learn/how-functions-are-executed) helps in managing retries, timeouts, and ensures robustness in distributed systems. This page covers how to implement loops in your Inngest functions and avoid common pitfalls. ## Simple function example Let's start with a simple example to illustrate the concept: ```javascript inngest.createFunction( { id: "simple-function", triggers: { event: "test/simple.function" } }, async ({ step }) => { console.log("hello"); await step.run("a", async () => { console.log("a") }); await step.run("b", async () => { console.log("b") }); await step.run("c", async () => { console.log("c") }); } ); ``` In the above example, you will see "hello" printed four times, once for the initial function entry and once for each step execution (`a`, `b`, and `c`). ```bash {{ title: "✅ How Inngest executes the code" }} # This is how Inngest executes the code above: "hello" "hello" "a" "hello" "b" "hello" "c" ``` ```bash {{ title: "❌ Common incorrect misconception" }} # This is a common assumption of how Inngest executes the code above. # It is not correct. "hello" "a" "b" "c" ``` Any non-deterministic logic (like database calls or API calls) must be placed inside a `step.run` call to ensure it is executed correctly within each step. With this in mind, here is how the previous example can be fixed: ```ts inngest.createFunction( { id: "simple-function", triggers: { event: "test/simple.function" } }, async ({ step }) => { await step.run("hello", () => { console.log("hello") }); await step.run("a", async () => { console.log("a") }); await step.run("b", async () => { console.log("b") }); await step.run("c", async () => { console.log("c") }); } ); // hello // a // b // c ``` Now, "hello" is printed only once, as expected. Let's start with a simple example to illustrate the concept: ```go import ( "fmt" "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ID: "simple-function"}, inngestgo.EventTrigger("test/simple.function", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { fmt.Println("hello") _, err := step.Run(ctx, "a", func(ctx context.Context) (any, error) { fmt.Println("a") return nil, nil }) if err != nil { return nil, err } _, err = step.Run(ctx, "b", func(ctx context.Context) (any, error) { fmt.Println("b") return nil, nil }) if err != nil { return nil, err } _, err = step.Run(ctx, "c", func(ctx context.Context) (any, error) { fmt.Println("c") return nil, nil }) if err != nil { return nil, err } return nil, nil }, ) ``` In the above example, you will see "hello" printed four times, once for the initial function entry and once for each step execution (`a`, `b`, and `c`). ```bash {{ title: "✅ How Inngest executes the code" }} # This is how Inngest executes the code above: "hello" "hello" "a" "hello" "b" "hello" "c" ``` ```bash {{ title: "❌ Common incorrect misconception" }} # This is a common assumption of how Inngest executes the code above. # It is not correct. "hello" "a" "b" "c" ``` Any non-deterministic logic (like database calls or API calls) must be placed inside a `step.run` call to ensure it is executed correctly within each step. With this in mind, here is how the previous example can be fixed: ```go import ( "fmt" "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ID: "simple-function"}, inngestgo.EventTrigger("test/simple.function", nil), func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) { if _, err := step.Run(ctx, "hello", func(ctx context.Context) (any, error) { fmt.Println("hello") return nil, nil }); err != nil { return nil, err } if _, err := step.Run(ctx, "a", func(ctx context.Context) (any, error) { fmt.Println("a") return nil, nil }); err != nil { return nil, err } if _, err := step.Run(ctx, "b", func(ctx context.Context) (any, error) { fmt.Println("b") return nil, nil }); err != nil { return nil, err } if _, err := step.Run(ctx, "c", func(ctx context.Context) (any, error) { fmt.Println("c") return nil, nil }); err != nil { return nil, err } return nil, nil }, ) // hello // a // b // c ``` Now, "hello" is printed only once, as expected. Let's start with a simple example to illustrate the concept: ```python @inngest_client.create_function( fn_id="simple-function", trigger=inngest.TriggerEvent(event="test/simple.function") ) async def simple_function(ctx: inngest.Context): print("hello") async def step_a(): print("a") await ctx.step.run("a", step_a) async def step_b(): print("b") await ctx.step.run("b", step_b) async def step_c(): print("c") await ctx.step.run("c", step_c) ``` In the above example, you will see "hello" printed four times, once for the initial function entry and once for each step execution (`a`, `b`, and `c`). ```bash {{ title: "✅ How Inngest executes the code" }} # This is how Inngest executes the code above: "hello" "hello" "a" "hello" "b" "hello" "c" ``` ```bash {{ title: "❌ Common incorrect misconception" }} # This is a common assumption of how Inngest executes the code above. # It is not correct. "hello" "a" "b" "c" ``` Any non-deterministic logic (like database calls or API calls) must be placed inside a `step.run` call to ensure it is executed correctly within each step. With this in mind, here is how the previous example can be fixed: ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( id="simple-function", trigger=inngest.TriggerEvent(event="test/simple.function") ) async def simple_function(ctx: inngest.Context): await ctx.step.run("hello", lambda: print("hello")) await ctx.step.run("a", lambda: print("a")) await ctx.step.run("b", lambda: print("b")) await ctx.step.run("c", lambda: print("c")) # hello # a # b # c ``` Now, "hello" is printed only once, as expected. ## Loop example Here's [an example](/blog/import-ecommerce-api-data-in-seconds) of an Inngest function that imports all products from a Shopify store into a local system. This function iterates over all pages combining all products into a single array. ```typescript export default inngest.createFunction( { id: "shopify-product-import", triggers: { event: "shopify/import.requested" } }, async ({ event, step }) => { [] let cursor = null let hasMore = true // Use the event's "data" to pass key info like IDs // Note: in this example is deterministic across multiple requests // If the returned results must stay in the same order, wrap the db call in step.run() await database.getShopifySession(event.data.storeId) while (hasMore) { await step.run(`fetch-products-${pageNumber}`, async () => { return await shopify.rest.Product.all({ session, since_id: cursor, }) }) // Combine all of the data into a single list allProducts.push(...page.products) if (page.products.length === 50) { cursor = page.products[49].id } else { hasMore = false } } // Now we have the entire list of products within allProducts! } ) ``` In the example above, each iteration of the loop is managed using `step.run()`, ensuring that **all non-deterministic logic (like fetching products from Shopify) is encapsulated within a step**. This approach guarantees that if the request fails, it will be retried automatically, in the correct order. This structure aligns with Inngest's execution model, where each step is a separate HTTP request, ensuring robust and consistent loop behavior. Note that in the example above `getShopifySession` is deterministic across multiple requests (and it's added to all API calls for authorization). If the returned results must stay in the same order, wrap the database call in `step.run()`. Read more about this use case in the [blog post](/blog/import-ecommerce-api-data-in-seconds). Here's an example of an Inngest function that imports all products from a Shopify store into a local system. This function iterates over all pages combining all products into a single array. ```go !snippet:path=snippets/go/v0_11/examples/working-with-loops.go ``` In the example above, each iteration of the loop is managed using `step.Run()`, ensuring that **all non-deterministic logic (like fetching products from Shopify) is encapsulated within a step**. This approach guarantees that if the request fails, it will be retried automatically, in the correct order. This structure aligns with Inngest's execution model, where each step is a separate HTTP request, ensuring robust and consistent loop behavior. Note that in the example above `getShopifySession` is deterministic across multiple requests (and it's added to all API calls for authorization). If the returned results must stay in the same order, wrap the database call in `step.Run()`. Read more about this use case in the [blog post](/blog/import-ecommerce-api-data-in-seconds). Here's an example of an Inngest function that imports all products from a Shopify store into a local system. This function iterates over all pages combining all products into a single array. ```python @inngest.create_function( id="shopify-product-import", trigger=inngest.TriggerEvent(event="shopify/import.requested") ) async def shopify_product_import(ctx: inngest.Context): all_products = [] cursor = None has_more = True # Use the event's "data" to pass key info like IDs # Note: in this example is deterministic across multiple requests # If the returned results must stay in the same order, wrap the db call in step.run() session = await database.get_shopify_session(ctx.event.data["store_id"]) while has_more: page = await ctx.step.run(f"fetch-products-{cursor}", lambda: shopify.Product.all( session=session, since_id=cursor )) # Combine all of the data into a single list all_products.extend(page.products) if len(page.products) == 50: cursor = page.products[49].id else: has_more = False # Now we have the entire list of products within all_products! ``` In the example above, each iteration of the loop is managed using `step.run()`, ensuring that **all non-deterministic logic (like fetching products from Shopify) is encapsulated within a step**. This approach guarantees that if the request fails, it will be retried automatically, in the correct order. This structure aligns with Inngest's execution model, where each step is a separate HTTP request, ensuring robust and consistent loop behavior. Note that in the example above `get_shopify_session` is deterministic across multiple requests (and it's added to all API calls for authorization). If the returned results must stay in the same order, wrap the database call in `step.run()`. Read more about this use case in the [blog post](/blog/import-ecommerce-api-data-in-seconds). ## Best practices: implementing loops in Inngest To ensure your loops run correctly within [Inngest's execution model](/docs/learn/how-functions-are-executed): ### 1. Treat each loop iterations as a single step In a typical programming environment, loops maintain their state across iterations. In Inngest, each step re-executes the function from the beginning to ensure that only the failed steps will be re-tried. To handle this, treat each loop iteration as a separate step. This way, the loop progresses correctly, and each iteration builds on the previous one. ### 2. Place non-deterministic logic inside steps Place non-deterministic logic (like API calls, database queries, or random number generation) inside `step.run` calls. This ensures that such operations are executed correctly and consistently within each step, preventing repeated execution with each function re-entry. ### 3. Use sleep effectively When using `step.sleep` inside a loop, ensure it is combined with structuring the loop to handle each iteration as a separate step. This prevents the function from appearing to restart and allows for controlled timing between iterations. ## Next steps - Docs explanation: [Inngest execution model](/docs/learn/how-functions-are-executed). - Docs guide: [multi-step functions](/docs/learn/inngest-steps). - Blog post: ["How to import 1000s of items from any E-commerce API in seconds with serverless functions"](/blog/import-ecommerce-api-data-in-seconds). # Writing expressions Source: https://www.inngest.com/docs/guides/writing-expressions Description: Write CEL expressions to filter which events trigger your Inngest functions. Match on event data fields, user properties, or custom attributes with precision. metaTitle = "Writing Trigger Expressions (CEL)" Expressions are used in a number of ways for configuring your functions. They are used for: * Defining keys based on event properties for [concurrency](/docs/functions/concurrency), [rate limiting](/docs/reference/typescript/v4/functions/rate-limit), [debounce](/docs/reference/typescript/v4/functions/debounce), or [idempotency](/docs/guides/handling-idempotency) * Conditionally matching events for [wait for event](/docs/reference/typescript/v4/functions/step-wait-for-event), [cancellation](/docs/guides/cancel-running-functions), or the [function trigger's `if` option](/docs/reference/typescript/v4/functions/create#trigger) * Returning values for function [run priority](/docs/reference/typescript/v4/functions/run-priority) All expressions are defined using the [Common Expression Language (CEL)](https://github.com/google/cel-go). CEL offers simple, fast, non-turing complete expressions. It allows Inngest to evaluate millions of expressions for all users at scale. ## Types of Expressions Within the scope of Inngest, expressions should evaluate to either a boolean or a value: * **Booleans** - Any expression used for conditional matching should return a boolean value. These are used in wait for event, cancellation, and the function trigger's `if` option. * **Values** - Other expressions can return any value which might be used as keys (for example, concurrency, rate limit, debounce or [idempotency keys](/docs/guides/handling-idempotency)) or a dynamic value (for example, run priority). ## Variables - `event` refers to the event that triggered the function run, in every case. - `async` refers to a new event in `step.waitForEvent` and [cancellation](/docs/guides/cancel-running-functions). It's the incoming event which is matched asynchronously. This is only present when matching new events in a function run. ## Examples Most expressions are given the `event` payload object as the input. Expressions that match additional events (for example, wait for event, cancellation) will also have the `async` object for the matched event payload. To learn more, consult this [reference of all the operators available in CEL](https://github.com/google/cel-spec/blob/master/doc/langdef.md#list-of-standard-definitions). ### Boolean Expressions ```js // Match a field to a string "event.data.billingPlan == 'enterprise'" // Number comparison "event.data.amount > 1000" // Combining multiple conditions "event.data.billingPlan == 'enterprise' && event.data.amount > 1000" "event.data.billingPlan != 'pro' || event.data.amount < 300" // Compare the function trigger with an inbound event (for wait for event or cancellation) "event.data.userId == async.data.userId" // Alternatively, you can use JavaScript string interpolation for wait for event `${userId} == async.data.userId` // => "user_1234 == async.data.userId" ``` {/* Omit macros until we review support individually ```js // Advanced CEL methods (see reference linked above): // Check if a string contains a substring "event.data.email.contains('gmail.com')" // Check that a field is set "has(event.data.email)" // Compare timestamps "timestamp(event.data.created) > timestamp('2024-01-01T00:00:00Z')" "timestamp(event.data.createdAt) + duration('5m') > timestamp(event.data.expireAt)" ``` */} ### Value Expressions #### Keys ```js // Use the user's id as a concurrency key "event.data.id" // => "1234" // Concatenate two strings together to create a unique key `event.data.userId + "-" + event.type` // => "user_1234-signup" ``` {/* Omit macros until we review support individually ```js // Advanced CEL methods (see reference linked above): // Convert a number to a string for concatenation `string(event.data.amount) + "-" event.data.planId` ``` */} #### Dynamic Values ```js // Return a 0 priority if the billing plan is enterprise, otherwise return 1800 `event.data.billingPlan == 'enterprise' ? 0 : 1800` // Return a value based on multiple conditions `event.data.billingPlan == 'enterprise' && event.data.requestNumber < 10 ? 0 : 1800` ``` {/* Omit macros until we review support individually ```js // Advanced CEL methods (see reference linked above): // Return a priority if the value is set in the payload `has(event.data.priority) ? event.data.priority : 0` ``` */} ## Tips * Use `+` to concatenate strings * Use `==` for equality checks * You can use single `'` or double quotes `"` for strings, but we recommend sticking with one for code consistency * When working with the TypeScript SDK, write expressions within backticks `` ` `` to use quotes in your expression or use JavaScript's string interpolation. * Use ternary operators to return default values * When using the or operator (`||`), CEL will always return a boolean. This is different from JavaScript, where the or operator returns the value of the statement left of the operator if truthy. Use the ternary operator (`?`) instead of `||` for conditional returns. Please note that while CEL supports a wide range of helpers and macros, Inngest only supports a subset of these to ensure a high level of performance and reliability. {/* TODO - Omit these advanced macros for now until we review support individually ### CEL Helpers & Macros This is a non-exhaustive list of CEL [helpers](https://github.com/google/cel-spec/blob/master/doc/langdef.md#list-of-standard-definitions) and [macros](https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros) that are useful for writing expressions: ```js // Check if a field is set "has(event.data.email)" // Convert a number to a string "string(event.data.count)" // Convert a string to an int "int(event.data.amount)" // Get the first item in an array "event.data.items[0]" // Check if an item exists in an array "event.data.items.exists(e, e == 'shirt_1234')" // Check if only one item matches a condition "event.data.items.exists_one(e, e.starsWith('shirt_'))" // Check if all items in an array match a condition "event.data.amounts.all(n, n > 10)" // Convert a timestamp to an int (unix timestamp) "int(timestamp(event.data.createdAt))" // Add a duration to a timestamp "timestamp(event.data.createdAt) + duration('5m')" ``` */} ## Testing out expressions You can test out expressions on [Undistro's CEL Playground](https://playcel.undistro.io/). It's a great way to quickly test out more complex expressions, especially with conditional returns. --- Note: Feature deep-dives (middleware, error handling, cancellation, realtime, AI orchestration) are available individually at https://www.inngest.com/docs/features or via https://www.inngest.com/docs-markdown/features/. --- Note: Platform and observability docs (traces, metrics, alerts, replay) are available at https://www.inngest.com/docs/platform or via https://www.inngest.com/docs-markdown/platform/. # Build an Agent Tool Loop Source: https://www.inngest.com/docs/ai-patterns/agent-tool-loops Description: Build a fault-tolerant AI agent loop where every LLM call and tool execution is a checkpointed, retriable step. Survive failures without losing progress. metaTitle = "Build a Durable AI Agent Tool Loop" This guide walks you through building a ReAct-style agent loop (Reason → Act → Observe → Repeat) where each iteration is a durable, retriable step. ## The basic loop Create a function that takes a user message, calls an LLM, executes any requested tools, and repeats until the LLM returns a final answer: ```ts new Anthropic(); agent = inngest.createFunction( { id: "agent-loop", triggers: [{ event: "agent/message.received" }] }, async ({ event, step }) => { [ // Prepare your initial messages array w/ system, user prompts { role: "user", content: event.data.message }, ]; 10; let iterations = 0; let done = false; while (!done && iterations < MAX_ITERATIONS) { iterations++; // 1. Think — ask the LLM what to do next await step.run(`think`, async () => { return await anthropic.messages.create({ model: "claude-opus-4-6", max_tokens: 4096, // Use your own expertly crafted prompt: system: "You are a helpful assistant with access to tools.", messages, tools, // your tool definitions }); }); // 2. Check if the LLM wants to use tools llmResult.content.filter( (block): block is Anthropic.ToolUseBlock => block.type === "tool_use" ); if (toolCalls.length === 0) { // No tools — we're done llmResult.content.find( (b): b is Anthropic.TextBlock => b.type === "text" ); done = true; return { response: text?.text ?? "", iterations }; } // 3. Act — execute each tool messages.push({ role: "assistant", content: llmResult.content }); []; for (const toolCall of toolCalls) { await step.run( `tool-${toolCall.name}`, async () => executeTool(toolCall.name, toolCall.input) ); toolResults.push({ type: "tool_result", tool_use_id: toolCall.id, content: result, }); } // 4. Observe — feed results back and loop messages.push({ role: "user", content: toolResults }); } return { response: "Reached iteration limit", iterations }; } ); ``` Implement `executeTool` however you want. It can be a switch statement, a map of functions, a plugin system. It takes a tool name and input, and returns a string result. That's the entire pattern. Everything below breaks down each piece and how to tune it. ## Make LLM calls durable Wrap every LLM call in [`step.run()`](/docs/reference/typescript/v4/functions/step-run) to make it retriable and checkpointed: ```ts // ✅ Durable — retries on failure, result is checkpointed await step.run("think", async () => { return await anthropic.messages.create({ /* ... */ }); }); // ❌ Not durable — if this fails, you lose all prior work await anthropic.messages.create({ /* ... */ }); ``` When an LLM call fails (rate limit, timeout, network error), Inngest retries just that step. Previous iterations, tool results — everything else is preserved. ## Run each tool as a step Wrap each tool execution in its own `step.run()` so tools retry independently from the LLM call and from each other. Using `step.run()` provides: - **Independent retries** — a failing tool doesn't re-run the LLM call - **Granular observability** — see exactly which tool failed in the dashboard - **Parallel execution** — tools that don't depend on each other can run concurrently Sequential execution (most common): ```ts for (const toolCall of toolCalls) { await step.run( `tool-${toolCall.name}`, async () => executeTool(toolCall.name, toolCall.input) ); toolResults.push({ type: "tool_result", tool_use_id: toolCall.id, content: result }); } ``` To run independent tools in parallel, use `Promise.all`. Include the index to make each step ID unique and easier to debug parallel calls: ```ts await Promise.all( toolCalls.map((toolCall, idx) => step.run(`tool-${toolCall.name}-${idx}`, async () => ({ toolUseId: toolCall.id, result: executeTool(toolCall.name, toolCall.input), })) ) ); ``` ## Control the loop ### Set a max iteration count Always cap iterations. Choose the limit based on task complexity — simple Q&A might need 2–3, a coding agent that reads, edits, and tests might need 15–20. ### Track token usage To stop before exceeding your budget, accumulate usage across iterations: ```ts let totalInputTokens = 0; while (!done && iterations < MAX_ITERATIONS) { await step.run(`think`, async () => { return await anthropic.messages.create({ /* ... */ }); }); totalInputTokens += llmResult.usage.input_tokens; if (totalInputTokens > 500_000) { return { response: "Token budget exceeded", iterations }; } // ... rest of loop } ``` ### Detect stuck loops If the agent keeps calling the same tool repeatedly, break the loop: ```ts if (iterations > 3 && lastThreeTools.every(t => t === prevTool)) { done = true; } ``` ## Prune context as the loop grows As your agent loops, the message array grows with each iteration. To avoid hitting context window limits, keep the original user message and recent messages, and drop the middle: ```ts function pruneMessages(messages, maxMessages) { if (messages.length <= maxMessages) return messages; messages[0]; messages.slice(-maxMessages + 1); return [first, ...recent]; } ``` {/*For LLM-powered conversation compaction and other advanced techniques, see the [Context Management guide](/docs/ai-patterns/context-management).*/} ## Next steps - [Delegate subtasks to child agents](/docs/ai-patterns/sub-agent-delegation) with `step.invoke()` - [Pause the loop for user approval](/docs/ai-patterns/human-in-the-loop) with `step.waitForEvent()` - [How to build a durable AI agent with Inngest](/blog/ai-agents-inngest-durable-steps) — a deep dive covering context loading, session management, and observability # Use the CLI with coding agents Source: https://www.inngest.com/docs/ai-patterns/cli-for-coding-agents Description: Use the Inngest CLI with AI coding agents to test, trigger, and debug durable functions from the terminal during local development. metaTitle = "Inngest CLI for Coding Agents" Coding agents like Claude Code, Cursor, and Codex can use the Inngest CLI to pull real execution data when debugging or iterating on your functions. Instead of guessing what happened, the agent reads the actual trace. ## Prerequisites - Inngest CLI: `npx inngest-cli@latest` - An environment-scoped API key set as `INNGEST_API_KEY` - A coding agent that can run shell commands --- ## 1. Give the agent your API key Set the API key in your environment so the agent's shell commands can access it: ```bash export INNGEST_API_KEY=sk-inn-api-... ``` For Claude Code, add it to your project's `.env` or pass it in your shell profile. The agent inherits the environment. --- ## 2. Add Inngest context to your agent Tell the agent how to use the CLI. Add this to your project's `CLAUDE.md`, `.cursorrules`, or equivalent: ```markdown ## Inngest debugging Use the Inngest CLI to inspect function runs and traces: - Get a run: `npx inngest-cli@latest api --prod get-function-run ` - Get a trace: `npx inngest-cli@latest api --prod get-function-trace --include-output` - Get runs from an event: `npx inngest-cli@latest api --prod get-event-runs --include-output --limit 5` - Invoke locally: `npx inngest-cli@latest api invoke-function --data '{...}'` Output is JSON. Use jq to filter. ``` The agent now knows to reach for the CLI when it needs execution context. --- ## 3. Debug a failing function When a function fails, point the agent at the run ID: > "The run 01ABC123 is failing in production. Use the Inngest CLI to get the trace and figure out which step is broken." The agent runs: ```bash npx inngest-cli@latest api --prod get-function-trace 01ABC123 --include-output ``` It reads the trace, identifies the failing step, sees the error output, and proposes a fix with real context instead of guessing. --- ## 4. Test the fix After the agent makes a code change, it can invoke the function locally against the dev server: ```bash npx inngest-cli@latest api invoke-function my-app process-order \ --data '{"orderId": "test-123"}' ``` Then check the result: ```bash npx inngest-cli@latest api get-event-runs --include-output ``` The agent verifies its own fix without leaving the terminal. --- ## Why this matters Without the CLI, an agent debugging an Inngest function has to guess what happened based on code alone. With the CLI, it reads the actual execution trace: which steps ran, which failed, what the error was, and what the output looked like. The feedback loop gets tighter. This also means the agent can pull production data to reproduce issues locally, compare expected vs actual output, and verify fixes before pushing. --- ## Next steps - [Inngest CLI reference](/docs/cli) for all commands and options - [Debug a function run from your terminal](/docs/guides/debug-with-cli) for the manual workflow - [REST API documentation](https://api-docs.inngest.com) for direct HTTP access # Human-in-the-loop (HITL) Source: https://www.inngest.com/docs/ai-patterns/human-in-the-loop Description: Pause AI workflows for human review using step.waitForEvent(). Build auditable pipelines that resume automatically when a decision or approval is received. metaTitle = "Human-in-the-Loop AI Workflows" Use [`step.waitForEvent()`](/docs/features/inngest-functions/steps-workflows/wait-for-event) to pause agent execution for human approval, then resume or abort based on the response. ## The basic pattern Create a function that proposes an action, notifies a human, waits for a response, and resumes or aborts: ```typescript emailApprovalWorkflow = inngest.createFunction( { id: "email-approval-workflow", triggers: [{ event: "agent/email.draft-requested" }] }, async ({ event, step }) => { event.data; // Step 1: Agent drafts the email await step.run("draft-email", async () => { return await generateEmail({ recipient, context, tone: "professional", }); }); // Step 2: Notify the human via Slack await step.run("request-approval", async () => { await sendSlackMessage({ channel: "#agent-approvals", blocks: [ { type: "section", text: { type: "mrkdwn", text: `*Agent wants to send an email*\n\n*To:* ${recipient}\n*Subject:* ${draft.subject}\n\n${draft.body}`, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "✅ Approve" }, action_id: "approve_email", value: JSON.stringify({ approvalId: event.data.approvalId, approved: true, }), style: "primary", }, { type: "button", text: { type: "plain_text", text: "❌ Reject" }, action_id: "reject_email", value: JSON.stringify({ approvalId: event.data.approvalId, approved: false, }), style: "danger", }, ], }, ], }); }); // Step 3: Wait for human response — no compute cost while waiting await step.waitForEvent("wait-for-approval", { event: "agent/approval.response", match: "data.approvalId", timeout: "24h", }); // Step 4: Handle the response // No event means it timed out if (!approval) { await step.run("notify-timeout", async () => { await sendSlackMessage({ channel: "#agent-approvals", text: `⏰ Email approval timed out. Draft discarded.\n*To:* ${recipient}\n*Subject:* ${draft.subject}`, }); }); return { status: "timed_out", action: "email_not_sent" }; } // The event payload can be used with whatever parameters that you send if (approval.data.approved) { await step.run("send-email", async () => { await sendEmail({ to: recipient, subject: draft.subject, body: draft.body, }); }); return { status: "approved", action: "email_sent" }; } return { status: "rejected", reason: approval.data.reason || "No reason provided", action: "email_not_sent", }; } ); ``` The `match` field correlates the response to the correct waiting function — if you have 50 pending approvals, each resolves independently. The function is suspended while waiting, so there's no compute cost while a human reviews. ## Send the approval response back When the human clicks a button (Slack, email, dashboard, etc.), send an event to Inngest so `step.waitForEvent()` resolves. **From a Slack interaction webhook:** ```typescript // NOTE - This is pseudo code for handling Slack interactions, please review their docs for implementation app.post("/api/slack/interactions", async (req, res) => { JSON.parse(req.body.payload); payload.actions[0]; JSON.parse(action.value); // Send the event using the client await inngest.send({ name: "agent/approval.response", data: { approvalId: value.approvalId, approved: value.approved, respondedBy: payload.user.id, reason: value.approved ? undefined : "Rejected via Slack", }, }); res.json({ text: value.approved ? "✅ Approved" : "❌ Rejected" }); }); ``` **From a custom dashboard API:** ```typescript app.post("/api/approvals/:approvalId/respond", async (req, res) => { req.params; req.body; await inngest.send({ name: "agent/approval.response", data: { approvalId, approved, respondedBy: req.user.id, reason, }, }); res.json({ status: "response_recorded" }); }); ``` The event's `data.approvalId` must match the `approvalId` from the original request. This is how `step.waitForEvent()` correlates the response. ## Handle approved, rejected, and timed-out responses Every approval gate has three outcomes. Handle all three: ```typescript await step.waitForEvent("wait-for-approval", { event: "agent/approval.response", match: "data.approvalId", timeout: "24h", }); if (!approval) { // TIMEOUT: No response within the window return { status: "timed_out" }; } if (approval.data.approved) { // APPROVED: Proceed with the action await step.run("execute-action", async () => { return await performAction(approval.data); }); return { status: "approved", result }; } // REJECTED return { status: "rejected", reason: approval.data.reason }; ``` ### Choose a timeout strategy You can choose how your AI workflow handles the human-in-the-loop timeout. Here are some ideas for suggestions: | Strategy | When to use | Implementation | |---|---|---| | **Auto-reject** | High-risk actions (delete, deploy, send to external) | Return early with `status: "timed_out"` | | **Auto-approve** | Low-risk, time-sensitive actions | Proceed if `!approval`, same as approved path | | **Escalate** | Actions that _must_ get a response | Notify a different reviewer, then `waitForEvent` again | | **Retry notification** | Human might have missed the first message | Re-notify, then wait with a new timeout | To escalate when nobody responds, wait again with a new reviewer: ```typescript await step.waitForEvent("wait-for-approval", { event: "agent/approval.response", match: "data.approvalId", timeout: "4h", }); if (!approval) { await step.run("escalate-to-manager", async () => { await sendSlackDM({ userId: event.data.escalationContact, text: `⚠️ Approval needed — original reviewer didn't respond in 4 hours.\n\n${actionSummary}`, }); }); await step.waitForEvent("wait-for-escalation", { event: "agent/approval.response", match: "data.approvalId", timeout: "4h", }); if (!escalatedApproval) { return { status: "timed_out", escalated: true }; } return handleApproval(escalatedApproval); } ``` ## Add approval gates inside a tool loop To gate dangerous tools while letting safe tools (reading data, searching) run freely, check tool names against an approval list inside [the loop](/docs/ai-patterns/agent-tool-loops): ```typescript ["send_email", "delete_record", "run_sql", "deploy"]; agentWithApproval = inngest.createFunction( { id: "agent-with-approval" }, { event: "agent/task.received" }, async ({ event, step }) => { let messages = [{ role: "user" as const, content: event.data.task }]; let iterations = 0; while (iterations < 20) { iterations++; await step.run(`think`, async () => { return await callLLM(messages, allTools); }); if (!llmResponse.toolCalls.length) { return { response: llmResponse.text, iterations }; } for (const toolCall of llmResponse.toolCalls) { if (APPROVAL_REQUIRED_TOOLS.includes(toolCall.name)) { // Create a unique approval ID that will not be re-used `${event.data.taskId}-${iterations}-${toolCall.name}`; await step.run(`request-approval-${approvalId}`, async () => { await sendSlackMessage({ channel: "#agent-approvals", text: [ `🔒 *Agent wants to execute: \`${toolCall.name}\`*`, `\`\`\`${JSON.stringify(toolCall.arguments, null, 2)}\`\`\``, ].join("\n"), }); }); await step.waitForEvent( `wait-approval-${approvalId}`, { event: "agent/approval.response", match: "data.approvalId", timeout: "4h", } ); if (!approval?.data.approved) { messages.push({ role: "tool" as const, content: `Tool call rejected by human reviewer. Reason: ${ approval?.data.reason || "No response / timed out" }. Choose a different approach.`, }); continue; } } await step.run( `tool-${toolCall.name}`, async () => { return await executeTool(toolCall.name, toolCall.arguments); } ); messages.push({ role: "tool" as const, content: result }); } } return { status: "max_iterations_reached" }; } ); ``` The loop pauses mid-iteration when a tool is called that requires approval. The human can take as long as the `timeout` waits - the function resumes exactly where it left off. ## Chain multiple approval gates To require sequential approvals from different reviewers (e.g., editorial then legal), chain multiple `step.waitForEvent()` calls: ```typescript multiApprovalWorkflow = inngest.createFunction( { id: "multi-approval-publish" }, { event: "content/publish.requested" }, async ({ event, step }) => { event.data; await step.run("generate-content", async () => { return await generateContent(contentId); }); // --- Gate 1: Editorial approval --- await step.run("request-editorial-review", async () => { await sendSlackMessage({ channel: "#editorial", text: `📝 Review needed: ${content.title}\n\n${content.preview}`, }); }); await step.waitForEvent("wait-editorial", { event: "content/review.completed", match: "data.contentId", timeout: "48h", }); if (!editorialApproval?.data.approved) { return { status: "rejected_by_editorial" }; } // --- Gate 2: Legal approval --- await step.run("request-legal-review", async () => { await sendSlackMessage({ channel: "#legal-review", text: `⚖️ Legal review needed: ${content.title}\n\nEditorial approved. Awaiting legal sign-off.`, }); }); await step.waitForEvent("wait-legal", { event: "content/legal-review.completed", match: "data.contentId", timeout: "72h", }); if (!legalApproval?.data.approved) { return { status: "rejected_by_legal" }; } // --- Both gates passed --- await step.run("publish", async () => { await publishContent(content); }); return { status: "published", approvals: ["editorial", "legal"] }; } ); ``` Each gate is durable and independent. If the editorial reviewer approves at 2 AM and the legal reviewer approves three days later, the function resumes correctly each time. ## Next steps - [Build an agent tool loop](/docs/ai-patterns/agent-tool-loops) with `step.run()` - [Delegate subtasks to child agents](/docs/ai-patterns/sub-agent-delegation) with `step.invoke()` - Learn more about [`step.waitForEvent()`](/docs/features/inngest-functions/steps-workflows/wait-for-event) in the reference docs - [Combine this approach with our "realtime" feature](/docs/examples/realtime#human-in-the-loop-bi-directional-workflows) for approvals from the UI - [Why durable execution matters for HITL](/blog/durable-execution-key-to-harnessing-ai-agents?ref=docs-ai-patterns-human-in-the-loop) — how suspend/resume makes approval gates possible # Delegate Tasks to Sub-Agents Source: https://www.inngest.com/docs/ai-patterns/sub-agent-delegation Description: Delegate tasks to specialized sub-agents in your AI system, each with its own context window, tools, and token budget. Scale multi-agent pipelines with Inngest. metaTitle = "Sub-Agent Delegation" Delegation gives sub-agents their own context window, tools, and token budget. A sub-agent can be modeled as a separate Inngest function that runs its own [agent loop](/docs/ai-patterns/agent-tool-loops) — the parent either waits for a result or fires and forgets. ## Define a sub-agent function A sub-agent is a regular Inngest function. It receives a task, runs an agent loop, and returns a result: ```typescript subAgent = inngest.createFunction( { id: "sub-agent", triggers: [{ event: "agent/sub-agent.spawn" }] }, async ({ event, step, logger }) => { event.data; `You are a focused sub-agent. Complete the following task and return a clear, concise result.\n\nTask: ${task}`; await runAgentLoop({ step, systemPrompt, sessionId, tools: SUB_AGENT_TOOLS, // No delegation tools — see "Prevent recursion" maxIterations: 20, }); return { response: result.response, iterations: result.iterations, }; } ); ``` The sub-agent uses a **restricted tool set** without delegation tools to prevent infinite recursion ([details below](#prevent-recursion)). ## Define the delegation tool Give the parent agent's LLM a tool that makes delegation a natural choice: ```typescript { type: "function", function: { name: "delegate_task", description: "Delegate a task to a sub-agent that will work on it independently and return a result. " + "Use this for tasks that require deep research, many tool calls, or focused work " + "that would clutter the current conversation.", parameters: { type: "object", properties: { task: { type: "string", description: "A clear, self-contained description of the task. Include all necessary context — " + "the sub-agent does not have access to this conversation's history.", }, }, required: ["task"], }, }, }; ``` ## Delegate synchronously with `step.invoke()` To delegate and wait for a result, use [`step.invoke()`](/docs/guides/invoking-functions-directly) — the parent blocks until the sub-agent returns: ```typescript parentAgent = inngest.createFunction( { id: "parent-agent", triggers: [{ event: "agent/task.received" }] }, async ({ event, step }) => { let messages = [{ role: "system", content: SYSTEM_PROMPT }]; let done = false; let i = 0; while (!done && i < 30) { await step.run(`think`, async () => { return await callLLM(messages, TOOLS); }); for (const toolCall of response.toolCalls) { let toolResult: string; if (toolCall.name === "delegate_task") { // Synchronous delegation — parent waits for the result await step.invoke(`sub-agent`, { function: subAgent, data: { task: toolCall.arguments.task, sessionId: `sub-${event.data.sessionId}-${Date.now()}`, }, }); toolResult = subResult?.response ?? "(no response from sub-agent)"; } else { toolResult = await step.run(`tool-${toolCall.name}`, async () => { return await executeTool(toolCall.name, toolCall.arguments); }); } messages.push( { role: "assistant", content: null, tool_calls: [toolCall] }, { role: "tool", tool_call_id: toolCall.id, content: toolResult } ); } if (response.toolCalls.length === 0) { done = true; } i++; } return { response: messages[messages.length - 1].content }; } ); ``` Because `step.invoke()` is a durable step, the parent pauses execution and resumes exactly where it left off when the sub-agent completes. The parent's LLM sees only the sub-agent's summary, not its full internal conversation. ## Delegate asynchronously with `step.sendEvent()` To delegate without waiting, use [`step.sendEvent()`](/docs/guides/sending-events-from-functions) — the parent fires an event and continues: ```typescript if (toolCall.name === "delegate_background_task") { await step.sendEvent("spawn-background-task", { name: "agent/sub-agent.spawn", data: { task: toolCall.arguments.task, sessionId: `sub-${event.data.sessionId}-${Date.now()}`, isAsync: true, replyTo: { type: "webhook", url: event.data.callbackUrl, }, }, }); toolResult = "Task delegated. The sub-agent is working on it in the background."; } ``` To deliver results when the sub-agent finishes, handle async mode in the sub-agent: ```typescript subAgent = inngest.createFunction( { id: "sub-agent", triggers: [{ event: "agent/sub-agent.spawn" }] }, async ({ event, step, logger }) => { event.data; await runAgentLoop({ step, systemPrompt: `Complete this task:\n\n${task}`, sessionId, tools: SUB_AGENT_TOOLS, maxIterations: 30, }); if (isAsync && replyTo) { await step.run("deliver-result", async () => { if (replyTo.type === "webhook") { await fetch(replyTo.url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ response: result.response }), }); } }); } return result; } ); ``` Alternatively, the sub-agent can emit a result event and a separate function handles delivery — this decouples the sub-agent from routing logic: ```typescript // Sub-agent emits result as an event if (isAsync) { await step.sendEvent("result-ready", { name: "agent/sub-agent.completed", data: { sessionId, parentSessionId: event.data.parentSessionId, response: result.response, }, }); } ``` ```typescript // Separate function handles result delivery deliverSubAgentResult = inngest.createFunction( { id: "deliver-sub-agent-result" }, { event: "agent/sub-agent.completed" }, async ({ event, step }) => { event.data; await step.run("deliver", async () => { await notifyUser(parentSessionId, response); }); } ); ``` ### Choose sync vs. async | | Sync (`step.invoke()`) | Async (`step.sendEvent()`) | |---|---|---| | **Parent blocks?** | Yes — waits for result | No — continues immediately | | **Result flows to** | Parent agent's tool output | Separate delivery (webhook, event, notification) | | **Best for** | Tasks where the parent needs the answer to continue (research, lookups, analysis) | Long-running tasks where the user can be notified later (reports, batch processing) | | **Timeout** | Subject to function execution time limits | Sub-agent runs on its own timeline | | **Retry behavior** | If sub-agent fails, parent's step retries | Sub-agent retries independently | ## Prevent recursion If a sub-agent has delegation tools, it could spawn sub-agents indefinitely. Prevent this by restricting the tool set: ```typescript // Tools available to the parent agent [ searchTool, readFileTool, writeFileTool, delegateTaskTool, // Can delegate delegateBackgroundTool, // Can delegate async ]; // Tools available to sub-agents — no delegation [ searchTool, readFileTool, writeFileTool, // No delegate tools — sub-agents cannot spawn further sub-agents ]; ``` Combine tool restriction with a hard iteration cap for two layers of protection: ```typescript subAgent = inngest.createFunction( { id: "sub-agent", retries: 1 }, { event: "agent/sub-agent.spawn" }, async ({ event, step }) => { await runAgentLoop({ step, systemPrompt: `Complete this task:\n\n${event.data.task}`, sessionId: event.data.sessionId, tools: SUB_AGENT_TOOLS, // Restricted — always maxIterations: 20, // Hard cap on iterations }); return result; } ); ``` ## Handle sub-agent failures `step.invoke()` retries the sub-agent based on its `retries` config. If all retries are exhausted, the error propagates to the parent. To let the parent LLM adapt, catch the error: ```typescript let toolResult: string; try { await step.invoke(`sub-agent`, { function: subAgent, data: { task: toolCall.arguments.task, sessionId: subSessionId }, }); toolResult = subResult?.response ?? "(no response)"; } catch (error) { toolResult = `Sub-agent failed: ${error.message}. You may need to handle this task directly.`; } ``` ## Schedule a sub-agent To run a sub-agent at a future time, include a [timestamp](/docs/guides/delayed-functions#schedule-a-function-for-later) (`ts`) when sending the event: ```typescript await step.sendEvent("schedule-daily-report", { name: "agent/sub-agent.spawn", data: { task: "Generate the daily analytics summary report.", sessionId: `scheduled-${Date.now()}`, isAsync: true, replyTo: { type: "webhook", url: REPORT_WEBHOOK_URL }, }, ts: tomorrow9am.getTime(), }); ``` For recurring work, use a [cron-triggered function](/docs/guides/scheduled-functions) instead. ## Write self-contained task descriptions In this set up, you can choose how context is shared between parent and sub-agents. With the above approach, the sub-agent gets it's main context from the "task" given to the sub-agent. In this approach, you'll want to include everything it needs in the task itself: ```typescript // ❌ Bad — relies on context the sub-agent doesn't have { task: "Summarize what we discussed above" } // ✅ Good — self-contained with all necessary context { task: "Summarize the key findings from the Q4 2025 revenue report. Focus on: 1) YoY growth rate, 2) top performing segments, 3) areas of concern." } ``` ## Generic vs. specialized sub-agents Generic sub-agents are a great way to get started and work for many use cases. If your system requires more specialized sets of tools or different models for sub-agents, you might consider creating specialized sub-agents. To create a system with specialized sub-agents, follow the patterns above, but create multiple tools, with each that invoke their own agent or a "loader" sub-agent that can handle multiple agent types conditionally. As a recommendation, LLMs often do better with separate tools for separate sub-agents rather than a single tool with different parameters for selection. The task description and available tools are enough to specialize behavior. ## Next steps - [Build an Agent Tool Loop](/docs/ai-patterns/agent-tool-loops) — Build the agent loop that powers both parent and sub-agents. - [Pause for Human Approval](/docs/ai-patterns/human-in-the-loop) — Add approval gates before or after delegation with `step.waitForEvent()`. - [Three sub-agent patterns you need for your agentic system](/blog/three-patterns-you-need-for-agentic-systems) — sync, async, and scheduled delegation patterns in depth {/*- [Run Sub-Agents in Parallel](/docs/ai-patterns/parallelization) — Spawn multiple sub-agents simultaneously and aggregate results. - [Manage Context Across Agents](/docs/ai-patterns/context-management) — Strategies for managing token budgets across agent hierarchies.*/} # AI Coding Agent Plugins and Skills Source: https://www.inngest.com/docs/ai-dev-tools/agent-skills Description: Install Inngest plugins and skills for Claude Code, Codex, Cursor, and other AI coding agents so they can write, test, and debug durable functions correctly. metaTitle = "AI Coding Agent Plugins & Skills"; Inngest provides official plugins and [agent skills](https://agentskills.io) for AI coding agents like Claude Code, Codex, Cursor, and Windsurf. They give your agent current guidance for building reliable applications with Inngest — from initial setup to advanced flow control and realtime updates. Use the Claude Code or Codex plugin for the best experience. Each plugin packages Inngest skills with local dev server MCP wiring so your agent can write code and inspect running functions in one loop. Start from the [AI development tools hub](/docs/ai-dev-tools?ref=docs-agent-skills) if you want to compare agent skills, Inngest MCP, CLI workflows, and LLM-ready docs in one place. ## What to install | AI tool | Recommended install | What it includes | | --- | --- | --- | | **Claude Code** | Inngest Claude Code plugin | Core Inngest skills, MCP config for the local dev server, and an eval harness | | **Codex** | Inngest Codex plugin bundle | Codex skills, Codex plugin metadata, MCP config, copyable examples, local marketplace metadata, and an eval harness | | **Cursor, Windsurf, and other agents** | Standalone `inngest-skills` repository | The core skill content in a portable format | ## Why use Inngest plugins? Instead of relying on an AI model's training data, which may be outdated or incomplete, Inngest plugins provide: - **Current API knowledge** — Accurate function signatures, configuration options, and best practices - **Step-by-step guidance** — Structured instructions for common Inngest patterns - **Workflow templates** — Proven patterns for background jobs, scheduled tasks, webhook handlers, and event-driven workflows - **Repository audits** — Agent-first guidance for finding durability gaps before making code changes - **Durable agent patterns** — Guidance for AgentKit workflows, tool calls, human approval, realtime progress, and provider flow control - **Local feedback loops** — MCP access to your running dev server so agents can list functions, send events, and inspect runs - **Cloud operations** — MCP access to deployed apps, runs, traces, environments, Insights, sessions, webhooks, and experiments ## Install ```bash {{ title: "Claude Code" }} /plugin marketplace add inngest/inngest-claude-code-plugin /plugin install inngest@inngest-claude-code-plugin ``` ```text {{ title: "Codex" }} git clone https://github.com/inngest/inngest-codex-plugin.git # In Codex: /plugin install /absolute/path/to/inngest-codex-plugin/plugins/inngest ``` ```bash {{ title: "Skills.sh" }} npx skills add inngest/inngest-skills ``` ```text {{ title: "Cursor" }} # Add to your .cursorrules file: Load the Inngest skills from https://github.com/inngest/inngest-skills for building with Inngest's durable execution platform. ``` For other agents, reference the [standalone skills repository](https://github.com/inngest/inngest-skills) directly or clone it to your agent's skills directory. Each skill is self-contained with full documentation in its `SKILL.md` file. ## Core skills | Skill | Description | What it covers | | --- | --- | --- | | **inngest-setup** | Set up Inngest in a TypeScript project | SDK installation, client config, environment variables, dev server | | **inngest-events** | Design and send Inngest events | Event schema, naming conventions, idempotency, fan-out patterns, system events | | **inngest-durable-functions** | Create and configure durable functions | Triggers, step execution, memoization, cancellation, error handling, retries | | **inngest-steps** | Use step methods to build durable workflows | `step.run`, `step.sleep`, `step.waitForEvent`, loops, parallel execution | | **inngest-flow-control** | Configure flow control for functions | Concurrency limits, throttling, rate limiting, debounce, priority, batching | | **inngest-middleware** | Create middleware for cross-cutting concerns | Middleware lifecycle, dependency injection, built-in middleware | | **inngest-realtime** | Stream workflow updates to users | Realtime channels, subscription tokens, React hooks, SSE consumers | ## Additional Codex plugin skills The Codex plugin adds skills that are tuned for repository-scale agent work: | Skill | Description | What it covers | | --- | --- | --- | | **inngest-brownfield-audit** | Audit an existing codebase before changing it | Framework detection, existing Inngest usage, webhooks, cron jobs, queues, long-running routes, polling loops, AI agents, and safe first integration slices | | **inngest-agents** | Build durable AI agents with Inngest and AgentKit | Model calls, tool calls, human approval, realtime progress, retries, and provider flow control | | **inngest-v3-v4-migration** | Upgrade TypeScript SDK v3 projects to v4 | Trigger syntax, typed events, serve options, `step.invoke`, native realtime, local dev mode, and mixed v3/v4 cleanup | | **inngest-api** | Operate Inngest through the alpha API CLI | Account, environment, webhook, app sync, function invocation, run, and trace operations | ## Language support These plugins and skills are currently focused on **TypeScript**. Core concepts like events, steps, flow control, and realtime updates apply across all Inngest SDKs, but code examples and setup instructions are TypeScript-specific. For Python or Go, refer to the [Inngest documentation](/docs) and [llms.txt](https://www.inngest.com/llms.txt) for language-specific guidance. ## Combine with Inngest MCP For the best AI development experience, use agent skills alongside [Inngest MCP](/docs/ai-dev-tools/mcp?ref=docs-agent-skills). Together they provide: - **Skills** give your agent knowledge of *how* to write correct Inngest code - **Dev Server MCP** lets your agent test and debug functions running on your machine - **Cloud MCP** lets your agent inspect and operate deployed environments, functions, events, and runs This enables a complete write, test, and debug loop powered by your coding agent. ## Repositories The plugin repositories share core skills from [`inngest/inngest-skills`](https://github.com/inngest/inngest-skills) so Claude Code, Codex, and portable skills installs stay aligned. The Codex plugin also includes Codex-specific skills, examples, and eval fixtures for agent-first codebase audits and durable agent workflows. ## Resources # AI development tools for Inngest Source: https://www.inngest.com/docs/ai-dev-tools/index Description: Use Inngest with AI coding agents, MCP, CLI workflows, and LLM-ready docs to build, test, and debug durable functions faster. Use Inngest with AI coding agents, MCP tools, CLI workflows, and LLM-ready documentation. These resources help agents write current Inngest code, work with local or deployed functions, trigger test events, inspect runs, and debug durable functions in one loop. } iconPlacement="top" > Connect Claude Code, Codex, Cursor, and other MCP clients to Inngest Cloud or your local Dev Server to inspect and operate functions and runs. } iconPlacement="top" > Install Inngest plugins and portable skills so coding agents have current guidance for setup, durable functions, steps, flow control, realtime, and migrations. } iconPlacement="top" > Use the Inngest CLI from an agent session to start the Dev Server, trigger events, inspect runs, and keep local debugging grounded in real execution. } iconPlacement="top" > Give AI tools compact or full-text documentation context with `llms.txt` and `llms-full.txt`. ## How the tools fit together - **Agent skills and plugins** teach your coding agent how to build with Inngest APIs and current patterns. - **Inngest MCP** lets your agent inspect and operate deployed Cloud environments or your running local app. - **The CLI** gives both you and your agent a terminal-first way to start local services and debug function runs. - **LLM docs** provide crawlable markdown context for AI tools that need a compact table of contents or the full docs corpus. For the best feedback loop, install the agent plugin or skills for your coding tool, connect it to the [Inngest MCP server](/docs/ai-dev-tools/mcp?ref=docs-ai-dev-tools), and use the local or Cloud endpoint that matches your task. ## LLM Docs An LLM-friendly version of the Inngest docs is available in two formats: - [inngest.com/llms.txt](https://www.inngest.com/llms.txt) - a table of contents for smaller context windows or tools that can crawl selected docs. - [inngest.com/llms-full.txt](https://www.inngest.com/llms-full.txt) - the full docs in markdown format. ## Next steps } > Start the Dev Server and run Inngest functions locally with production-like execution semantics. } > Build with the latest TypeScript SDK, including checkpointing, realtime, middleware, and updated step APIs. # Inngest Model Context Protocol (MCP) Source: https://www.inngest.com/docs/ai-dev-tools/mcp Description: Connect AI coding agents to Inngest Cloud or the local Dev Server with MCP to inspect apps, functions, events, runs, traces, and more. metaTitle = "Inngest Model Context Protocol (MCP)"; Inngest provides Model Context Protocol (MCP) servers for both Inngest Cloud and the local Dev Server. Connect Claude Code, Codex, Cursor, or another MCP client to inspect and operate Inngest from your coding agent. Inngest MCP tools match the [REST API v2](https://api-docs.inngest.com/) and [Inngest CLI commands](/docs/cli?ref=docs-ai-dev-tools-mcp). ## Choose an MCP server You can configure both servers in the same MCP client. Use the Dev Server while you build and test locally, then use Cloud MCP to inspect deployed environments and production data. | MCP server | Use it for | Endpoint | Authentication | | --- | --- | --- | --- | | **Inngest Cloud** | Deployed apps, functions, events, runs, traces, environments, Insights, sessions, webhooks, and experiments | `https://api.inngest.com/mcp` | Inngest API key | | **Dev Server** | Apps, functions, events, and runs on your local machine, plus embedded Inngest docs | `http://127.0.0.1:8288/mcp` | None | Some Cloud MCP tools change data, including tools that send events, invoke functions, rerun or cancel runs, sync apps, and manage environments or webhooks. Review a tool call before you approve it, especially in production. ## Connect to Inngest Cloud ### 1. Create an API key Create an [Inngest API key](/docs/platform/api-keys?ref=docs-ai-dev-tools-mcp) in the Cloud dashboard, then make it available to your MCP client: ```bash export INNGEST_API_KEY=sk-inn-api-... ``` Cloud MCP accepts API keys, not signing keys. Keep the key out of source control and use the narrowest access that your work needs. ### 2. Add Cloud MCP to your client ```bash {{ title: "Claude Code" }} claude mcp add --transport http inngest-cloud https://api.inngest.com/mcp \ --header "Authorization: Bearer $INNGEST_API_KEY" ``` ```bash {{ title: "Codex" }} codex mcp add inngest-cloud \ --url https://api.inngest.com/mcp \ --bearer-token-env-var INNGEST_API_KEY ``` ```json {{ title: "Cursor" }} { "mcpServers": { "inngest-cloud": { "url": "https://api.inngest.com/mcp", "headers": { "Authorization": "Bearer ${env:INNGEST_API_KEY}" } } } } ``` You can also open the [Cloud MCP setup page](https://app.inngest.com/mcp/setup) to copy client configuration and see the live tool list. ### 3. Try Cloud MCP Ask your coding agent to complete a task such as: ```text List my Inngest environments, then show the apps in production. ``` ```text Find recent failed runs for my billing app and explain the first failed step. ``` ```text Show the available Insights tables, then query event volume for the past 24 hours. ``` ## Connect to the Dev Server ### 1. Start the Dev Server The local MCP endpoint starts with the Inngest Dev Server: ```bash inngest dev ``` The default endpoint is `http://127.0.0.1:8288/mcp`. ### 2. Add Dev Server MCP to your client ```bash {{ title: "Claude Code" }} claude mcp add --transport http inngest-dev http://127.0.0.1:8288/mcp ``` ```bash {{ title: "Codex" }} codex mcp add inngest-dev --url http://127.0.0.1:8288/mcp ``` ```json {{ title: "Cursor" }} { "mcpServers": { "inngest-dev": { "url": "http://127.0.0.1:8288/mcp" } } } ``` The Dev Server dashboard also has an MCP setup page with client configuration and the live tool list. ### 3. Try Dev Server MCP ```text List my local apps, then list the functions in the checkout app. ``` ```text Send an app/order.created event and inspect every run it triggers. ``` ```text Inspect the trace for the latest failed run and explain the error. ``` ```text Search the Inngest docs for rate limiting examples. ``` ## Available MCP tools MCP clients receive the live tool list and each tool's input schema when they connect. The tables below summarize the current tools. Because eligible REST API v2 endpoints become MCP tools automatically, your client's tool list is the source of truth if it differs from this page. ### Tools available in Cloud and the Dev Server | Tool | What it does | | --- | --- | | `cancel_run` | Cancel an in-progress function run. | | `get_app` | Get one app, including sync metadata and function count. | | `get_apps` | List active or archived apps. | | `get_event_runs` | List the function runs triggered by an event. | | `get_function` | Get one function's configuration and status. | | `get_run` | Get the summary and optional output for one run. | | `get_run_trace` | Get the trace tree and optional span output for one run. | | `health` | Check the API service health. | | `invoke_function` | Invoke a function and return its run details. | | `list_function_runs` | List runs for one function. | | `list_functions` | List the functions in one app. | | `list_runs` | List runs, with optional app, function, status, and time filters. | | `rerun` | Rerun a function from the start or from a selected step. | | `send_event` | Send an event and return its event ID. | ### Cloud-only tools | Tool | What it does | | --- | --- | | `create_env` | Create a custom environment. | | `create_score` | Add scores to a run or its steps. | | `create_webhook` | Create an incoming webhook. | | `fetch_account` | Get the authenticated account. | | `fetch_account_event_keys` | List account event keys, optionally for one environment. | | `fetch_account_signing_keys` | List account signing keys, optionally for one environment. | | `get_experiment` | Get run counts and score aggregates for an experiment. | | `list_envs` | List custom environments. | | `list_experiments` | List observed experiments. | | `list_insights_event_schemas` | List event schemas observed by Insights. | | `list_insights_tables` | List tables available to Insights queries. | | `list_session_keys` | List session keys observed in an environment. | | `list_session_runs` | List runs for one session. | | `list_sessions` | List session IDs for one session key. | | `list_webhooks` | List incoming webhooks. | | `patch_env` | Archive or unarchive an environment. | | `query_insights` | Run an Insights SQL query. | | `query_insights_prompt` | Turn a natural-language prompt into an Insights SQL query. | | `sync_app` | Sync an app from its Inngest endpoint URL. | ### Dev Server-only tools | Tool | What it does | | --- | --- | | `grep_docs` | Search the docs embedded in the Dev Server. | | `list_docs` | List embedded documentation categories and counts. | | `read_doc` | Read one embedded documentation file. | For exact parameters and response shapes, inspect the schema shown by your MCP client or use the [REST API v2 reference](https://api-docs.inngest.com/). ## Target a Cloud environment Cloud tools accept an optional `env` argument. Your agent sends it when it calls a tool; it is not part of the MCP server setup. With account-scoped credentials, omit `env` to use the production environment or pass an environment slug to select another environment. An environment-scoped API key can only access its assigned environment. Be explicit in prompts that might affect data: ```text In the staging environment, send an app/order.created event with orderId test-123. ``` ## Common workflows ### Inspect a failed run 1. Use `list_runs` or `list_function_runs` with a failed status filter. 2. Use `get_run` to inspect the run summary and output. 3. Use `get_run_trace` to find the failed step and its error. ### Test an event-driven workflow 1. Use `get_apps` and `list_functions` to confirm the app and its triggers. 2. Use `send_event` with a test payload. 3. Use `get_event_runs` with the returned event ID. 4. Use `get_run` and `get_run_trace` to inspect each run. ### Find implementation guidance locally 1. Use `grep_docs` in the Dev Server to find a term or API name. 2. Use `read_doc` with a matching path to read the full source. 3. Apply the guidance, send a test event, and inspect the resulting run. ## Troubleshooting **Cloud MCP returns `401 Unauthorized`** - Confirm the client sends `Authorization: Bearer $INNGEST_API_KEY`. - Use an Inngest API key that starts with `sk-inn-api-`; signing keys are not supported. - Restart the MCP client after changing its environment variables. **The Dev Server MCP endpoint is not found** - Confirm `inngest dev` is running. - Confirm the client uses `http://127.0.0.1:8288/mcp`, or update the port if you changed it. - Restart the client after changing its MCP configuration. **Functions are not listed** - Call `get_apps` first and pass the returned app ID to `list_functions`. - Confirm the app has synced to the selected Cloud environment or local Dev Server. - Check app and Dev Server logs for registration errors. **Runs or events appear to be missing** - Confirm the Cloud tool call targeted the expected `env`. - Check that the event name matches the function trigger. - Allow a moment for event and run data to become available, then retry the read tool. ## Resources # Event format and structure Source: https://www.inngest.com/docs/events/_event-format-and-structure Inngest events are just JSON allowing them to be easily created and read. Here is a basic example of all required and optional payload fields: ```js await inngest.send({ name: "api/user.signup", data: { method: "google_auth" }, user: { id: "1JDydig4HHBJCiaGu2a9" }, ts: new Date().valueOf(), // = 1663702869305 v: "2022-09-20.1", }) ``` ## Required fields - `name: String` - The name of your event. We recommend names are lowercase and use dot-notation. Using prefixes (e.g. `/some.event`) is also encouraged to help organize your events. - `data: Object` - All data associated with the event. You can pass any data here and it will be serialized as JSON. Nested data is accepted, but we recommend keeping the payload simple and, more importantly, consistent. ## Optional fields - `user: Object` - Any relevant user identifying data or attributes associated with the event. All fields are upserted into a “User” in Inngest cloud to associate and group events together for easier debugging (see: “Benefits of the user object” below). - `ts: Number` - A **timestamp** integer representing the time (in milliseconds) at which the event occurred. NOTE - Inngest will automatically set this to the exact time the event is received so this is only needed if you want to use historic events or want exact values. - `v: String` - A **version** identifier for a particular event payload. Versions become useful to record when the payload format (aka event schema) was changed. We recommend the format `YYYY-MM-DD.N` where the `N` is an integer increased for every change. This is an optional, but very useful field. ### Benefits of the `user` object - User-based debugging The `user` object is a special field in Inngest Cloud. Sending data in this object enables: **unified user-based debugging and audit trails**. Inngest creates and stores a unified identity (aka profile) for each unique user that you send events for. You can think of it as performing an “upsert” for any data passed in the `user` object. There are two key ways to use this feature: - **Identifiers** - `id`, `email`, `phone`, or any field ending in `_id` will be used to match and group events together into a single identity. (Examples: `stripe_customer_id`, `zendesk_id`) - **Attributes** - Any non-identifier field will be stored as an attribute for your own reference. Potential uses for attributes: `billing_plan` , `signup_source`, or `last_login_at`. # Creating an Event Key Source: https://www.inngest.com/docs/events/creating-an-event-key Description: Generate and manage Event Keys to authenticate event ingestion from your app or third-party services. Scope keys to specific environments for security. metaTitle = "Create an Inngest Event Key" “Event Keys” are unique keys that allow applications to send (aka publish) events to Inngest. When using Event Keys with the [Inngest SDK](/docs/events), you can configure the `Inngest` client in 2 ways: 1. Setting the key as an [`INNGEST_EVENT_KEY`](/docs/sdk/environment-variables#inngest-event-key) environment variable in your application* 2. Passing the key as an argument ```jsx // Recommended: Set an INNGEST_EVENT_KEY environment variable for automatic configuration: new Inngest({ name: "Your app name" }); // Or you can pass the eventKey explicitly to the constructor: new Inngest({ name: "Your app name", eventKey: "xyz..." }); // With the Event Key, you're now ready to send data: await inngest.send({ ... }) ``` \* Our [Vercel integration](/docs/deploy/vercel) automatically sets the [`INNGEST_EVENT_KEY`](/docs/sdk/environment-variables#inngest-event-key) as an environment variable for you 🙋 Event Keys should be unique to a given environment (e.g. production, branch environments) and a specific application (your API, your mobile app, etc.). Keeping keys separated by application makes it easier to manage keys and rotate them when necessary. 🔐 **Securing Event Keys** - As Event Keys are used to send data to your Inngest environment, you should take precautions to secure your keys. Avoid storing them in source code and store the keys as secrets in your chosen platform when possible. ## Creating a new Event Key From the Inngest Cloud dashboard, Event Keys are listed in the "Manage" tab: 1. Next to the environment drop down, click the key icon, then "Event keys" ([direct link](https://app.inngest.com/env/production/manage/keys)) 2. Click the "+ Create Event Key" button at the top right 3. Update the Event Key's name to something descriptive and click "Save changes" 4. Copy the newly created key using the “Copy” button: ![A newly created Event Key in the Inngest Cloud dashboard](/assets/docs/platform/manage/event-keys/event-key-list.png) 🎉 You can now use this event key with the Inngest SDK to send events directly from any codebase. You can also: - Rename your event key at any time using the “Name” field so you and your team can identify it later - Delete the event key when your key is no longer needed - Filter events by name or IP addresses for increased control and security ⚠️ While it is _possible_ to use Event Keys to send events from the browser, this practice presents risks as anyone inspecting your client side code will be able to read your key and send events to your Inngest environment. If you'd like to send events from the client, we recommend creating an API endpoint or edge function to proxy the sending of events. # Sending events to Inngest Source: https://www.inngest.com/docs/events/index Description: Send events to Inngest using the TypeScript, Python, or Go SDK, or via HTTP from any language. Learn payload format, batching, and environment routing. metaTitle = "Sending Events to Inngest | SDK & HTTP Reference" structuredData = { "@type": "HowTo", name: "Send events to Inngest", description: "Create an Inngest client, send one or more events, and configure an Event Key for production environments.", step: [ { "@type": "HowToStep", name: "Create an Inngest client", text: "Instantiate the Inngest client in a shared file so it can be imported anywhere in your application.", }, { "@type": "HowToStep", name: "Send an event", text: "Call send() with an event name and data payload, then await the returned promise so the event is delivered before the process exits.", }, { "@type": "HowToStep", name: "Set an Event Key in production", text: "Set the INNGEST_EVENT_KEY environment variable with your Event Key so your application can send events to the correct Inngest environment.", }, ], }; Send events to Inngest from the TypeScript, Python, or Go SDK, or use the Event API to send events over HTTP from any language or service. To start, make sure you have [installed the Inngest SDK](/docs). In order to send events, you'll need to instantiate the `Inngest` client. We recommend doing this in a single file and exporting the client so you can import it anywhere in your app. In production, you'll need an event key, which [we'll cover below](#setting-an-event-key). ```ts {{ filename: 'inngest/client.ts' }} inngest = new Inngest({ id: "acme-storefront-app" }); // Use your app's ID ``` Now with this client, you can send events from anywhere in your app. You can send a single event, or [multiple events at once](#sending-multiple-events-at-once). ```ts {{ filename: 'app/api/checkout/route.ts' }} // This sends an event to Inngest. await inngest.send({ // The event name name: "storefront/cart.checkout.completed", // The event's data data: { cartId: "ed12c8bde", itemIds: ["9f08sdh84", "sdf098487", "0fnun498n"], account: { id: 123, email: "test@example.com", }, }, }); ``` 👉 `send()` is an asynchronous method that returns a `Promise`. You should always use `await` or `.then()` to ensure that the method has finished sending the event to Inngest. Serverless functions can shut down very quickly, so skipping `await` may result in events failing to be sent. ```python {{ filename: 'src/inngest/client.py' }} import inngest inngest_client = inngest.Inngest(app_id="acme-storefront-app") ``` Now with this client, you can send events from anywhere in your app. You can send a single event, or [multiple events at once](#sending-multiple-events-at-once). ```python {{ filename: 'src/api/checkout/route.py' }} import inngest from src.inngest.client import inngest_client # This sends an event to Inngest. await inngest_client.send( inngest.Event( name="storefront/cart.checkout.completed", data={ "cartId": "ed12c8bde", "itemIds": ["9f08sdh84", "sdf098487", "0fnun498n"], "account": { "id": 123, "email": "test@example.com", }, }, ) ) ``` 👉 `send()` is meant to be called asynchronously using `await`. For synchronous code, [use the `send_sync()` method instead](/docs/reference/python/client/send). ```go {{ title: "Go" }} !snippet:path=snippets/go/docs/clients/new_client_acme_storefront_app.go ``` Now with this client, you can send events from anywhere in your app. You can send a single event, or [multiple events at once](#sending-multiple-events-at-once). ```go {{ title: "Go" }} !snippet:path=snippets/go/docs/events/checkout_completed.go ``` Sending this event, named `storefront/cart.checkout.completed`, to Inngest will do two things: 1. Automatically run any [functions](/docs/learn/inngest-functions) that are triggered by this specific event, passing the event payload to the function's arguments. 2. Store the event payload in Inngest cloud. You can find this in the **Events** tab of the dashboard. 💡 One event can trigger multiple functions, enabling you to consume a single event in multiple ways. This is different than traditional message queues where only one worker can consume a single message. Learn about [the fan-out approach here](/docs/guides/fan-out-jobs). ## Setting an Event Key In production, your application will need an "Event Key" to send events to Inngest. This is a secret key that is used to authenticate your application and ensure that only your application can send events to a given [environment](/docs/platform/environments) in your Inngest account. You can learn [how to create an Event Key here](/docs/events/creating-an-event-key). Once you have a key, you can set it in one of two ways: 1. Set an `INNGEST_EVENT_KEY` environment variable with your Event Key. **This is the recommended approach.** 2. Pass the Event Key to the `Inngest` constructor as the `eventKey` option: ```ts {{ filename: 'inngest/client.ts' }} // NOTE - It is not recommended to hard-code your Event Key in your code. new Inngest({ id: "your-app-id", eventKey: "xyz..." }); ``` ```python {{ filename: 'src/inngest/client.py' }} import inngest # It is not recommended to hard-code your Event Key in your code. inngest_client = inngest.Inngest(app_id="your-app-id", event_key="xyz...") ``` Event keys are _not_ required in local development with the [Inngest Dev Server](/docs/local-development). You can omit them in development and your events will still be sent to the Dev Server. ## Event payload format The event payload is a JSON object that must contain a `name` and `data` property. Explore all events properties in the [Event payload format guide](/docs/features/events-triggers/event-format). ## Sending multiple events at once You can also send multiple events in a single `send()` call. This enables you to send a batch of events very easily. You can send up to `512kb` in a single request which means you can send anywhere between 10 and 1000 typically sized payloads at once. This is the default and can be increased for your account. ```ts await inngest.send([ { name: "storefront/cart.checkout.completed", data: { ... } }, { name: "storefront/coupon.used", data: { ... } }, { name: "storefront/loyalty.program.joined", data: { ... } }, ]) ``` This is especially useful if you have an array of data in your app and you want to send an event for each item in the array: ```ts // This function call might return 10s or 100s of items, so we can use map // to transform the items into event payloads then pass that array to send: await api.fetchAllItems(); importedItems.map((item) => ({ name: "storefront/item.imported", data: { ...item, } })); await inngest.send(events); ``` ## Sending events from within functions You can also send events from within your functions using `step.sendEvent()` to, for example, trigger other functions. Learn more about [sending events from within functions](/docs/guides/sending-events-from-functions). Within functions, `step.sendEvent()` wraps the event sending request within a `step` to ensure reliable event delivery and prevent duplicate events from being sent. We recommend using `step.sendEvent()` instead of `inngest.send()` within functions. ```ts export default inngest.createFunction( { id: "user-onboarding", triggers: { event: "app/user.signup" } }, async ({ event, step }) => { // Do something await step.sendEvent("send-activation-event", { name: "app/user.activated", data: { userId: event.data.userId }, }); // Do something else } ); ``` ## Using Event IDs Each event sent to Inngest is assigned a unique Event ID. These `ids` are returned from `inngest.send()` or `step.sendEvent()`. Event IDs can be used to look up the event in the Inngest dashboard or via [the REST API](https://api-docs.inngest.com/v1/events/GetEvent). You can choose to log or save these Event IDs if you want to look them up later. ```ts await inngest.send([ { name: "app/invoice.created", data: { invoiceId: "645e9e024befa68763f5b500" } }, { name: "app/invoice.created", data: { invoiceId: "645e9e08f29fb563c972b1f7" } }, ]); /** * ids = [ * "01HQ8PTAESBZPBDS8JTRZZYY3S", * "01HQ8PTFYYKDH1CP3C6PSTBZN5" * ] */ ``` ```python await inngest_client.send([ { name: "storefront/cart.checkout.completed", data: { ... } }, { name: "storefront/coupon.used", data: { ... } }, { name: "storefront/loyalty.program.joined", data: { ... } }, ]) ``` This is especially useful if you have an array of data in your app and you want to send an event for each item in the array: ```python # This function call might return 10s or 100s of items, so we can use map # to transform the items into event payloads then pass that array to send: importedItems = await api.fetchAllItems(); events = [ inngest.Event(name="storefront/item.imported", data=item) for item in importedItems ] await inngest_client.send(events); ``` ## Sending events from within functions You can also send events from within your functions using `step.send_event()` to, for example, trigger other functions. Learn more about [sending events from within functions](/docs/guides/sending-events-from-functions). Within functions, `step.send_event()` wraps the event sending request within a `step` to ensure reliable event delivery and prevent duplicate events from being sent. We recommend using `step.send_event()` instead of `inngest.send()` within functions. ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn(ctx: inngest.Context) -> list[str]: return await ctx.step.send_event("send", inngest.Event(name="foo")) ``` ## Using Event IDs Each event sent to Inngest is assigned a unique Event ID. These `ids` are returned from `inngest.send()` or `step.sendEvent()`. Event IDs can be used to look up the event in the Inngest dashboard or via [the REST API](https://api-docs.inngest.com/v1/events/GetEvent). You can choose to log or save these Event IDs if you want to look them up later. ```python ids = await inngest_client.send( [ inngest.Event(name="my_event", data={"msg": "Hello!"}), inngest.Event(name="my_other_event", data={"name": "Alice"}), ] ) # # ids = [ # "01HQ8PTAESBZPBDS8JTRZZYY3S", # "01HQ8PTFYYKDH1CP3C6PSTBZN5" # ] # ``` ```go !snippet:path=snippets/go/docs/events/send_many_events.go ``` ## Using Event IDs Each event sent to Inngest is assigned a unique Event ID. These `ids` are returned from `client.SendMany()` . Event IDs can be used to look up the event in the Inngest dashboard or via [the REST API](https://api-docs.inngest.com/v1/events/GetEvent). You can choose to log or save these Event IDs if you want to look them up later. ```go !snippet:path=snippets/go/docs/events/send_many_events_return_ids.go ``` ## Send events via HTTP (Event API) You can send events from any system or programming language with our API and an Inngest Event Key. The API accepts a single event payload or an array of event payloads. {/* NOTE - We'll leave other SDKs here for now, but in time, instead we'll make this entire guide have Python and Go examples for each section above */} To send an event to a specific [branch environment](/docs/platform/environments#branch-environments), set the `x-inngest-env` header to the name of your branch environment, for example: `x-inngest-env: feature/my-branch`. ```bash {{ title: 'cURL' }} curl -X POST https://inn.gs/e/$INNGEST_EVENT_KEY \ -H 'Content-Type: application/json' \ --data '{ "name": "user.signup", "data": { "userId": "645ea8289ad09eac29230442" } }' ``` ```php $url = "https://inn.gs/e/{$eventKey}"; $content = json_encode([ "name" => "user.signup", "data" => [ "userId" => "645ea8289ad09eac29230442", ], ]); $curl = curl_init($url); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HTTPHEADER, ["Content-type: application/json"]); curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_POSTFIELDS, $content); $json_response = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); if ($status != 200) { return [ 'status' => $status, 'message' => "Error: call to URL $url failed with status $status, response $json_response, curl_error " . curl_error($curl) . ", curl_errno " . curl_errno($curl), ]; } curl_close($curl); $response = json_decode($json_response, true); ``` When using the [dev server](/docs/local-development), use `http://localhost:8288/e/` as the endpoint. If [self-hosting](/docs/self-hosting), replace with the url for your self-hosted instance. The response will contain the `ids` of the events that were sent: ```json {{ title: 'Response' }} { "ids": ["01H08W4TMBNKMEWFD0TYC532GG"], "status": 200 } ``` ## Deduplication Often, you may need to prevent duplicate events from being processed by Inngest. If your system could possibly send the same event more than once, you will want to ensure that it does not run functions more than once. To prevent duplicate function runs from events, you can add an `id` parameter to the event payload. Once Inngest receives an event with an `id`, any events sent with the same `id` will be ignored, regardless of the event's payload. ```ts await inngest.send({ // Your deduplication id must be specific to this event payload. // Use something that will not be used across event types, not a generic value like cartId id: "cart-checkout-completed-ed12c8bde", name: "storefront/cart.checkout.completed", data: { cartId: "ed12c8bde", // ...the rest of the payload's data... } }); ``` ```python await inngest_client.send( inngest.Event( name="storefront/cart.checkout.completed", id="cart-checkout-completed-ed12c8bde", data={"cartId": "ed12c8bde"}, ) ) ``` ```go {{ title: "Go" }} !snippet:path=snippets/go/docs/events/event_idempotency.go ``` Learn more about this in the [handling idempotency guide](/docs/guides/handling-idempotency). 💡 Deduplication prevents duplicate function runs for 24 hours from the first event. The `id` is global across all event types, so make sure your `id` isn't a value that will be shared across different event types. For example, for two events like `storefront/item.imported` and `storefront/item.deleted`, do not use the `item`'s `id` (`9f08sdh84`) as the event deduplication `id`. Instead, combine the item's `id` with the event type to ensure it's specific to that event (e.g. `item-imported-9f08sdh84`). ## Further reading * [Creating an Event Key](/docs/events/creating-an-event-key) * [TypeScript SDK Reference: Send events](/docs/reference/typescript/v4/events/send) * [Python SDK Reference: Send events](/docs/reference/python/client/send) * [Go SDK Reference: Send events](https://pkg.go.dev/github.com/inngest/inngestgo#Client) # Cloudflare Pages Source: https://www.inngest.com/docs/deploy/cloudflare Description: Host Inngest functions on Cloudflare Pages. Set up the serve handler, configure environment variables, and deploy durable workflows. metaTitle = "Deploy Inngest to Cloudflare Pages" Inngest allows you to deploy your event-driven functions to [Cloudflare Pages](https://pages.cloudflare.com/). ## Deploying to Cloudflare Pages 1. [Write your functions](/docs/learn/inngest-functions) 2. [Serve your functions](/docs/learn/serving-inngest-functions#framework-cloudflare-pages-functions) 3. [Set environment variables](https://developers.cloudflare.com/pages/get-started/#environment-variables) for your deployment - `NODE_VERSION: 22` - `INNGEST_SIGNING_KEY: ***` - from [the Inngest dashboard](https://app.inngest.com/env/production/manage/signing-key) - `INNGEST_EVENT_KEY: ***` - from [the Inngest dashboard](https://app.inngest.com/env/production/manage/keys) Cloudflare Pages Functions run on the Workers runtime. Enable Node.js compatibility so Inngest can use `AsyncLocalStorage` during function execution: ```toml compatibility_flags = ["nodejs_compat"] compatibility_date = "2024-09-23" ``` ## Syncing your app After your code is deployed to Cloudflare Pages, you'll need to sync your app with Inngest. Learn how to [sync your app with Inngest here](/docs/apps/cloud#sync-a-new-app-in-inngest-cloud). # DigitalOcean Source: https://www.inngest.com/docs/deploy/digital-ocean Description: Run Inngest functions on DigitalOcean. Configure your serve endpoint, set environment variables, and connect your DigitalOcean app to the Inngest platform. metaTitle = "Deploy Inngest to DigitalOcean" Inngest functions can be deployed to DigitalOcean's Functions, App Platform, or Droplets. This page covers how to configure the [Inngest Add-Ons](https://marketplace.digitalocean.com/add-ons/inngest) for your DigitalOcean App Platform projects or Droplets. To configure Inngest with DigitalOcean Functions, see the [`serve()` reference](/docs/learn/serving-inngest-functions#framework-digital-ocean-functions). - [Configure a DigitalOcean App Platform project or Droplet with a new Inngest account](#configure-a-digitalocean-app-platform-project-or-droplet-with-a-new-inngest-account) - [Configure a DigitalOcean App Platform project or Droplet with an existing Inngest account](#configure-a-digitalocean-app-platform-project-or-droplet-with-an-existing-inngest-account) ## Configure a DigitalOcean App Platform project or Droplet with a new Inngest account ### Prerequisites Before starting, make sure you have: - A DigitalOcean account with an application deployed on App Platform or on a Droplet. - Access to SaaS Add-Ons and environment variables in your App Platform project or Droplet. ### Step 1: Add Inngest as a SaaS Add-On From your DigitalOcean dashboard: - Navigate to [the Inngest Add-Ons page](https://marketplace.digitalocean.com/add-ons/inngest) - Click the "Add Inngest" button in the top-right. - Select the region closest to your app's deployment. - Click "Create Resource". DigitalOcean will create an Inngest account for you. You can now proceed to step 2 to access your Inngest application credentials. ### Step 2: Access your Inngest application credentials Navigate to your [DigitalOcean Dashboard](https://cloud.digitalocean.com/) and click "Add-Ons" in the left sidebar. Once the Inngest Add-On is installed, click the "View Inngest" link to access the Inngest Dashboard: ![image.png]() From the Inngest Dashboard, click the keys icon in the top-left to access the Event and Signing keys: ![image.png]() Visit both the Event key and Signing key pages to copy their values. ### Step 3: Configure Environment Variables Back in your DigitalOcean Dashboard, copy the Event and Signing keys into your application's environment variables as follows: - `INNGEST_EVENT_KEY` → copy from the Inngest Dashboard - `INNGEST_SIGNING_KEY` → copy from the Inngest Dashboard ### You're all set: install and configure the Inngest SDK Your DigitalOcean application is now configured with the Inngest Add-On. You can now install the Inngest SDK in your application and follow the tutorials below based on your language and framework: - [TypeScript quick start]() - [Python quick start]() - [Go reference]() **Note:** Once your application is configured and deployed with the Inngest SDK, go to the Inngest Dashboard to [sync your application](/docs/apps/cloud). ## Configure a DigitalOcean App Platform project or Droplet with an existing Inngest account To configure a DigitalOcean App Platform project or Droplet with an existing Inngest account: [Follow this guide](/docs/apps/cloud) to create and sync a new app in Inngest Cloud. # Netlify Source: https://www.inngest.com/docs/deploy/netlify Description: Host Inngest functions on Netlify. Set up the serve handler, configure your Netlify function endpoint, and connect to Inngest for durable background jobs. metaTitle = "Deploy Inngest to Netlify" We provide a Netlify build plugin, [netlify-plugin-inngest](https://www.npmjs.com/package/netlify-plugin-inngest), that allows you to automatically sync any found apps whenever your site is deployed to Netlify. {/* TODO Add Netlify UI instructions once PR is merged at https://github.com/netlify/plugins/pull/843 */} ## Setup 1. Install `netlify-plugin-inngest` as a dev dependency: ```sh npm install --save-dev netlify-plugin-inngest # or yarn add --dev netlify-plugin-inngest ``` 2. Create or edit a `netlify.toml` file at the root of your project with the following: ```toml [[plugins]] package = "netlify-plugin-inngest" ``` Done! 🥳 Whenever your site is deployed, your app hosted at `/api/inngest` will be synced. ## Configuration If you want to use a URL that isn't your "primary" Netlify domain, or your functions are served at a different path, provide either `host`, `path`, or both as inputs in the same file: ```toml [[plugins]] package = "netlify-plugin-inngest" [plugins.inputs] host = "https://my-specific-domain.com" path = "/api/inngest" ``` # Render Source: https://www.inngest.com/docs/deploy/render Description: Run Inngest background functions on Render. Configure the serve endpoint, set required environment variables, and sync your app with the Inngest platform. metaTitle = "Deploy Inngest to Render" [Render](https://render.com) lets you easily deploy and scale full stack applications. You can deploy your Inngest functions on Render using any web framework, including [Next.js](https://docs.render.com/deploy-nextjs-app), [Express](https://docs.render.com/deploy-node-express-app), and [FastAPI](https://docs.render.com/deploy-fastapi). Below, we'll cover how to deploy: 1. A production Inngest app 1. Preview apps for each of your Git development branches ### Before you begin * Create a web application that serves Inngest functions. * Test this web app locally with the [Inngest dev server](/docs/local-development). ## Deploy a production app on Render 1. Deploy the web application that contains your Inngest functions to Render. * See [Render's guides](https://docs.render.com) to learn how to deploy specific frameworks, such as: - [Next.js](https://docs.render.com/deploy-nextjs-app) - [Express](https://docs.render.com/deploy-node-express-app) - [FastAPI](https://docs.render.com/deploy-fastapi) 1. Set the `INNGEST_SIGNING_KEY` and `INNGEST_EVENT_KEY` environment variables on your Render web app. * You can easily [configure environment variables](https://docs.render.com/configure-environment-variables) on a Render service through the Render dashboard. * You can find your production `INNGEST_SIGNING_KEY` [here](https://app.inngest.com/env/production/manage/signing-key), and your production `INNGEST_EVENT_KEY`s [here](https://app.inngest.com/env/production/manage/keys). 1. Manually sync your Render web app with Inngest. * See [this Inngest guide](/docs/apps/cloud) for instructions. ## Automatically sync your app Each time you push changes to your Inngest functions, you need to sync your web app with Inngest. For convenience, you can automate these syncs from your CI/CD. See our [programmatic syncing guide](/docs/apps/cloud#programmatically) for more information. ## Set up preview apps on Render ### What are preview apps? Render lets you deploy work-in-progress versions of your apps using code in a Git development branch. Specifically, you can deploy: * [Service previews](https://docs.render.com/pull-request-previews): a temporary standalone instance of a single Render service. * [Preview environments](https://docs.render.com/preview-environments): a disposable copy of your production environment that can include multiple services and databases. You can use Render's service previews and preview environments together with Inngest's [branch environments](/docs/platform/environments). ### Set up Inngest in preview apps To use Inngest in a Render service preview or preview environment, follow these steps. One-time setup: 1. Follow Render's guides to enable either a [service preview](https://docs.render.com/pull-request-previews) or a [preview environment](https://docs.render.com/preview-environments). 2. In Inngest, create a _branch environment_ `INNGEST_SIGNING_KEY` and a _branch environment_ `INNGEST_EVENT_KEY`. * You can find your branch environment `INNGEST_SIGNING_KEY` [here](https://app.inngest.com/env/branch/manage/signing-key). * You can create a branch environment `INNGEST_EVENT_KEY` [here](https://app.inngest.com/env/branch/manage/keys). Each time a preview app is deployed: 1. Set the following environment variables on the preview service: * `INNGEST_SIGNING_KEY` and `INNGEST_EVENT_KEY`: Use the values from your Inngest branch environment. * `INNGEST_ENV`: Provide any value you want. This value will be used as [the name of the branch in Inngest](/docs/platform/environments#configuring-branch-environments). As an option, you can use the value of [`RENDER_GIT_BRANCH`](https://docs.render.com/environment-variables#all-runtimes). You can [configure environment variables](https://docs.render.com/configure-environment-variables) on the preview service through the Render dashboard. Alternatively, you can send a `PUT` or `PATCH` request [via the Render API](https://api-docs.render.com/reference/update-env-vars-for-service). 2. Sync the app with Inngest. You can manually sync the app [from the branch environments section](https://app.inngest.com/env/branch/apps/sync-new) of your Inngest dashboard, or automatically sync your app using a strategy [described above](#automatically-sync-your-app-with-inngest). # Vercel Source: https://www.inngest.com/docs/deploy/vercel Description: Host Inngest functions on Vercel serverless functions alongside your existing API routes. Supports Next.js App Router, Pages Router, and Express-style handlers. metaTitle = "Deploy Inngest to Vercel" Inngest enables you to host your functions on Vercel using their [serverless functions platform](https://vercel.com/docs/concepts/functions/serverless-functions). This allows you to deploy your Inngest functions right alongside your existing website and API functions running on Vercel. Inngest will call your functions securely via HTTP request on-demand, whether triggered by an event or on a schedule in the case of cron jobs. ## Hosting Inngest functions on Vercel After you've written your functions using [Next.js](/docs/learn/serving-inngest-functions?ref=docs-deploy-vercel#framework-next-js) or Vercel's [Express-like](/docs/learn/serving-inngest-functions?ref=docs-deploy-vercel#framework-express) functions within your project, you need to serve them via the `serve` handler. Using the `serve` handler, create a Vercel/Next.js function at the `/api/inngest` endpoint. Here's an example in a Next.js app: ## Choose the Next.js App Router or Pages Router: ```ts // This endpoint can run for a maximum of 300 seconds maxDuration = 300; export default serve({ client: client, functions: [ firstFunction, anotherFunction ] }); ``` ```ts // This endpoint can run for a maximum of 300 seconds maxDuration = 300; { GET, POST, PUT } = serve({ client: client, functions: [ firstFunction, anotherFunction ] }); ``` We strongly recommend that you configure the `maxDuration` for your Inngest endpoint to ensure that your steps can successfully execute without timeouts. When using with [`checkpointing`](/docs/setup/checkpointing) (the default in our v4 SDK), you should set checkpointing's `maxRuntime` option to 20-40% below your `maxDuration` setting on Vercel. [See the Vercel docs for more information](https://vercel.com/docs/functions/configuring-functions/duration). Note, when `streaming` is enabled, your function can execute well beyond the `maxDuration`. Learn more about [the streaming option here](/docs/reference/typescript/v4/serve/streaming). ## Deploying to Vercel Installing [Inngest's official Vercel integration](https://app.inngest.com/settings/integrations/vercel/connect) does 3 things: 1. Automatically sets the required [`INNGEST_SIGNING_KEY`](/docs/sdk/environment-variables#inngest-signing-key) environment variable to securely communicate with Inngest's API ([docs](/docs/platform/signing-keys)). 2. Automatically sets the [`INNGEST_EVENT_KEY`](/docs/sdk/environment-variables#inngest-event-key) environment variable to enable your application to send events ([docs](/docs/events/creating-an-event-key)). 3. Automatically syncs your app to Inngest every time you deploy updated code to Vercel - no need to change your existing workflow! [Install the Inngest Vercel integration](https://app.inngest.com/settings/integrations/vercel/connect) To enable communication between Inngest and your code, you need to either [disable Deployment Protection](https://vercel.com/docs/security/deployment-protection#configuring-deployment-protection) or, if you're on Vercel's Pro plan, configure protection bypass: ## Bypassing Deployment Protection If you have Vercel's [Deployment Protection feature](https://vercel.com/docs/security/deployment-protection) enabled, _by default_, Inngest may not be able to communicate with your application. This may depend on what configuration you have set: * **"Standard protection"** or **"All deployments"** - This affects Inngest production and [branch environments](/docs/platform/environments). * **"Only preview deployments"** - This affects [branch environments](/docs/platform/environments). To work around this, you can either: 1. Disable deployment protection 2. Configure protection bypass (_Protection bypass may or may not be available depending on your pricing plan_) ### Configure protection bypass To enable this, you will need to leverage Vercel's "[Protection Bypass for Automation](https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation)" feature. Here's how to set it up: 1. Enable "Protection Bypass for Automation" on your Vercel project 2. Copy your secret 3. Go to [the Vercel integration settings page in the Inngest dashboard](https://app.inngest.com/settings/integrations/vercel) 4. For each project that you would like to enable this for, add the secret in the "Deployment protection key" input. Inngest will now use this parameter to communicate with your application to bypass the deployment protection. [IMAGE] 5. Trigger a re-deploy of your preview environment(s) (this resyncs your app to Inngest) ## Overriding the default hostname By default, Inngest uses your Vercel project's "[deployment urls](https://vercel.com/docs/deployments/generated-urls)" to sync with Inngest. When you deploy, Vercel's webhook sends these URLs to Inngest which are unique for each version of your code that you deploy. These unique URLs are used both for production deploys and branch environments. The URLs used in production are _not_ preview environments. One of these URLs might look like this: ``` https://myapp-pxz35n5o2-acmeinc.vercel.app ``` If you desire Inngest to use your custom domain in production, you can set the `INNGEST_SERVE_ORIGIN` in your Vercel project's environment variables for production. When you push to production and Inngest syncs your app, it will instead use this URL. Please note: if your application is not reachable on this URL, your app sync will fail. ``` INNGEST_SERVE_ORIGIN=https://acme.com ``` ## Multiple apps in one single Vercel project You can pass multiple paths by adding their path information to each Vercel project in the Vercel Integration’s settings page. [IMAGE] You can also add paths to separate functions within the same app for bundle size issues or for running certain functions on the edge runtime for streaming. ## Setting up a staging environment If you prefer to set up a dedicated staging environment instead of using [branch environments](/docs/platform/environments), you can do that leveraging Inngest's custom environments and Vercel's custom environments. Here are the steps to set that up correctly: 1. Create a Vercel custom environment ([docs](https://vercel.com/docs/deployments/environments#custom-environments)) 2. Create an Inngest custom environment ([docs](/docs/platform/environments#custom-environments)) 3. Create an Inngest Event Key in your new custom environment with a memorable name like "Vercel - My app" ([docs](/docs/events/creating-an-event-key)). Copy this for later. [IMAGE] 4. Copy your Inngest [Signing Key](/docs/platform/signing-keys) 5. In the Vercel project dashboard, add two new environment variables, `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` using your two copied values, selecting only to use them for your new custom environment, e.g. "staging." [IMAGE] 6. If using the Vercel integration, the next time you deploy, the app should be synced to your custom environment in Inngest. If you don't use the Vercel integration, you will have to manually sync your app after you deploy (see section below). ## Manually syncing apps While we strongly recommend our Vercel integration, you can still use Inngest by manually telling Inngest that you've deployed updated functions. You can sync your app [via the Inngest UI](/docs/apps/cloud#sync-a-new-app-in-inngest-cloud) or [via our API with a curl request](/docs/apps/cloud#curl-command). # Syncing an Inngest App Source: https://www.inngest.com/docs/apps/cloud Description: Learn how to sync your Inngest app with the platform after each deploy, so your functions are always up to date in production and branch environments. metaTitle = "Sync Your Inngest App After Deploy" After deploying your code to a hosting platform, it is time to go to production and inform Inngest about your apps and functions. Check what [Inngest Apps](/docs/apps) are if you haven't done it yet. ## Sync a new app in Inngest Cloud You can synchronize your app with Inngest using three methods: - Manually - Automatically using an integration - Programmatically with the REST API ### Manually 1. Select your environment (for example, "Production") in Inngest Cloud and navigate to the Apps page. You’ll find a button named “Sync App” or “Sync New App”, depending on whether you already have synced apps. [IMAGE] [IMAGE] 2. Provide the location of your app by pasting the URL of your project’s `serve()` endpoint and click on “Sync App”. [IMAGE] 3. Your app is now synced with Inngest. 🎉 [IMAGE] ### Automatically using an integration [Learn how to install our official Vercel integration](/docs/deploy/vercel?ref=docs-app) [Learn how to install our official Netlify integration](/docs/deploy/netlify?ref=docs-app) ### Programmatically Use the [Inngest REST API](https://api-docs.inngest.com/v2/apps/SyncApp) to programmatically sync your app. This is particularly useful when syncing from a CI/CD pipeline. Before you send the request, you'll need 3 things: 1. An [API key](/docs/platform/api-keys). 2. The URL for your app's Inngest endpoint (e.g. `https://my-app.com/api/inngest`). 3. Your app's ID (this is the Inngest client's ID). Once you have these, you can send a request like this: ```sh curl -X POST "https://api.inngest.com/v2/apps/$APP_ID/syncs" \ -H "Authorization: Bearer $INNGEST_API_KEY" \ -d "{\"url\": \"$APP_URL\"}" ``` Before syncing with Inngest, ensure that the latest version of your code is live on your platform. This is because some platforms have rolling deploys that take seconds or minutes until the latest version of your code is live. This is especially important when setting up your own automated process. ## How and when to resync an app To ensure that your functions are up to date, you need to resync your app with Inngest whenever you deploy new function configurations to your hosted platform. If you are syncing your app through an integration, this process is automatically handled for you. ### When to resync Vercel apps manually We recommend using our official Vercel integration, since the syncing process is automatic. You will want to resync a Vercel app manually if: - There was an error in the automatic syncing process (such as a network error) - You chose not to install the Vercel integration and synced the app manually If you have the Vercel integration and resync the app manually, the next time you deploy code to Vercel, the app will still be automatically resynced. [Vercel generates a unique URL for each deployment](https://vercel.com/docs/deployments/generated-urls). Please confirm that you are using the correct URL if you choose a deployment's generated URL instead of a static domain for your app. ### How to resync manually 1. Navigate to the app you want to resync. You will find a “Resync” button at the top-right corner of the page. [IMAGE] 2. You will see a confirmation modal. Click on “Resync App”. [IMAGE] If your app location changes, enable the "Override" switch and edit the URL before clicking on "Resync App". Please ensure that the app ID is the same, otherwise Inngest will consider it a new app white resyncing. [IMAGE] ## Troubleshooting

Why is my app syncing to the wrong environment?

- Apps are synced to one environment. The [**`INNGEST_SIGNING_KEY`**](/docs/platform/signing-keys) ensures that your app is synced within the correct Inngest environment. Verify that you assigned your signing key to the right `INNGEST_SIGNING_KEY` environment variable in your hosting provider or **`.env`** file locally.

Why do I have duplicated apps?

- Each app ID is considered a persistent identifier. [Since the app ID is determined by the ID passed to the serve handler from the Inngest client](/docs/apps#apps-in-sdk), changing that ID will result in Inngest not recognizing the app ID during the next sync. As a result, Inngest will create a new app.

Why is my sync inside unattached syncs?

- Failures in automatic syncs may not be immediately visible. In such cases, an unattached sync (a sync without an app) containing the failure message is created.

Why don’t I see my sync in the sync list?

If you're experiencing difficulties with syncing and cannot locate your sync in the sync list, consider the following scenarios: 1. **Different App ID:** - If you resync the app after modifying the [app ID](/docs/reference/typescript/v4/client/create), a new app is created, not a new sync within the existing app. - Solution: Confirm the creation of a new app when changing the app ID. 2. **Syncing Errors:** - *Manual Syncs and Manual Resyncs:* - Sync failures during manual operations are immediately displayed, preventing the creation of a new sync. The image below shows an example of an error while manually syncing: [IMAGE] - Solution: Review the displayed error message and address the syncing issue accordingly. - *Automatic Syncs (such as Vercel Integration):* - Failures in automatic syncs may not be immediately visible. In such cases, an unattached sync (a sync without an app) containing the failure message is created. [IMAGE] - Solution: Check for unattached syncs and address the issues outlined in the failure message. The image below shows the location of unattached syncs in Inngest Cloud: [IMAGE]
# Inngest Apps Source: https://www.inngest.com/docs/apps/index Description: Understand Inngest apps: how they map to your codebase, connect to environments, sync after deploys, and use event keys to send and receive events. metaTitle = "Inngest Apps | Environments, Syncing & Event Keys" In Inngest, apps map directly to your projects or services. When you serve your functions using our serve API handler, you are hosting a new Inngest app. With Inngest apps, your dashboard reflects your code organization better. It's important to note that apps are synced to one environment. You can sync any number of apps to one single environment using different Inngest Clients. The diagram below shows how each environment can have multiple apps which can have multiple functions each: [IMAGE] [IMAGE] ## Apps in SDK Each [`serve()` API handler](/docs/learn/serving-inngest-functions) will generate an app in Inngest upon syncing. The app ID is determined by the ID passed to the serve handler from the Inngest client. For example, the code below will create an Inngest app called “example-app” which contains one function: ```ts {{ title: "Node.js" }} // or your preferred framework new Inngest({ id: "example-app" }); serve({ client: inngest, functions: [sendSignupEmail], }); ``` ```python {{ title: "Python (Flask)" }} import logging import inngest from src.flask import app import inngest.flask logger = logging.getLogger(f"{app.logger.name}.inngest") logger.setLevel(logging.DEBUG) inngest_client = inngest.Inngest(app_id="flask_example", logger=logger) @inngest_client.create_function( fn_id="hello-world", trigger=inngest.TriggerEvent(event="say-hello"), ) def hello(ctx: inngest.ContextSync) -> str: inngest.flask.serve( app, inngest_client, [hello], ) app.run(port=8000) ``` ```python {{ title: "Python (FastAPI)" }} import logging import inngest import fastapi import inngest.fast_api logger = logging.getLogger("uvicorn.inngest") logger.setLevel(logging.DEBUG) inngest_client = inngest.Inngest(app_id="fast_api_example", logger=logger) @inngest_client.create_function( fn_id="hello-world", trigger=inngest.TriggerEvent(event="say-hello"), ) async def hello(ctx: inngest.Context) -> str: return "Hello world!" app = fastapi.FastAPI() inngest.fast_api.serve( app, inngest_client, [hello], ) ``` ```go {{ title: "Go (HTTP)" }} package main import ( "context" "fmt" "net/http" "time" "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) func main() { client, err := inngestgo.NewClient(inngestgo.ClientOpts{ AppID: "sandbox-go", }) if err != nil { panic(err) } f, err := inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "account-created", Name: "Account creation flow", }, // Run on every api/account.created event. inngestgo.EventTrigger("api/account.created", nil), AccountCreated, ) if err != nil { log.Fatal(err) } http.ListenAndServe(":8080", f.Serve()) } ``` Each app ID is considered a persistent identifier. Changing your client ID will result in Inngest not recognizing the app ID during the next sync. As a result, Inngest will create a new app. ## Apps in Inngest Cloud In the image below, you can see the apps page in Inngest Cloud. Check the [Working with Apps Guide](/docs/apps/cloud) for more information about how to sync apps in Inngest Cloud. [IMAGE] ## Apps in Inngest Dev Server In the image below, you can see the apps page in Inngest Dev Server. For more information on how to sync apps in Inngest Dev Server check the [Local Development Guide](/docs/local-development#connecting-apps-to-the-dev-server). [IMAGE] ## Informing Inngest about your apps To integrate your code hosted on another platform with Inngest, you need to inform Inngest about the location of your app and functions. For example, imagine that your `serve()` handler is located at `/api/inngest`, and your domain is `myapp.com`. In this scenario, you will need to sync your app to inform Inngest that your apps and functions are hosted at `https://myapp.com/api/inngest`. To ensure that your functions are up to date, you need to resync your app with Inngest whenever you deploy new function configurations to your hosted platform. Inngest uses the [`INNGEST_SIGNING_KEY`](/docs/platform/signing-keys?ref=deploy) to securely communicate with your application and identify the correct environment to sync your app. ## Next Steps To continue your exploration, feel free to check out: - How to [work with Apps in the Dev Server](/docs/local-development#connecting-apps-to-the-dev-server) - How to [work with Apps in Inngest Cloud](/docs/apps/cloud) # Environment Variables Source: https://www.inngest.com/docs/sdk/environment-variables Description: Environment variables for Inngest SDKs: INNGEST_API_KEY, INNGEST_SIGNING_KEY, INNGEST_EVENT_KEY, INNGEST_BASE_URL, and dev mode settings. metaTitle = "Inngest SDK Environment Variables | Reference" You can set environment variables to change various parts of Inngest's configuration. We'll look at all available environment variables here, what to set them to, and what our recommendations are for their use. - [INNGEST_BASE_URL](#inngest-base-url) - [INNGEST_DEV](#inngest-dev) - [INNGEST_ENV](#inngest-env) - [INNGEST_EVENT_KEY](#inngest-event-key) - [INNGEST_LOG_LEVEL](#inngest-log-level) - [INNGEST_SERVE_ORIGIN](#inngest-serve-origin) - [INNGEST_SERVE_PATH](#inngest-serve-path) - [INNGEST_SIGNING_KEY](#inngest-signing-key) - [INNGEST_STREAMING](#inngest-streaming) Within some frameworks and platforms such as Cloudflare Workers, environment variables are not available in the global scope and are instead passed as runtime arguments to your handler. In this case, you can use `inngest.setEnvVars()` to ensure your client has the correct configuration before communicating with Inngest. ```ts // For example, in Hono on Cloudflare Workers app.on("POST", "/my-api/send-some-event", async (c) => { inngest.setEnvVars(c.env); await inngest.send({ name: "test/event" }); return c.json({ message: "Done!" }); }); // You can also chain the call to be succinct await inngest.setEnvVars(c.env).send({ name: "test/event" }); ``` --- ## INNGEST_BASE_URL Use this to tell an SDK the host to use to communicate with Inngest. If set, it should be the host including the protocol and port, e.g. `http://localhost:8288` or `https://my.tunnel.com`. Can be overwritten by manually specifying `baseUrl` in `new Inngest()`. In most cases we recommend keeping this unset. A common case, though, is wanting to force a production build of your app to use the Inngest Dev Server instead of Inngest Cloud for local integration testing or similar. In this case, prefer using [INNGEST_DEV=1](#inngest-dev). For Docker, it may be appropriate to also set `INNGEST_BASE_URL=http://host.docker.internal:8288`. Learn more in our [Docker guide](/docs/local-development). --- ## INNGEST_DEV Use this to force an SDK to be in Dev Mode with `INNGEST_DEV=1`, or Cloud mode with `INNGEST_DEV=0`. A URL for the dev server can be set at the same time with `INNGEST_DEV=http://localhost:8288`. Can be overwritten by manually specifying `isDev` in `new Inngest()`. Explicitly setting either mode will change the URLs used to communicate with Inngest, as well as turning **off** signature verification in Dev mode, or **on** in Cloud mode. If neither the environment variable nor config option are specified, the SDK defaults to **cloud mode**. For local development, explicitly set `INNGEST_DEV=1` or `isDev: true` in your client configuration. --- ## INNGEST_ENV Use this to tell Inngest which [Inngest Environment](/docs/platform/environments?ref=environment-variables) you're wanting to send and receive events from. Can be overwritten by manually specifying `env` in `new Inngest()`. This is detected and set automatically for some platforms, but others will need manual action. See [Configuring branch environments](/docs/platform/environments#configuring-branch-environments?ref=environment-variables) to see if you need this. --- ## INNGEST_EVENT_KEY The key to use to send events to Inngest. See [Creating an Event Key](/docs/events/creating-an-event-key?ref=environment-variables) for more information. Can be overwritten by manually specifying `eventKey` in `new Inngest()`. --- ## INNGEST_SERVE_ORIGIN The origin used to access this application from Inngest Cloud. If set, it should be the origin including the protocol and port, e.g. `http://localhost:8288` or `https://my.tunnel.com`. Can be overwritten by manually specifying `serveOrigin` in `serve()`. By default, an SDK will try to infer this using request details such as the `Host` header, but sometimes this isn't possible (e.g. when running in a more controlled environment such as AWS Lambda or when dealing with proxies/redirects). --- ## INNGEST_SERVE_PATH The path used to access this application from Inngest Cloud. If set, it should be a valid URL path with a leading `/`, e.g. `/api/inngest`. By default, an SDK will try to infer this using request details, but sometimes this isn't possible (e.g. when running in a more controlled environment such as AWS Lambda or when dealing with proxies/redirects). --- ## INNGEST_SIGNING_KEY The key used to sign requests to and from Inngest to ensure secure communication. See [Serve - Signing Key](/docs/learn/serving-inngest-functions#signing-key?ref=environment-variables) for more information. Can be overwritten by manually specifying `signingKey` in `new Inngest()`. --- ## INNGEST_SIGNING_KEY_FALLBACK Only used during signing key rotation. When it's specified, the SDK will automatically retry signing key auth failures with the fallback key. Available in version `3.18.0` and above. --- ## INNGEST_STREAMING Sets an SDK's streaming support, potentially circumventing restrictive request timeouts and other limitations. See [Streaming](/docs/streaming?ref=environment-variables) for more information. Can be `true` or `false`. By default, this is `false`, disabling streaming. It can also be overwritten by setting `streaming` in `serve()` with the same values. # ESLint Plugin Source: https://www.inngest.com/docs/sdk/eslint Description: Use the Inngest ESLint plugin to catch common mistakes: incorrect step usage, missing await, and patterns that break durable execution in TypeScript functions. metaTitle = "Inngest ESLint Plugin | Catch Common Mistakes" An ESLint plugin is available at [@inngest/eslint-plugin](https://www.npmjs.com/package/@inngest/eslint-plugin), providing rules to enforce best practices when writing Inngest functions. ## Getting started Install the package using whichever package manager you'd prefer as a [dev dependency](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#devdependencies). ```sh npm install -D @inngest/eslint-plugin ``` Add the plugin to your ESLint configuration file with the recommended config. ```json { "plugins": ["@inngest"], "extends": ["plugin:@inngest/recommended"] } ``` You can also manually configure each rule instead of using the `plugin:@inngest/recommend` config. ```json { "plugins": ["@inngest"], "rules": { "@inngest/await-inngest-send": "warn" } } ``` See below for a list of all rules available to configure. ## Rules - [@inngest/await-inngest-send](#inngest-await-inngest-send) - [@inngest/no-nested-steps](#inngest-no-nested-steps) - [@inngest/no-variable-mutation-in-step](#inngest-no-variable-mutation-in-step) ### @inngest/await-inngest-send You should use `await` or `return` before `inngest.send(). ```json "@inngest/await-inngest-send": "warn" // recommended ``` In serverless environments, it's common that runtimes are forcibly killed once a request handler has resolved, meaning any pending promises that are not performed before that handler ends may be cancelled. ```ts // ❌ Bad inngest.send({ name: "some.event" }); ``` ```ts // ✅ Good await inngest.send({ name: "some.event" }); ``` #### When not to use it There are cases where you have deeper control of the runtime or when you'll safely `await` the send at a later time, in which case it's okay to turn this rule off. ### @inngest/no-nested-steps Use of `step.*` within a `step.run()` function is not allowed. ```json "@inngest/no-nested-steps": "error" // recommended ``` Nesting `step.run()` calls is not supported and will result in an error at runtime. If your steps are nested, they're probably reliant on each other in some way. If this is the case, extract them into a separate function that runs them in sequence instead. ```ts // ❌ Bad await step.run("a", async () => { "..."; await step.run("b", () => { return use(someValue); }); }); ``` ```ts // ✅ Good async () => { await step.run("a", async () => { return "..."; }); return step.run("b", async () => { return use(someValue); }); }; await aThenB(); ``` ### @inngest/no-variable-mutation-in-step Do not mutate variables inside `step.run()`, return the result instead. ```json "@inngest/no-variable-mutation-in-step": "error" // recommended ``` Inngest executes your function multiple times over the course of a single run, memoizing state as it goes. This means that code within calls to `step.run()` is not called on every execution. This can be confusing if you're using steps to update variables within the function's closure, like so: ```ts // ❌ Bad // THIS IS WRONG! step.run only runs once and is skipped for future // steps, so userID will not be defined. let userId; // Do NOT do this! Instead, return data from step.run. await step.run("get-user", async () => { userId = await getRandomUserId(); }); console.log(userId); // undefined ``` Instead, make sure that any variables needed for the overall function are _returned_ from calls to `step.run()`. ```ts // ✅ Good // This is the right way to set variables within step.run :) await step.run("get-user", () => getRandomUserId()); console.log(userId); // 123 ``` # Installing the SDK Source: https://www.inngest.com/docs/sdk/overview The Inngest SDK allows you to write reliable, durable functions in your existing projects incrementally. Functions can be automatically triggered by events or run on a schedule without infrastructure, and can be fully serverless or added to your existing HTTP server. - It works with any framework and platform by using HTTP to call your functions - It supports serverless providers, without any additional infrastructure - It fully supports TypeScript out of the box - You can locally test your code without any extra setup ## Getting started ### Installation To get started, install the SDK via your favorite package manager: ```shell {{ title: "npm" }} npm install inngest ``` ```shell {{ title: "yarn" }} yarn add inngest ``` ```shell {{ title: "pnpm" }} pnpm add inngest ``` ```shell {{ title: "bun" }} bun add inngest ``` ### Setup Once Inngest is installed, create an Inngest client, later used to define your functions and trigger events: ```typescript inngest = new Inngest({ id: "my-app", }); ``` To get started, install the SDK via `go get`: ```shell go get github.com/inngest/inngestgo ``` ### Installation To get started, install the SDK via `pip`: ```shell pip install inngest ``` ### Setup Once Inngest is installed, create an Inngest client, later used to define your functions and trigger events: ```python import inngest import logging # Create an Inngest client inngest_client = inngest.Inngest( app_id="app_example", logger=logging.getLogger("uvicorn"), ) ``` Your project is now ready to start writing Inngest functions: 1. [Define and write your functions](/docs/learn/inngest-functions) 2. [Trigger functions with events](/docs/events) 3. [Set up and serve the Inngest API for your framework](/docs/learn/serving-inngest-functions) # Checkpointing Source: https://www.inngest.com/docs/setup/checkpointing Description: Learn how Inngest checkpointing saves function execution state between steps so runs can recover from failures without re-executing completed work. metaTitle = "Checkpointing" Checkpointing is a performance optimization for Inngest functions that executes steps eagerly rather than waiting on internal orchestration. The result is dramatically lower latency — ideal for real-time AI workflows. In v4 of the TypeScript SDK, checkpointing is **enabled by default**. You only need to configure it if you want to customize options like `maxRuntime` or disable it. ## Minimum Requirements ### Language - **TypeScript**: SDK `3.51.0` or higher (enabled by default in v4). - **Go**: SDK version `v0.15.0`. ## Getting Started In v4, checkpointing is enabled by default. No configuration is required for long-running or always-on servers. ### Max runtime for serverless environments If your app is deployed to serverless platforms like [Vercel](/docs/deploy/vercel), you should configure the `maxRuntime` option to slightly below your function's maximum duration. See [configuration](#configuration) for more information. ```ts new Inngest({ id: 'my-app', checkpointing: { maxRuntime: '50s', // 50s might be a good option if your max duration is 60s } }) ``` Tip: Many platforms, like Vercel, allow you to configure the maximum duration per function, e.g. on your `/api/inngest` endpoint. Learn more about configuring on Vercel [here](https://vercel.com/docs/functions/configuring-functions/duration). ### Disabling checkpointing To disable checkpointing for all functions, set `checkpointing: false` on the client: ```ts inngest = new Inngest({ id: "my-app", checkpointing: false, }); ``` You can also disable it per-function: ```ts myFunction = inngest.createFunction( { id: "my-function", checkpointing: false, }, async ({ step }) => { // steps here will use standard orchestration } ); ``` ### Configuration Configure how checkpointing behaves with these options: * `maxRuntime` - default: `0` (unlimited): The maximum amount of time the function should continuously execute and checkpoint steps before returning the request response. Configure this to be slightly less than the maximum allowed request timeout for your platform or server. For example, if your platform allows `900s`, you might set `maxRuntime` to `800s`. * `bufferedSteps` - default: `1` (no buffering): The number of steps to buffer together before checkpointing. This can help reduce the number of requests made to Inngest when running many steps in sequence. Consider that buffered steps that are not checkpointed may be lost if your server is not gracefully terminated. * `maxInterval`: The maximum interval to wait before checkpointing, even if the `bufferedSteps` count has not been reached. ```ts {{ title: "Example configuration" }} checkpointing: { maxRuntime: '300s', bufferedSteps: 2, maxInterval: "10s", } ``` To enable checkpointing: 1. Install the checkpoint package: ```shell go get github.com/inngest/inngestgo/pkg/checkpoint ``` 2. Set `Checkpoint` on your function options: ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/pkg/checkpoint" ) _, err := inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ ID: "my-function", Name: "My Function", Checkpoint: checkpoint.ConfigSafe, }, // ... triggers and handler ) ``` ## How Does It Work? The [Inngest default execution model](/docs/learn/how-functions-are-executed) is a complete handoff to the Inngest Platform, where an HTTP request is performed to store the execution state upon each step completion, leading to inter-step latency. ![With and Without Checkpointing](/assets/docs/checkpointing/checkpointing_with_without.png) Checkpointing uses the SDK orchestrates steps on the client-side (_on your server_) and executes them immediately. As steps complete, checkpoint messages are sent to Inngest to track progress. The result is dramatically lower latency — ideal for real-time AI workflows. ![Inngest Workflow Execution](/assets/docs/checkpointing/checkpointing_inngest_workflow.jpg) ### Failures and Retries What happens when something goes wrong? If a step fails and needs to retry, the execution engine falls back to standard orchestration to handle it properly. You get speed when things work, and safety when they don't. ## Notes and limitations - **Parallel step execution** — When a function branches into parallel steps, execution switches to standard orchestration. In most cases, when the executor signals that it is finished with parallelization, the SDK is able to switch back into checkpointing mode. The deprecated `optimizeParallelism: false` configuration option prevents this, so if `optimizeParallelism` is set to false, checkpointing does _not_ resume after parallel execution. | Feature | Supported | |---------|-----------| | Local development | ✅ | | Self-hosted Inngest | ✅ | | Inngest Cloud | ✅ | # Connect (workers) Source: https://www.inngest.com/docs/setup/connect Description: Use connect() to create persistent outbound connections from workers to Inngest. Lowest-latency option with elastic scaling and no open inbound ports required. metaTitle = "Connect | Persistent Worker Connections for Inngest" The `connect` API allows your app to create an outbound persistent connection to Inngest. Each app can establish multiple connections to Inngest, which enable you to scale horizontally across multiple workers. The key benefits of using `connect` compared to [`serve`](/docs/learn/serving-inngest-functions) are: - **Lowest latency** - Persistent connections enable the lowest latency between your app and Inngest. - **Elastic horizontal scaling** - Easily add more capacity by running additional workers. - **Ideal for container runtimes** - Deploy on Kubernetes or ECS without the need of a load balancer for inbound traffic - **Simpler long running steps** - Step execution is not bound by platform http timeouts. - **Automatic syncing** - Connect apps automatically sync your functions with Inngest when a worker connects. No manual app syncing is required. The number of concurrent worker connections available depends on your plan. For new plans, the limits are: * Free plan: 3 concurrent worker connections * All paid plans: 20 concurrent worker connections * Max apps per connection: 10 You can find your plan's connection limits on your [billing page](https://app.inngest.com/billing). ## Minimum requirements ### Language - **TypeScript**: SDK `3.34.1` or higher. - **Go**: SDK `0.11.2` or higher. - **Python**: SDK `0.5.0` or higher. - Install the SDK with `pip install inngest[connect]` since there are additional dependencies required. - We also recommend the following constraints: - `protobuf>=5.29.4,<6.0.0` - `psutil>=6.0.0,<7.0.0` - `websockets>=15.0.0,<16.0.0` ### Runtime You must use a long running server (Render, Fly.io, Kubernetes, etc.). Serverless runtimes (AWS Lambda, Vercel, etc.) are not supported. If using TypeScript, your runtime must support built-in WebSocket support (Node `22.4.0` or higher, Deno `1.4` or higher, Bun `1.1` or higher). ## Getting started Using `connect` with your app is simple. Using each SDK's "connect" method only requires a list of functions that are available to be executed. Here is a one-file example of a fully-functioning app that connects to Inngest. ```ts new Inngest({ id: "my-app", }); inngest.createFunction( { id: "handle-signup", triggers: [{ event: "user.created" }] }, async ({ event, step }) => { console.log("Function called", event); }, ); (async () => { await connect({ apps: [{ client: inngest, functions: [handleSignupFunction] }], }); console.log("Worker: connected", connection); })(); ``` ```go type UserCreatedEvent struct { Name string Data struct { UserID string `json:"user_id"` } } func main() { ctx := context.Background() client, err := inngestgo.NewClient(inngestgo.ClientOpts{ AppID: "my-app", Logger: logger.StdlibLogger(ctx), AppVersion: nil, // Optional, defaults to the git commit SHA }) if err != nil { panic(err) } _, err = inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ID: "handle-signup", Name: "Handle signup"}, inngestgo.EventTrigger("user.created", nil), func(ctx context.Context, input inngestgo.Input[UserCreatedEvent]) (any, error) { fmt.Println("Function called") return map[string]any{"success": true}, nil }, ) if err != nil { panic(err) } fmt.Println("Worker: connecting") conn, err := inngestgo.Connect(ctx, inngestgo.ConnectOpts{ InstanceID: inngestgo.Ptr("example-worker"), Apps: []inngestgo.Client{client}, }) if err != nil { fmt.Printf("ERROR: %#v\n", err) os.Exit(1) } defer func(conn connect.WorkerConnection) { <-ctx.Done() err := conn.Close() if err != nil { fmt.Printf("could not close connection: %s\n", err) } }(conn) } ``` ```python import asyncio import inngest from inngest.connect import connect client = inngest.Inngest(app_id="my-app") @client.create_function( fn_id="handle-signup", trigger=inngest.TriggerEvent(event="user.created"), ) async def fn_1(ctx: inngest.Context) -> None: print("Function called") functions = [fn_1] asyncio.run( connect( apps=[(client, functions)], ).start() ) ``` ## How does it work? The `connect` API establishes a persistent WebSocket connection to Inngest. Each connection can handle executing multiple functions and steps concurrently. Each app can create multiple connections to Inngest enabling horizontal scaling. Additionally, connect has the following features: - **Automatic re-connections** - The connection will automatically reconnect if it is closed. - **Graceful shutdown** - The connection will gracefully shutdown when the app receives a signal to terminate (`SIGTERM`). New steps will not be accepted after the connection is closed, and existing steps will be allowed to complete. - **Worker-level maximum concurrency** - Each worker can configure the maximum number of concurrent steps it can handle. This allows Inngest to distribute load across multiple workers and not overload a single worker. See [Worker concurrency](#worker-concurrency) for details. ## Local development During local development, set the `INNGEST_DEV=1` environment variable to enable local development mode. This will cause the SDK to connect to [the Inngest dev server](/docs/local-development). When your worker process is running it will automatically connect to the dev server and sync your functions' configurations. No signing or event keys are required in local development mode. ## Deploying to production To enable your application to securely connect to Inngest, you must set the `INNGEST_SIGNING_KEY` and `INNGEST_EVENT_KEY` environment variables. These keys can be found in the Inngest Dashboard. Learn more about [Event keys](/docs/events/creating-an-event-key) and [Signing Keys](/docs/platform/signing-keys). The `appVersion` is used to identify the version of your app that is connected to Inngest. This allows Inngest to support rolling deploys where multiple versions of your app may be connected to Inngest. When a new version of your app is connected to Inngest, the functions' configurations are synced to Inngest. When a new version is connected, Inngest update the function configuration in your environment and starts routing new function runs to the latest version. You can set the `appVersion` to whatever you want, but we recommend using something that automatically changes with each deploy, like a git commit sha or Docker image tag. ```ts {{ title: "Any platform" }} // You can set the app version to any environment variable, you might use // a build number ('v2025.02.12.01'), git commit sha ('f5a40ff'), or // a custom value ('my-app-v1'). new Inngest({ id: 'my-app', appVersion: process.env.MY_APP_VERSION, // Use any environment variable you choose }) ``` ```ts {{ title: "GitHub Actions" }} // If you're using Github Actions to build your app, you can set the // app version to the GITHUB_SHA environment variable during build time // or inject into the build of a Docker image. new Inngest({ id: 'my-app', appVersion: process.env.GITHUB_SHA, }) ``` ```ts {{ title: "Render" }} // Render includes the RENDER_GIT_COMMIT env var at build and runtime. // https://render.com/docs/environment-variables new Inngest({ id: 'my-app', appVersion: process.env.RENDER_GIT_COMMIT, }) ``` ```ts {{ title: "Fly.io" }} // Fly includes a machine version env var at runtime. // https://fly.io/docs/machines/runtime-environment/ new Inngest({ id: 'my-app', appVersion: process.env.FLY_MACHINE_VERSION, }) ``` The `instanceId` is used to identify the worker instance of your app that is connected to Inngest. This allows Inngest to support multiple instances (workers) of your app connected to Inngest. By default, Inngest will attempt to use the hostname of the worker as the instance id. If you're running your app in a containerized environment, you can set the `instanceId` to the container id. ```ts {{ title: "Any platform" }} // Set the instance ID to any environment variable that is unique to the worker await connect({ apps: [...], instanceId: process.env.MY_CONTAINER_ID, }) ``` ```ts {{ title: "Kubernetes + Docker" }} // instanceId defaults to the HOSTNAME environment variable. // By default, Kubernetes and Docker set the HOSTNAME environment variable to the pod name // so it is automatically set for you. await connect({ apps: [...], // This is what happens under the hood if you don't set instanceId // instanceId: process.env.HOSTNAME, }) ``` ```ts {{ title: "Render" }} // Render includes the RENDER_INSTANCE_ID env var at runtime. // https://render.com/docs/environment-variables await connect({ apps: [...], instanceId: process.env.RENDER_INSTANCE_ID, }) ``` ```ts {{ title: "Fly.io" }} // Fly includes the FLY_MACHINE_ID env var at runtime. // https://fly.io/docs/machines/runtime-environment/ await connect({ apps: [...], instanceId: process.env.FLY_MACHINE_ID, }) ``` The `maxWorkerConcurrency` option is used to limit the number of concurrent steps that can be executed by the worker instance. This allows Inngest to distribute load across multiple workers and not overload a single worker. ```ts await connect({ apps: [...], maxWorkerConcurrency: 10, }) ``` ### Bundling TypeScript and JavaScript workers If you bundle a Node.js application that uses `connect()`, configure your bundler to treat `inngest` and its subpaths, such as `inngest/connect`, as external dependencies. Externalizing keeps the SDK out of the application bundle and loads it from the installed package at runtime. Connect uses a separate worker thread to manage the connection and heartbeats. Application bundlers can omit the worker's runner file or change the path used to locate it. Externalizing the SDK preserves the package layout so the worker can find its runner. For [esbuild](https://esbuild.github.io/api/#external), add `external: ["inngest", "inngest/*"]` to your build options: ```js {{ title: "build.mjs" }} await build({ entryPoints: ["src/index.ts"], bundle: true, platform: "node", format: "cjs", outfile: "dist/index.cjs", external: ["inngest", "inngest/*"], }); ``` Keep `inngest` in your application's `dependencies`, and install or include it and its dependencies in the deployed environment's `node_modules`. For container deployments, these packages must be present in the final runtime image. Deploying only the application bundle is not sufficient when the SDK is externalized. If a bundled application fails with `Cannot find module` pointing to `runner.js`, `runner.mjs`, or `runner.cjs`, check that the SDK is externalized and its installed package is included in your deployment, then rebuild and redeploy. ## Lifecycle As a connect worker is a long-running process, it's important to understand the lifecycle of the worker and how it relates to the deployment of a new version of your app. Here is an overview of the lifecycle of a connect worker and where you can hook into it to handle graceful shutdowns and other lifecycle events. `CONNECTING` - The worker is establishing a connection to Inngest. This starts when `connect()` is called. First, the worker sends a request to the Inngest API via HTTP to get connection information. The response includes the WebSocket gateway URL. The worker then connects to the WebSocket gateway. `ACTIVE` - The worker is connected to Inngest and ready to execute functions. * The new `appVersion` is synced including the latest function configurations. * The worker begins sending and receiving "heartbeat" messages to Inngest to ensure the connection is still active. * The worker will automatically reconnect if the connection is lost. ```ts {{ title: "TypeScript" }} // The connect promise will resolve when the connection is ACTIVE await connect({ apps: [...], }) console.log(`The worker connection is: ${connection.state}`) // The worker connection is: ACTIVE ``` `RECONNECTING` - The worker is reconnecting to Inngest after a connection was lost. The worker will automatically flush any in-flight steps via the HTTP API when the WebSocket connection is lost. By default, the worker will attempt to reconnect to Inngest an infinite number of times. `CLOSING` - The worker is beginning the shutdown process. * New steps will not be accepted after this state is entered. * Existing steps will be allowed to complete. The worker will flush any in-flight steps via the HTTP API after the WebSocket connection is closed. By default, the SDK listens for `SIGTERM` and `SIGINT` signals and begins the shutdown process. You can customize this behavior by in each SDK: ```ts // You can explicitly configure which signals the SDK should // listen for by an array of signals to `handleShutdownSignals`: await connect({ apps: [...], // ex. Only listen for SIGTERM, or pass an empty array to listen to no signals handleShutdownSignals: ['SIGTERM'], }) ``` ```go // The Go SDK must receive a Context object that will be notified // when the correct signals are received. Use signal.NotifyContext: ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() // Later in your function - pass the context to the connect function: ws, err := inngestgo.Connect(ctx, inngestgo.ConnectOpts{ InstanceID: inngestgo.Ptr("example-worker"), Apps: []inngestgo.Client{client}, }) ``` You can manually close the connection with the `close` method on the connection object: ```ts await connection.close() // Connection is now closed ``` `CLOSED` - The worker's WebSocket connection has closed. By this stage, all in-flight steps will be flushed via the HTTP API as the WebSocket connection is closed, ensuring that no in-progress steps are lost. ```ts {{ title: "TypeScript" }} // The `closed` promise will resolve when the connection is "CLOSED" await connection.closed // Connection is now closed ``` **WebSocket connection and HTTP fallback** - While a WebSocket connection is open, the worker will receive and send all step results via the WebSocket connection. When the connection closes, the worker will fallback to the HTTP API to send any remaining step results. ## Worker observability In the Inngest Cloud dashboard, you can view the connection status of each of your workers. At a glance, you can see each worker's instance id, connection status, connected at timestamp, last heartbeat, the app version, and app version. This view is helpful for debugging connection issues or verifying rolling deploys of new app versions. ![App worker observability](/assets/docs/connect/cloud-app-workers.png) ## Syncing and Rollbacks {/* TODO: Create diagram to explain syncing */} Inngest keeps track of the version your workers are running on. This internal representation changes when you update your function configuration, provide a new app version identifier to the client configuration, or change the SDK version or language. When you deploy a new version of your application, the first worker to connect to Inngest will automatically sync your app. This will update function configurations to the desired state configured in your code. Unlike `serve`, connect apps do not require [manual syncing](/docs/apps/cloud) with Inngest Cloud. `connect` supports rolling releases: During a deployment of your app, Inngest will run functions on all connected workers, regardless of the version, as long as they are able to process a request for a given function. This prevents traffic from concentrating on a single instance during rollouts and causing a thundering herd issue. Once all old workers have terminated after a deployment, you can roll back to an old version by bringing back an old worker. Similar to the deployment process, this will update the function configuration to the previous state and gradually allow you to shift traffic to the old version by bringing up more old workers while terminating workers running the newer version. ## Health checks If you are running your app in a containerized environment, we recommend using a health check to ensure that your app is running and ready to accept connections. This is key for graceful rollouts of new app versions. If you are using Kubernetes, we recommend using the `readinessProbe` to check that the app is ready to accept connections. The simplest way to implement a health check is to create an http endpoint that listens for health check requests. As connect is an outbound WebSocket connection, you'll need to create a small http server that listens for health check requests and returns a 200 status code when the connection to Inngest is active. Here is an example of using `connect` with a basic Node.js http server to listen for health check requests and return a 200 status code when the connection to Inngest is active. ```ts {{ title: "Node.js" }} (async () => { await connect({ apps: [{ client: inngest, functions }], }); console.log("Worker: connected", connection); // This is a basic web server that only listens for the /ready endpoint // and returns a 200 status code when the connection to Inngest is active. createServer((req, res) => { if (req.url === "/ready") { if (connection.state === ConnectionState.ACTIVE) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("OK"); } else { res.writeHead(500, { "Content-Type": "text/plain" }); res.end("NOT OK"); } return; } res.writeHead(404, { "Content-Type": "text/plain" }); res.end("NOT FOUND"); }); // Start the server on a port of your choice httpServer.listen(8080, () => { console.log("Worker: HTTP server listening on port 8080"); }); // When the Inngest connection has gracefully closed, // this will resolve and the app will exit. await connection.closed; console.log("Worker: Shut down"); // Stop the HTTP server httpServer.close(); })(); ``` ```ts {{ title: "Bun (JavaScript)" }} await connect({ apps: [{ client: inngest, functions }], }); console.log("Worker: connected", connection); // Start a basic web server that only listens for the /ready endpoint // and returns a 200 status code when the connection to Inngest is active. Bun.serve({ port: 8080, routes: { "/ready": async () => { return connection.state === ConnectionState.ACTIVE ? new Response("OK") : new Response("Not Ready", { status: 500 }); }, }, fetch(req) { return new Response("Not Found", { status: 404 }); }, }); console.log("Worker: HTTP server listening on port 8080"); // When the Inngest connection has gracefully closed, // this will resolve and the app will exit. await connection.closed; console.log("Worker: Shut down"); // Stop the HTTP server await server.stop(); ``` ### Kubernetes readiness probe If you are running your app in Kubernetes, you can use the `readinessProbe` to check that the app is ready to accept connections. For the above example running on port 8080, the readiness probe would look like this: ```yaml readinessProbe: httpGet: path: /ready initialDelaySeconds: 3 periodSeconds: 10 successThreshold: 3 failureThreshold: 3 ``` ## Worker concurrency {/* TODO: Mention minimum supported version for this feature */} Worker concurrency is supported in the following versions of the SDK: - **TypeScript**: SDK `3.45.1` or higher. - **Go**: SDK `0.14.3` or higher. - **Python**: SDK `0.5.12` or higher. Each worker can configure the maximum number of concurrent steps it can execute simultaneously using the `maxWorkerConcurrency` option. This allows you to control resource usage and prevent a single worker from being overwhelmed with too many concurrent requests. By default, there is no limit on concurrent step execution. The worker will accept as many steps as Inngest sends to it. When you set `maxWorkerConcurrency`, Inngest will distribute load across multiple workers based on their available capacity. Workers with available capacity will receive more work, while workers at their limit will not receive additional steps until capacity becomes available. When a worker reaches its `maxWorkerConcurrency` limit, you may see an **"All workers are at capacity"** error in the Inngest dashboard. This is expected behavior — the worker rejects the step and Inngest automatically re-enqueues it, retrying until a worker has available capacity. These capacity errors do **not** count towards your function's [retries](/docs/features/inngest-functions/error-retries/inngest-errors) configuration. Inngest uses the `instanceId` to track the `maxWorkerConcurrency` for each worker. This means that if you have multiple workers with the same `instanceId`, they will share the same `maxWorkerConcurrency` limit. Inngest considers the `maxWorkerConcurrency` from the latest connection request. This means that if you update the `maxWorkerConcurrency` after a worker is connected and create a new connection, the worker's `maxWorkerConcurrency` will be updated to the new limit. **Benefits:** - Prevents worker resource exhaustion (CPU, memory, connections) - Enables predictable resource allocation per worker - Helps with horizontal scaling over both homogeneous and heterogenous workers {/* TODO: Uncomment when we have intelligent load balancing - Allows Inngest to intelligently distribute load across your worker fleet - Helps with horizontal scaling by ensuring even distribution */} ```ts await connect({ apps: [{ client: inngest, functions }], instanceId: "example-worker", maxWorkerConcurrency: 10, // Max 10 concurrent steps on this worker }) ``` ```go conn, err := inngestgo.Connect(ctx, inngestgo.ConnectOpts{ InstanceID: inngestgo.Ptr("example-worker"), MaxWorkerConcurrency: inngestgo.Ptr(int64(10)), // Max 10 concurrent steps Apps: []inngestgo.Client{client}, }) ``` ```python asyncio.run( connect( apps=[(client, functions)], instance_id="example-worker", max_worker_concurrency=10, # Max 10 concurrent steps ).start() ) ``` **Environment variable:** You can also set the maximum worker concurrency via the `INNGEST_CONNECT_MAX_WORKER_CONCURRENCY` environment variable. This is useful for configuring concurrency without changing code. ```bash INNGEST_CONNECT_MAX_WORKER_CONCURRENCY=100 ``` If both the option and environment variable are set, the option takes precedence. **Default behavior:** If `maxWorkerConcurrency` is not set (or set to `0`), there is no limit on concurrent step execution. Inngest will send as many steps as limited by your account's concurrency limit. ### Connection-level concurrency You can set a different `maxWorkerConcurrency` for each connection from the same worker by specifying a unique `instanceId` for each connection. This will allow you to have different `maxWorkerConcurrency` limits for different connections. Due to different `instanceId` value, Inngest will consider each connection as a separate worker and will not share the same `maxWorkerConcurrency` limit. ```ts // Connection 1 with different concurrency limit await connect({ apps: [{ client: inngest, functions }], instanceId: "worker-1-conn-1", maxWorkerConcurrency: 50, }) // Connection 2 with different concurrency limit await connect({ apps: [{ client: inngest, functions }], instanceId: "worker-1-conn-2", maxWorkerConcurrency: 20, }) ``` ```go // Connection 1 with different concurrency limit conn1, err := inngestgo.Connect(ctx, inngestgo.ConnectOpts{ InstanceID: inngestgo.Ptr("worker-1-conn-1"), MaxWorkerConcurrency: inngestgo.Ptr(int64(50)), Apps: []inngestgo.Client{client}, }) // Connection 2 with different concurrency limit conn2, err := inngestgo.Connect(ctx, inngestgo.ConnectOpts{ InstanceID: inngestgo.Ptr("worker-1-conn-2"), MaxWorkerConcurrency: inngestgo.Ptr(int64(20)), Apps: []inngestgo.Client{client}, }) ``` ```python # Connection 1 with different concurrency limit asyncio.run( connect( apps=[(client, functions)], instance_id="worker-1-conn-1", max_worker_concurrency=50, ).start() ) # Connection 2 with different concurrency limit asyncio.run( connect( apps=[(client, functions)], instance_id="worker-1-conn-2", max_worker_concurrency=20, ).start() ) ``` ## Self hosted Inngest If you are [self-hosting](/docs/self-hosting?ref=docs-connect) Inngest, you need to ensure that the Inngest WebSocket gateway is accessible within your network. The Inngest WebSocket gateway is available at port `8289`. Depending on your network configuration, you may need to set the gateway URL that the SDK uses to connect. ```ts await connect({ apps: [...], gatewayUrl: "ws://my-cluster-host:8289/v0/connect", }) ``` {/* TODO: multiple apps in a single worker */} ## Migrating from serve We are working on enabling more fine-grained function and app migrations from existing `serve` apps to `connect`. We recommend setting up a new app for trying out `connect` before migrating your existing `serve` apps. {/* We will support gradually migrating your existing `serve` apps in a future release. */} # Managing concurrency Source: https://www.inngest.com/docs/functions/concurrency Description: Set limits, keys, and scopes to control step-level parallelism across function runs. metaTitle = "Concurrency Configuration Reference" Limit the number of concurrently running steps for your function with the [`concurrency`](/docs/reference/typescript/v4/functions/create#configuration) configuration options. Setting an optional `key` parameter limits the concurrency for each unique value of the expression. **Important:** Concurrency limits _step_ execution, not the total number of function runs. Functions that are sleeping, waiting for events, or paused between steps do **not** count against your concurrency limit. This means you may have many more function runs in progress than your concurrency limit. [Learn more about how concurrency works →](/docs/guides/concurrency#how-concurrency-works) [Read our concurrency guide for more information on concurrency, including how it works and any limits](/docs/guides/concurrency). ```ts {{ title: "Simple" }} export default inngest.createFunction( { id: "sync-contacts", concurrency: { limit: 10, }, } // ... ); ``` ```ts {{ title: "Multiple keys" }} inngest.createFunction( { id: "unique-function-id", concurrency: [ { // Use an account-level concurrency limit for this function, using the // "openai" key as a virtual queue. Any other function which // runs using the same "openai"` key counts towards this limit. scope: "account", key: `"openai"`, // If there are 10 steps executing with the "openai" key, this function's // runs will wait for capacity before executing. limit: 10, }, { // Create another virtual concurrency queue for this function only. This // limits all accounts to a single executing step for this function, based off // of the `event.data.account_id` field. // "fn" is the default scope, so we could omit this field. scope: "fn", key: "event.data.account_id", limit: 1, }, ], triggers: { event: "ai/summary.requested" }, }, async ({ event, step }) => { } ); ``` Setting `concurrency` limits are very useful for: * Handling API rate limits - Limit concurrency to stay within the rate limit quotas that are allowed by a given third party API. * Limiting database operations or connections * Preventing one of your user's accounts from consuming too many resources (see `key`) Alternatively, if you want to limit the number of times that your function runs in a given period, [the `rateLimit` option](/docs/reference/typescript/v4/functions/rate-limit) may be better for your use case. ## Configuration Options to configure concurrency. Specifying a `number` is a shorthand to set the `limit` property. The maximum number of concurrently running steps. A value of `0` or `undefined` is the equivalent of not setting a limit. The maximum value is dictated by your account's plan. The scope for the concurrency limit, which impacts whether concurrency is managed on an individual function, across an environment, or across your entire account. * `fn` (default): only the runs of this function affects the concurrency limit * `env`: all runs within the same environment that share the same evaluated key value will affect the concurrency limit. This requires setting a `key` which evaluates to a virtual queue name. * `account`: every run that shares the same evaluated key value will affect the concurrency limit, across every environment. This requires setting a `key` which evaluates to a virtual queue name. An expression which evaluates to a string given the triggering event. The string returned from the expression is used as the concurrency queue name. A key is required when setting an `env` or `account` level scope. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Limit concurrency to `n` (via `limit`) per customer id: `'event.data.customer_id'` * Limit concurrency to `n` per user, per import id: `'event.data.user_id + "-" + event.data.import_id'` * Limit globally using a specific string: `'"global-quoted-key"'` (wrapped in quotes, as the expression is evaluated as a language) **Step-level concurrency:** The concurrency option controls the number of concurrent _steps_ that can be running at any one time, not the number of function runs. Because function runs frequently pause (for sleeps, waiting for events, or between steps), it's common to have many more function runs in progress than your concurrency limit. However, only the configured number of steps will ever be _actively executing_ at once. For example, with `concurrency: 10`, you might have 500 function runs in progress, but only 10 steps are executing code at any given moment. # Referencing functions Source: https://www.inngest.com/docs/functions/references Description: Reference other Inngest functions by ID using inngest.getFunction() to invoke them directly or retrieve metadata without hard-coding function identifiers. metaTitle = "Referencing Inngest Functions | inngest.getFunction()" Using [`step.invoke()`](/docs/reference/typescript/v4/functions/step-invoke), you can directly call one Inngest function from another and handle the result. You can use this with `referenceFunction` to call Inngest functions located in other apps, or to avoid importing dependencies of functions within the same app. ```ts // @/inngest/compute.ts // Create a local reference to a function without importing dependencies computePi = referenceFunction({ functionId: "compute-pi", }); // Create a reference to a function in another application computeSquare = referenceFunction({ appId: "my-python-app", functionId: "compute-square", // Schemas are optional, but provide types for your call if specified schemas: { data: z.object({ number: z.number(), }), return: z.object({ result: z.number(), }), }, }); ``` ```ts // @/inngest/someFn.ts // import the referenece // square.result is typed as a number await step.invoke("compute-square-value", { function: computeSquare, data: { number: 4 }, // input data is typed, requiring input if it's needed }); ``` ## How to use `referenceFunction` The simplest reference just contains a `functionId`. When used, this will invoke the function with the given ID in the same app that is used to invoke it. The input and output types are `unknown`. ```ts await step.invoke("start-process", { function: referenceFunction({ functionId: "some-fn", }), }); ``` If referencing a function in a different application, specify an `appId` too: ```ts await step.invoke("start-process", { function: referenceFunction({ functionId: "some-fn", appId: "some-app", }), }); ``` You can optionally provide `schemas`, which are a collection of [Standard Schemas](https://standardschema.dev/) used to provide typing to the input and output of the referenced function. In the future, this will also _validate_ the input and output. ```ts await step.invoke("start-process", { function: referenceFunction({ functionId: "some-fn", appId: "some-app", schemas: { data: z.object({ foo: z.string(), }), return: z.object({ success: z.boolean(), }), }, }), }); ``` Even if functions are within the same app, this can also be used to avoid importing the dependencies of one function into another, which is useful for frameworks like Next.js where edge and serverless logic can be colocated but require different dependencies. ```ts // import only the type await step.invoke("start-process", { function: referenceFunction({ functionId: "some-fn", }), }); ``` ## Configuration The ID of the function to reference. This can be either a local function ID or the ID of a function that exists in another app. If the latter, `appId` must also be provided. If `appId` is not provided, the function ID will be assumed to be a local function ID (the app ID of the calling app will be used). The ID of the app that the function belongs to. This is only required if the function being refenced exists in another app. The schemas of the referenced function, providing typing to the input `data` and `return` of invoking the referenced function. If not provided and a local function type is not being passed as a generic into `referenceFunction()`, the schemas will be inferred as `unknown`. The [Standard Schema](https://standardschema.dev/) to use to provide typing to the `data` payload required by the referenced function. The [Standard Schema](https://standardschema.dev/) to use to provide typing to the return value of the referenced function when invoked. # Inngest usage limits Source: https://www.inngest.com/docs/usage-limits/inngest Description: Inngest platform limits by plan: max concurrency, event size, step count, function run timeout, log retention, and event history. metaTitle = "Inngest Usage Limits | Runs, Events & Retention" structuredData = { "@type": "FAQPage", mainEntity: [ { "@type": "Question", name: "How long can an Inngest function sleep?", acceptedAnswer: { "@type": "Answer", text: "Inngest supports sleeps up to one year. Free plan sleeps are limited to up to seven days.", }, }, { "@type": "Question", name: "How many steps can an Inngest function have?", acceptedAnswer: { "@type": "Answer", text: "The maximum number of steps allowed per function is 1000.", }, }, { "@type": "Question", name: "What is the default event payload size limit?", acceptedAnswer: { "@type": "Answer", text: "The default event payload size on the Free Tier is 256KB and can be upgraded to 3MB.", }, }, { "@type": "Question", name: "How many events can I send in one request?", acceptedAnswer: { "@type": "Answer", text: "You can send a maximum of 5000 events in one request.", }, }, ], }; Inngest usage limits keep function runs, steps, events, and history predictable for every account. Use these tables to check plan limits, rate-limit related controls, event payload limits, retention windows, and hard platform limits. If you're looking for function-level rate limiting, use the [rate limiting guide](/docs/guides/rate-limiting?ref=docs-usage-limits) or [throttling guide](/docs/guides/throttling?ref=docs-usage-limits). Some of these limits are customizable, so if you need more than what the current limits provide, please [contact us][contact] and we can update the limits for you. ## Plan limits | Limit | Free | Basic | Pro | Enterprise | | --- | --- | --- | --- | --- | | Max concurrent steps | 5 | 25 | 200+ | Custom | | Single event size | 256KiB | 512KiB | 3MiB | Custom | | Trace and log history | 24 hours | 7 days | 14 days | 90 days | | Event lookback period | 1 hour | 1 hour | 3 days | Custom | | Maximum function run length | 30 days | 90 days | 366 days | Custom | See the [pricing page](/pricing?ref=docs-usage-limits) for plan packaging, included usage, and paid upgrade details. ## Platform limits | Limit | Current value | Notes | | --- | --- | --- | | Step sleep duration | Up to 1 year | Free plan supports sleeps up to 7 days. | | Step timeout | Up to 2 hours | Also depends on your hosting provider's timeout. | | Step-returned payload size | 4MiB | Applies to data returned by a step. | | Function run state size | 32MiB | Includes event data, step data, function return data, and internal metadata. | | Steps per function | 1000 | Loops that create one step per item can hit this quickly. | | Event name length | 256 characters | Applies to the event `name`. | | Events per request | 5000 | Applies to `step.sendEvent(events)` and `inngest.send(events)`. | | Batch size | 10MiB | Hard cap regardless of `timeout` or `maxSize`. | ## Functions The following applies to `step` usage. ### Sleep duration Sleep (with `step.sleep()` and `step.sleepUntil()`) up to a year, and for free plan up to seven days. Check the [pricing page](/pricing?ref=docs-usage-limits) for more information. ### Timeout Each step has a timeout depending on the hosting provider of your choice ([see more info][provider-docs]), but Inngest supports up to `2 hours` at the maximum. ### Concurrency Upgradable Check your concurrency limits on the [billing page](https://app.inngest.com/billing). See the [pricing page](/pricing?ref=docs-usage-limits) for more info about the concurrency limits in all plans. ### Payload Size The limit for data returned by a step is `4MB`. ### Function run state size Function run state cannot exceed `32MB`. Its state includes: - Event data (multiple events if using batching) - Step-returned data - Function-returned data - Internal metadata (_small - around a few bytes_) ### Number of Steps per Function The maximum number of steps allowed per function is `1000`. ⚠️ This limit is easily reached if you're using `step` on each item in a loop. Instead we recommend one or both of the following: - Process the loop within a `step` and return that data - Utilize the [fan out][fanout-guide] feature to process each item in a separate function ## Events ### Name length The maximum length allowed for an event name is `256` characters. ### Request Body Size Upgradable The maximum event payload size is dependent on your billing plan. The Free plan supports `256KiB`, Basic supports `512KiB`, Pro supports `3MiB`, and Enterprise plans can use custom limits. See [the pricing page](/pricing?ref=docs-usage-limits) for additional detail. ### Number of events per request Customizable Maximum number of events you can send in one request is `5000`. If you're doing fan out, you'll need to be aware of this limitation when you run `step.sendEvent(events)`. ```ts {{ title: "TypeScript" }} // this `events` list will need to be <= 5000 [{name: "", data: {}}, ...]; await step.sendEvent("send-example-events", events); // or await inngest.send(events); ``` ```go {{ title: "Go" }} // this `events` list will need to be <= 5000 events := []inngestgo.Event{{Name: "", Data: {}}} ids, err := client.SendMany(ctx, events) ``` ```python {{ title: "Python" }} # this `events` list will need to be <= 5000 events = [{'name': '', 'data': {}}, ...] await step.send_event('send-example-events', events) # or await inngest.send(events) ``` ### Batch size The hard limit of a batch size is 10 MiB regardless of the `timeout` or `maxSize` limit. Meaning the batch will be started if that limit is crossed even if the batch is not full or has not reached the timeout duration configured. [provider-docs]: /docs/usage-limits/providers [fanout-guide]: /docs/guides/fan-out-jobs [contact]: /contact # Providers' Usage Limits Source: https://www.inngest.com/docs/usage-limits/providers Description: Understand how cloud provider compute limits for Vercel, Netlify, Cloudflare, and Render affect Inngest function execution time, memory, and request sizes. metaTitle = "Cloud Provider Usage Limits" As your functions' code runs on the hosting provider of your choice, you will be subject to provider or billing plan limits separate from [Inngest's own limits](/docs/usage-limits/inngest). Here are the known usage limits for each provider we support based on their documentation. | | Payload size | Concurrency | Timeout | |-----------------------------------------|:--------------|---------------------|----------------------------| | [AWS Lambda][aws-quota] | 6MB - 20MB | 1000 | 15m | | [Google Cloud Functions][gcp-quota] | 512KB - 32MB | 3000 (1st gen only) | 10m - 60m | | [Cloudflare Workers][cf-workers-limits] | 100MB - 500MB | 100 - 500 | [N/A][cf-workers-duration] | | [Vercel][vercel-limits] | 4MB - 4.5MB | 1000 | 10s - 900s, N/A (Edge Fn) | | [Netlify][netlify-limits] | 256KB - 6MB | Undocumented | 10s - 15m | | [DigitalOcean][digitalocean-limits] | 1MB | 120 | 15m | | Fly.io | Undocumented | [User configured][flyio-limits] | Undocumented | For more details tailored to your plan, please check each provider's website. [aws-quota]: https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html [gcp-quota]: https://cloud.google.com/functions/quotas [cf-workers-limits]: https://developers.cloudflare.com/workers/platform/limits/ [cf-workers-duration]: https://developers.cloudflare.com/workers/platform/limits/#worker-limits [vercel-limits]: https://vercel.com/docs/concepts/limits/overview [netlify-limits]: https://docs.netlify.com/functions/overview/#default-deployment-options [digitalocean-limits]: https://docs.digitalocean.com/products/functions/details/limits/ [flyio-limits]: https://fly.io/docs/reference/configuration/#http_service-concurrency # Create the Inngest Client Source: https://www.inngest.com/docs/reference/typescript/v4/client/create Description: Configure your app ID, event schemas, signing key, logger, and middleware for the Inngest client instance. metaTitle = "Create the Inngest Client | TypeScript SDK v4 Reference" The `Inngest` client object is used to configure your application, enabling you to create functions and send events. ```ts new Inngest({ id: "my-application", }); ``` --- ## Configuration A unique identifier for your application. We recommend a hyphenated slug. Override the default (`https://inn.gs/`) base URL for sending events. See also the [`INNGEST_BASE_URL`](/docs/sdk/environment-variables#inngest-base-url) environment variable. The environment name. Required only when using [Branch Environments](/docs/platform/environments). An Inngest [Event Key](/docs/events/creating-an-event-key). Alternatively, set the [`INNGEST_EVENT_KEY`](/docs/sdk/environment-variables#inngest-event-key) environment variable. Override the default [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) implementation. Defaults to the runtime's native Fetch API. If you need to specify this, make sure that you preserve the function's [binding](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind), either by using `.bind` or by wrapping it in an anonymous function. The signing key used to authenticate with Inngest Cloud. We recommend setting this via the [`INNGEST_SIGNING_KEY`](/docs/sdk/environment-variables#inngest-signing-key) environment variable instead. A fallback signing key, useful during key rotation. We recommend setting this via the [`INNGEST_SIGNING_KEY_FALLBACK`](/docs/sdk/environment-variables#inngest-signing-key-fallback) environment variable instead. Set to `true` to force Dev mode, setting default local URLs and turning off signature verification, or force Cloud mode with `false`. Alternatively, set [`INNGEST_DEV`](/docs/sdk/environment-variables#inngest-dev). A logger object that provides `.info()`, `.warn()`, `.error()`, and `.debug()` methods. The SDK uses Pino-style object-first logging. For string-first loggers like Winston, use `wrapStringFirstLogger`. Defaults to `new ConsoleLogger({ level: "info" })` if not provided. See the [logging reference](/docs/reference/typescript/v4/logging) for details. ```ts inngest = new Inngest({ id: "my-app", logger: new ConsoleLogger({ level: "debug" }), }); ``` A separate logger for SDK internal messages (registration, request handling, middleware errors). If not provided, falls back to `logger`. Use this to route SDK internals separately from your function logs. See the [logging reference](/docs/reference/typescript/v4/logging#internal-logger) for details. A stack of [middleware](/docs/features/middleware) to add to the client. Automatically extract AI metadata from OpenTelemetry spans with OpenTelemetry GenAI attributes. Metadata is attached to its step. Defaults to `true`. Set this to `false` to opt out of the SDK's built-in AI metadata extraction. This does not affect [Extended Traces](/docs/reference/typescript/v4/extended-traces). Whether events created during a run — via `inngest.send()`, `step.sendEvent()`, `step.invoke()`, and `defer()` — inherit the run's [sessions](/docs/features/events-triggers/sessions?ref=docs-reference-client-create), so the resulting child runs stay grouped with their parent. Defaults to `true`. Set this to `false` to opt out. Sessions you set explicitly in `meta.sessions` are still sent. See [disabling propagation](/docs/features/events-triggers/sessions?ref=docs-reference-client-create#disabling-propagation). An endpoint adapter that enables [Durable Endpoints](/docs/reference/typescript/v4/durable-endpoints). When provided, `inngest.endpoint()` and `inngest.endpointProxy()` become available. We recommend setting the [`INNGEST_EVENT_KEY`](/docs/sdk/environment-variables#inngest-event-key) as an environment variable over using the `eventKey` option. As with any secret, it's not a good practice to hard-code the event key in your codebase. ## Cloud Mode and Dev Mode An SDK can run in two separate "modes:" **Cloud** or **Dev**. - **Cloud Mode** - 🔒 Signature verification **ON** - Defaults to communicating with Inngest Cloud (e.g. `https://api.inngest.com`) - **Dev Mode** - ❌ Signature verification **OFF** - Defaults to communicating with an Inngest Dev Server (e.g. `http://localhost:8288`) You can force either Dev or Cloud Mode by setting [`INNGEST_DEV`](/docs/sdk/environment-variables#inngest-dev) or the [`isDev`](#configuration) option. If no mode is explicitly set, the SDK will default to **cloud mode** to ensure that Inngest applications are more secure by default. You can use `INNGEST_DEV` or `isDev=true` to let the SDK know that you are intentionally using development mode. ## Best Practices ### Share your client across your codebase Instantiating the `Inngest` client in a single file and sharing it across your codebase is ideal as you only need a single place to configure your client and define types which can be leveraged anywhere you send events or create functions. ```ts inngest = new Inngest({ id: "my-app" }); ``` ```ts {{ filename: './inngest/myFunction.ts' }} export default inngest.createFunction(...); ``` ### Handling multiple environments with middleware If your client uses middleware, that middleware may import dependencies that are not supported across multiple environments such as "Edge" and "Serverless" (commonly with either access to WebAPIs or Node). In this case, we'd recommend creating a separate client for each environment, ensuring Node-compatible middleware is only used in Node-compatible environments and vice versa. This need is common in places where function execution should declare more involved middleware, while sending events from the edge often requires much less. ```ts // inngest/client.ts inngest = new Inngest({ id: "my-app", middleware: [nodeMiddleware], }); // inngest/edgeClient.ts inngest = new Inngest({ id: "my-app-edge", }); ``` Also see [Referencing functions](/docs/functions/references), which can help you invoke functions across these environments without pulling in any dependencies. # Durable Endpoints Source: https://www.inngest.com/docs/reference/typescript/v4/durable-endpoints Description: Turn HTTP handlers into durable, checkpointed workflows with automatic retries and streaming support. metaTitle = "Durable Endpoints | TypeScript SDK v4 Reference" Create durable HTTP endpoints using `inngest.endpoint()`. Each step within the handler is checkpointed, allowing automatic recovery from failures. ```ts new Inngest({ id: "my-app", endpointAdapter }); handler = inngest.endpoint(async (req: Request): Promise => { await step.run("fetch-data", async () => { return await fetchExternalAPI(); }); return Response.json({ data }); }); ``` --- ## Setup ### `endpointAdapter` The `endpointAdapter` must be passed to the Inngest client constructor to enable Durable Endpoints. Import it from the entry point matching your runtime: ```ts {{ title: "Edge" }} new Inngest({ id: "my-app", endpointAdapter, }); ``` ```ts {{ title: "Node.js" }} new Inngest({ id: "my-app", endpointAdapter, }); ``` The `endpointAdapter` is required. Without it, `inngest.endpoint()` will not be available. ### `endpointAdapter.withOptions(options)` Use `withOptions()` to customize adapter behavior: ```ts new Inngest({ id: "my-app", endpointAdapter: endpointAdapter.withOptions({ asyncRedirectUrl: "/api/inngest/poll", retries: 5, }), }); ``` Custom URL to redirect to when transitioning from sync to async mode. A string path is resolved relative to the request origin and automatically appends `runId` and `token` query parameters. A function gives you full control over URL construction. Override the auto-detected function ID for this endpoint. Defaults to `{METHOD} {path}`. Maximum retries for all steps in this endpoint. Must be between `0` and `20`. Defaults to `3`. Response type when transitioning from sync to async mode. Defaults to `"redirect"`. --- ## `inngest.endpoint(handler): Handler` Creates a durable endpoint handler that can use step primitives for checkpointing. A handler function compatible with the framework you're using. This would be the usual function you use for a request handler before wrapping in `inngest.endpoint()`. Within this handler, you can use all step primitives (`step.run()`, `step.sleep()`, `step.waitForEvent()`) for durable execution. **Returns:** A request handler for your framework. ```ts handler = inngest.endpoint( async (req: Request): Promise => { new URL(req.url); url.searchParams.get("id"); await step.run("process", async () => { return await processItem(id); }); return Response.json({ result }); } ); ``` --- ## Available Step Methods Within a Durable Endpoint handler, you have access to all step methods: see [Available Step Methods](/docs/learn/inngest-steps#available-step-methods). For example: ### `step.run(id, fn)` Execute and checkpoint a function. If the endpoint is retried, completed steps return their cached result instantly. ```ts await step.run("fetch-user", async () => { return await db.users.findOne({ id: userId }); }); ``` See [step.run() reference](/docs/reference/typescript/v4/functions/step-run) for full documentation. ### `step.sleep(id, duration)` Pause execution for a specified duration. The endpoint will be resumed after the sleep completes. ```ts await step.sleep("rate-limit-pause", "30s"); ``` See [step.sleep() reference](/docs/reference/typescript/v4/functions/step-sleep) for full documentation. ### `step.waitForEvent(id, options)` Wait for an external event before continuing. Useful for human-in-the-loop workflows. ```ts await step.waitForEvent("wait-for-approval", { event: "approval/received", match: "data.requestId", timeout: "24h", }); ``` See [step.waitForEvent() reference](/docs/reference/typescript/v4/functions/step-wait-for-event) for full documentation. --- ## Passing Data to Endpoints **POST body is not yet supported.** Use query string parameters to pass data to Durable Endpoints. POST body support is coming soon. ```ts handler = inngest.endpoint(async (req: Request): Promise => { new URL(req.url); // Read data from query parameters url.searchParams.get("userId"); url.searchParams.get("action"); // Process with durable steps await step.run("process", async () => { return await processAction(userId, action); }); return Response.json({ result }); }); ``` --- ## Returning Responses However you return data in your framework is compatible with Durable Endpoints. For example, using a regular Web API request: ```ts // JSON response return Response.json({ success: true, data: result }); // Text response return new Response("OK", { status: 200 }); // Error response return new Response(JSON.stringify({ error: "Not found" }), { status: 404, headers: { "Content-Type": "application/json" }, }); ``` --- ## Error Handling Errors thrown within `step.run()` will trigger automatic retries. Use standard try/catch for custom error handling: ```ts handler = inngest.endpoint(async (req: Request): Promise => { try { await step.run("risky-operation", async () => { return await riskyAPICall(); }); return Response.json({ result }); } catch (error) { // All retries exhausted, handle gracefully return Response.json( { error: "Operation failed after retries" }, { status: 500 } ); } }); ``` --- ## Framework Integration Durable Endpoints is only available for Bun and Next.js API endpoints. [Reach out on Discord](/discord) to ask support for additional frameworks. --- ## Requesting a Durable Endpoint Durable Endpoints behave like regular API endpoints on the success path. You can request them from your front-end (_or back-end_) using `fetch()` or your favorite query or http library. When a failure triggers retries or long-running steps like `step.waitForEvent()` are used, a Durable Endpoint redirects to a separate endpoint that waits for the call to finish. By default, this is an endpoint either in the Inngest Dev Server or Inngest Cloud, depending on which environment you're in, but `inngest.endpointProxy()` can be used to create your own URL to satisfy CORS constraints when the endpoint is used from browsers. ```typescript // When setting the `endpointAdapter`, use `.withOptions()` to set more config new Inngest({ id: "my-app", endpointAdapter: endpointAdapter.withOptions({ asyncRedirectUrl: "/wait", }), }); // Then create the route with `inngest.endpointProxy()` Bun.serve({ port: 3000, routes: { "/process": ..., "/wait": inngest.endpointProxy(), }, }); ``` Requests will now be redirected to `/wait`. To stream data to the client during execution, see [streaming](#streaming) below or the full [streaming guide](/docs/learn/durable-endpoints/streaming). --- ## Streaming Durable Endpoints can stream data back to clients in real-time using Server-Sent Events (SSE). For a full guide covering concepts, examples, and rollback semantics, see [the guide](/docs/learn/durable-endpoints/streaming). ### Server: `stream.push()` and `stream.pipe()` Import the `stream` object from `inngest/experimental/durable-endpoints` and use it inside your endpoint handler within a [`step.run()`](/docs/reference/typescript/v4/functions/step-run) call: ```ts ``` Send a single chunk of data to the client as an SSE event. Accepts any JSON-serializable value. Does not block execution. No-op outside of an Inngest execution context. Pipe a stream source to the client, sending each chunk as an SSE event in real-time. Resolves with the concatenated text of all chunks. No-op outside of an Inngest execution context (resolves with an empty string). ### Client: `fetchWithStream()` Handles the implementation details of streaming, including: - Automatically committing and rolling back chunks based on the step's success or failure - Filtering out internal events - Handling the sync-to-async redirect ```ts ``` Returns a `Promise` containing the endpoint's final return value. Sync-to-async redirects are handled automatically. If the endpoint does not use streaming, the raw `Response` is returned as-is. The URL of the Durable Endpoint to call. Custom fetch implementation. Defaults to `globalThis.fetch`. Options passed to the underlying `fetch` call (e.g. `{ signal }` for cancellation). Called when run metadata is received. Always fires first. Called for each streamed chunk. Data should be considered uncommitted until `onCommit` fires. Called when a step completes successfully. Chunks from that step are now permanent. Called when a step fails and will retry. Discard uncommitted chunks from that step. --- ## Node.js Utilities The `inngest/node` entry point exports helpers for serving Durable Endpoints in Node.js environments: ```ts ``` Bridge a Web API endpoint handler to a Node.js `http.RequestListener`. Converts an incoming `http.IncomingMessage` into a Web API `Request`, invokes the handler, then streams the resulting `Response` back through the Node.js `http.ServerResponse`. Create an `http.Server` that serves a Durable Endpoint handler directly. A convenience wrapper around `serveEndpoint()`. ```ts createEndpointServer( inngest.endpoint(async (req) => { await step.run("work", async () => { return await doWork(); }); return Response.json({ result }); }) ); server.listen(3000); ``` # Send events Source: https://www.inngest.com/docs/reference/typescript/v4/events/send Description: inngest.send() in TypeScript SDK v4 sends one or more typed events to Inngest with full TypeScript inference from your defined event schemas. metaTitle = "inngest.send() | Send Events (TypeScript SDK v4)" Send events to Inngest. Functions with matching event triggers will be invoked. ```ts await inngest.send({ name: "app/account.created", data: { accountId: "645e9f6794e10937e9bdc201", billingPlan: "pro", }, user: { external_id: "645ea000129f1c40109ca7ad", email: "taylor@example.com", } }) ``` To send events from within the context of a function, use [`step.sendEvent()`](/docs/reference/typescript/v4/functions/step-send-event). Use [`eventType().create()`](/docs/reference/typescript/v4/functions/triggers#with-inngestsend-and-stepsendevent) to build fully typed event payloads for `inngest.send()`. --- ## `inngest.send(eventPayload | eventPayload[], options): Promise<{ ids: string[] }>` An event payload object or an array of event payload objects. The event name. We recommend using lowercase dot notation for names, prepending `prefixes/` with a slash for organization. Any data to associate with the event. Will be serialized as JSON. Any relevant user identifying data or attributes associated with the event. **This data is encrypted at rest.** An external identifier for the user. Most commonly, their user id in your system. A unique ID used to idempotently trigger function runs. If duplicate event IDs are seen, only the first event will trigger function runs. [Read the idempotency guide here](/docs/guides/handling-idempotency). A timestamp integer representing the time (in milliseconds) at which the event occurred. Defaults to the time the Inngest receives the event. If the `ts` time is in the future, function runs will be scheduled to start at the given time. This has the same effect as running `await step.sleepUntil(event.ts)` at the start of the function. Note: This does not apply to functions waiting for events. Functions waiting for events will immediately resume, regardless of the timestamp. A version identifier for a particular event payload. e.g. `"2023-04-14.1"` The [environment](/docs/platform/environments) to send the events to. ```ts // Send a single event await inngest.send({ name: "app/post.created", data: { postId: "01H08SEAXBJFJNGTTZ5TAWB0BD" } }); // Send an array of events await inngest.send([ { name: "app/invoice.created", data: { invoiceId: "645e9e024befa68763f5b500" } }, { name: "app/invoice.created", data: { invoiceId: "645e9e08f29fb563c972b1f7" } }, ]); // Send user data that will be encrypted at rest await inngest.send({ name: "app/account.created", data: { billingPlan: "pro" }, user: { external_id: "6463da8211cdbbcb191dd7da", email: "test@example.com" } }); // Specify the idempotency id, version, and timestamp await inngest.send({ // Use an id specific to the event type & payload id: "cart-checkout-completed-ed12c8bde", name: "storefront/cart.checkout.completed", data: { cartId: "ed12c8bde" }, user: { external_id: "6463da8211cdbbcb191dd7da" }, ts: 1684274328198, v: "2024-05-15.1" }); ``` ### Return values The function returns a promise that resolves to an object with an array of Event IDs that were sent. These events can be used to look up the event in the Inngest dashboard or via [the REST API](https://api-docs.inngest.com/v1/events/GetEvent). ```ts await inngest.send([ { name: "app/invoice.created", data: { invoiceId: "645e9e024befa68763f5b500" } }, { name: "app/invoice.created", data: { invoiceId: "645e9e08f29fb563c972b1f7" } }, ]); /** * ids = [ * "01HQ8PTAESBZPBDS8JTRZZYY3S", * "01HQ8PTFYYKDH1CP3C6PSTBZN5" * ] */ ``` ## User data encryption 🔐 All data sent in the `user` object is fully encrypted at rest. ⚠️ When [replaying a function](/docs/platform/replay), `event.user` will be empty. This will be fixed in the future, but for now assume that you cannot replay functions that rely on `event.user` data. In the future, this object will be used to support programmatic deletion via API endpoint to support certain right-to-be-forgotten flows in your system. This will use the `user.external_id` property for lookup. ## Usage limits See [usage limits][usage-limits] for more details. [usage-limits]: /docs/usage-limits/inngest#events # Extended Traces (OpenTelemetry) Source: https://www.inngest.com/docs/reference/typescript/v4/extended-traces Description: Use the ExtendedTracesMiddleware in TypeScript SDK v4 to automatically instrument Inngest functions and forward spans to Datadog, Grafana, or any OTel backend. metaTitle = "Extended Traces (OpenTelemetry) | TypeScript SDK v4" Inngest supports OpenTelemetry for distributed tracing and observability across your functions. The `extendedTracesMiddleware` exports OpenTelemetry spans from your functions to Inngest Traces, giving you deep insights into function execution, step timing, and performance. If you want Inngest to set up OpenTelemetry for you, use [`@inngest/otel`](https://www.npmjs.com/package/@inngest/otel) for a Node.js OpenTelemetry provider and common instrumentation. If your app already configures OpenTelemetry, keep that setup and add the Extended Traces middleware so spans are exported to Inngest. ## Basic Usage Use this path when your application does not already configure OpenTelemetry. Install and use `@inngest/otel` to set up a Node.js OpenTelemetry provider and common instrumentations before your application code starts, and `extendedTracesMiddleware()` attaches to that provider. Preload it before your application: ```shell node --import @inngest/otel/node ./app.js ``` If `--import` is not available, use a small bootstrap file as your process entrypoint. Because ESM static imports are evaluated before the importing module runs, keep the dynamic `await import("./app.js")`; a static `import "./app.js"` in the same file can load app modules before instrumentation is registered. ```ts import "@inngest/otel/node"; await import("./app.js"); ``` Then configure the Inngest client to use the Extended Traces middleware: ```ts new Inngest({ id: "my-app", middleware: [extendedTracesMiddleware()], }); ``` Because `@inngest/otel` is preloaded, the middleware's default behavior extends the existing provider. Load `@inngest/otel/node` before importing code that uses libraries you want OpenTelemetry to instrument. Loading it later can miss instrumentation patches for modules that were already imported. AI metadata extraction is configured separately from Extended Traces. To stop the TypeScript SDK from extracting AI metadata from OpenTelemetry spans, set `aiMetadata: false` on the [Inngest client](/docs/reference/typescript/v4/client/create#aiMetadata). Extended Traces will continue to export spans when configured. ## Advanced Usage } title={'Set up an OpenTelemetry client with Inngest or create custom spans'} > Follow this guide to set up OpenTelemetry for AI metadata extraction, Extended Traces, and custom spans, including CommonJS and ESM preload examples. ### Serverless If you're using serverless, the entrypoint of your app will likely be the file for a particular endpoint, for example `/api/inngest`. If your platform supports Node.js flags, prefer the `--import @inngest/otel/node` preload shown above. If not, import `@inngest/otel/node` before your Inngest client and functions in the route module. ```ts // Import instrumentation first import "@inngest/otel/node"; // Then import your Inngest client and functions { GET, POST, PUT } = serve({ client: inngest, functions: [myFn], }); ``` ### Extending existing providers A JavaScript process can only have a single OpenTelemetry Provider. Some libraries such as Sentry also create their own provider. If your application already creates an OpenTelemetry provider, keep that setup. By default, `extendedTracesMiddleware()` extends the existing provider when it can. ```ts extendedTracesMiddleware(); ``` In the case of Sentry, `extendedTracesMiddleware()` will extend Sentry's provider as long as it's run after `Sentry.init()`. This extension should also work for OpenTelemetry providers that originate within the runtime, like [Deno's OpenTelemetry](https://docs.deno.com/runtime/fundamentals/open_telemetry/). Set `behaviour: "extendProvider"` if you want the middleware to fail when no existing provider is available instead of falling back to provider creation: ```ts extendedTracesMiddleware({ behaviour: "extendProvider", }); ``` The options are: - `"auto"` (default): Attempt to extend a provider if one exists. For backward compatibility, this may create a provider if none exists, but provider creation is deprecated in favor of `@inngest/otel`. - `"extendProvider"`: Only attempt to extend a provider and fails if none exists - `"createProvider"` (deprecated): Only attempt to create a provider. Use `@inngest/otel` instead. - `"off"`: Do nothing If you only use `extendedTracesMiddleware()` to extend an existing provider, you do not need to call it before other application code. The provider and instrumentation setup remains owned by your existing OpenTelemetry configuration. ### Manually extend If you're already manually creating your own trace provider and import ordering is an issue, you may want to manually add Inngest's `InngestSpanProcessor` to your existing setup. Add an `InngestSpanProcessor` to your provider: ```ts // Create your client the same as you would normally inngest = new Inngest({ id: "my-app", middleware: [ extendedTracesMiddleware({ // The provider below adds InngestSpanProcessor, so keep the middleware from creating or extending another provider. behaviour: "off", }), ], }); // Then when you create your provider, pass the client to it new BasicTracerProvider({ // Add the span processor when creating your provider spanProcessors: [new InngestSpanProcessor(inngest)], }); // Register the provider globally provider.register(); ``` ## Instrumentation `@inngest/otel` automatically instruments common Node.js libraries when it is loaded before your application code. It instruments common Node packages, OpenAI, Anthropic, and Google Generative AI. If you configure OpenTelemetry yourself, continue to manage your own instrumentation list. Check the `@opentelemetry/auto-instrumentations-node` docs for the current supported instrumentation list and default-disabled entries. See the [OpenTelemetry setup guide](/docs/examples/open-telemetry) for general setup, AI metadata extraction, and CommonJS and ESM preload examples. ### Custom instrumentation If you need instrumentations that are not covered by `@inngest/otel`, configure them yourself and add `InngestSpanProcessor` to your provider. For example, here's an example of adding [Prisma OpenTelemetry](https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/opentelemetry-tracing): ```ts new Inngest({ id: "my-app", middleware: [extendedTracesMiddleware({ behaviour: "off" })], }); new NodeSDK({ spanProcessors: [new InngestSpanProcessor(inngest)], instrumentations: [new PrismaInstrumentation()], }); sdk.start(); ``` # Cancel on Source: https://www.inngest.com/docs/reference/typescript/v4/functions/cancel-on Description: Auto-cancel in-flight runs when a matching event arrives, with optional CEL expression filtering. metaTitle = "cancelOn | Auto-Cancel on Events (TypeScript SDK v4)" Stop the execution of a running function when a specific event is received using `cancelOn`. ```ts inngest.createFunction( { id: "sync-contacts", triggers: { event: "app/user.created" }, cancelOn: [ { // Can be a string or EventType event: "app/user.deleted", // ensure the async (future) event's userId matches the trigger userId if: "async.data.userId == event.data.userId", }, ], } // ... ); ``` Using `cancelOn` is very useful for handling scenarios where a long-running function should be terminated early due to changes elsewhere in your system. The API for this is similar to the [`step.waitForEvent()`](/docs/reference/typescript/v4/functions/step-wait-for-event) tool, allowing you to specify the incoming event and different methods for matching pieces of data within. --- ## How to use `cancelOn` The most common use case for cancellation is to cancel a function's execution if a specific field in the incoming event matches the same field in the triggering event. For example, you might want to cancel a sync event for a user if that user is deleted. For this, you need to specify a `match` [expression](/docs/guides/writing-expressions). Let's look at an example function and two events. This function specifies it will `cancelOn` the `"app/user.deleted"` event only when it and the original `"app/user.created"` event have the same `data.userId` value: ```ts inngest.createFunction( { id: "sync-contacts", triggers: { event: "app/user.created" }, cancelOn: [ { event: "app/user.deleted", // ensure the async (future) event's userId matches the trigger userId if: "async.data.userId == event.data.userId", }, ], }, // ... ); ``` For the given function, this is an example of an event that would trigger the function: ```json { "name": "app/user.created", "data": { "userId": "123", "name": "John Doe" } } ``` And this is an example of an event that would cancel the function as it and the original event have the same `data.userId` value of `"123"`: ```json { "name": "app/user.deleted", "data": { "userId": "123" } } ``` Match expressions can be simple equalities or be more complex. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Functions are cancelled _between steps_, meaning that if there is a `step.run` currently executing, it will finish before the function is cancelled. Inngest does this to ensure that steps are treated like atomic operations and each step either completes or does not run at all. ## Configuration Define events that can be used to cancel a running or sleeping function The event that will be used to cancel. This can be a string event name (e.g. `"app/user.deleted"`) or an event object created with `eventType()` The property to match the event trigger and the cancelling event, using dot-notation, for example, `data.userId`. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. An expression on which to conditionally match the original event trigger (`event`) and the wait event (`async`). Cannot be combined with `match`. Expressions are defined using the Common Expression Language (CEL) with the events accessible using dot-notation. Read our [guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * `event.data.userId == async.data.userId && async.data.billing_plan == 'pro'` How long to wait to receive the cancelling event. Either: - A duration string compatible with the [ms](https://npm.im/ms) package, e.g. `"30m"`, `"3 hours"`, or `"2.5d"`, - A `number` of milliseconds, - An absolute `Date`, - A [`Temporal.Duration`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration) for a relative wait, or - A [`Temporal.Instant`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant) or [`Temporal.ZonedDateTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime) for an absolute deadline. ## Examples ### Using an event object The `event` property also accepts an event object instead of a string. This is useful when you have typed event definitions or want to reference events from a schema. ```ts inngest.createFunction( { id: "sync-contacts", triggers: userCreatedEvent, cancelOn: [ { event: userDeletedEvent, if: "async.data.userId == event.data.userId", }, ], } // ... ); ``` ### With a timeout window Cancel a function's execution if a matching event is received within a given amount of time from the function being triggered. ```ts inngest.createFunction( { id: "sync-contacts", triggers: { event: "app/user.created" }, cancelOn: [{ event: "app/user.deleted", match: "data.userId", timeout: "1h" }], } // ... ); ``` This is useful when you want to limit the time window for cancellation, ensuring that the function will continue to execute if no matching event is received within the specified time frame. # Managing concurrency Source: https://www.inngest.com/docs/reference/typescript/v4/functions/concurrency Description: Set step-level concurrency limits with key expressions for per-user or per-tenant scoping. metaTitle = "Concurrency | TypeScript SDK v4 Reference" Limit the number of concurrently running steps for your function with the [`concurrency`](/docs/reference/typescript/v4/functions/create#configuration) configuration options. Setting an optional `key` parameter limits the concurrency for each unique value of the expression. **Important:** Concurrency limits _step_ execution, not the total number of function runs. Functions that are sleeping, waiting for events, or paused between steps do **not** count against your concurrency limit. This means you may have many more function runs in progress than your concurrency limit. [Learn more about how concurrency works →](/docs/guides/concurrency#how-concurrency-works) [Read our concurrency guide for more information on concurrency, including how it works and any limits](/docs/guides/concurrency). ```ts {{ title: "Simple" }} export default inngest.createFunction( { id: "sync-contacts", triggers: { event: "app/user.created" }, concurrency: { limit: 10, }, }, async ({ event, step }) => { // ... } ); ``` ```ts {{ title: "Multiple keys" }} inngest.createFunction( { id: "unique-function-id", triggers: { event: "ai/summary.requested" }, concurrency: [ { // Use an account-level concurrency limit for this function, using the // "openai" key as a virtual queue. Any other function which // runs using the same "openai"` key counts towards this limit. scope: "account", key: `"openai"`, // If there are 10 steps executing with the "openai" key, this function's // runs will wait for capacity before executing. limit: 10, }, { // Create another virtual concurrency queue for this function only. This // limits all accounts to a single executing step for this function, based off // of the `event.data.account_id` field. // "fn" is the default scope, so we could omit this field. scope: "fn", key: "event.data.account_id", limit: 1, }, ], }, async ({ event, step }) => { // ... } ); ``` Setting `concurrency` limits are very useful for: * Handling API rate limits - Limit concurrency to stay within the rate limit quotas that are allowed by a given third party API. * Limiting database operations or connections * Preventing one of your user's accounts from consuming too many resources (see `key`) Alternatively, if you want to limit the number of times that your function runs in a given period, [the `rateLimit` option](/docs/reference/typescript/v4/functions/rate-limit) may be better for your use case. ## Configuration Options to configure concurrency. Specifying a `number` is a shorthand to set the `limit` property. The maximum number of concurrently running steps. A value of `0` or `undefined` is the equivalent of not setting a limit. The maximum value is dictated by your account's plan. The scope for the concurrency limit, which impacts whether concurrency is managed on an individual function, across an environment, or across your entire account. * `fn` (default): only the runs of this function affects the concurrency limit * `env`: all runs within the same environment that share the same evaluated key value will affect the concurrency limit. This requires setting a `key` which evaluates to a virtual queue name. * `account`: every run that shares the same evaluated key value will affect the concurrency limit, across every environment. This requires setting a `key` which evaluates to a virtual queue name. An expression which evaluates to a string given the triggering event. The string returned from the expression is used as the concurrency queue name. A key is required when setting an `env` or `account` level scope. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Limit concurrency to `n` (via `limit`) per customer id: `'event.data.customer_id'` * Limit concurrency to `n` per user, per import id: `'event.data.user_id + "-" + event.data.import_id'` * Limit globally using a specific string: `'"global-quoted-key"'` (wrapped in quotes, as the expression is evaluated as a language) **Step-level concurrency:** The concurrency option controls the number of concurrent _steps_ that can be running at any one time, not the number of function runs. Because function runs frequently pause (for sleeps, waiting for events, or between steps), it's common to have many more function runs in progress than your concurrency limit. However, only the configured number of steps will ever be _actively executing_ at once. For example, with `concurrency: 10`, you might have 500 function runs in progress, but only 10 steps are executing code at any given moment. # Create Function Source: https://www.inngest.com/docs/reference/typescript/v4/functions/create Description: inngest.createFunction() in TypeScript SDK v4 configures triggers, concurrency, throttle, debounce, priority, retries, and the function handler. metaTitle = "inngest.createFunction() | TypeScript SDK v4 Reference" Define your functions using the `createFunction` method on the [Inngest client](/docs/reference/typescript/v4/client/create). ```ts export default inngest.createFunction( { id: "import-product-images", triggers: { event: "shop/product.imported" }, }, async ({ event, step, runId }) => { // Your function code } ); ``` --- ## `inngest.createFunction(configuration, handler): InngestFunction` The `createFunction` method accepts a series of arguments to define your function. ### Configuration A unique identifier for your function. This should not change between deploys. A name for your function. If defined, this will be shown in the UI as a friendly display name instead of the ID. One or more triggers. [Trigger helpers](/docs/reference/typescript/v4/functions/triggers) like `eventType()` and `cron()` are recommended, but you can also use normal objects. Limit the number of concurrently running functions ([reference](/docs/functions/concurrency)) The maximum number of concurrently running steps. The scope for the concurrency limit, which impacts whether concurrency is managed on an individual function, across an environment, or across your entire account. * `fn` (default): only the runs of this function affects the concurrency limit * `env`: all runs within the same environment that share the same evaluated key value will affect the concurrency limit. This requires setting a `key` which evaluates to a virtual queue name. * `account`: every run that shares the same evaluated key value will affect the concurrency limit, across every environment. This requires setting a `key` which evaluates to a virtual queue name. A unique key expression for which to restrict concurrently running steps to. The expression is evaluated for each triggering event and a unique key is generated. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Limits the number of new function runs started over a given period of time ([guide](/docs/guides/throttling)). The total number of runs allowed to start within the given `period`. The period within which the `limit` will be applied. The number of additional runs allowed to start in the given window in a single burst. This is added on top of the limit, which ensures high throughput within the period. A unique expression for which to apply the throttle limit to. The expression is evaluated for each triggering event and will be applied for each unique value. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. A key expression which is used to prevent duplicate events from triggering a function more than once in 24 hours. This is equivalent to setting `rateLimit` with a `key`, a `limit` of `1` and `period` of `24hr`. [Read the idempotency guide here](/docs/guides/handling-idempotency). Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Only run once for each customer id: `'event.data.customer_id'` * Only run once for each account and email address: `'event.data.account_id + "-" + event.user.email'` Options to configure how to rate limit function execution ([reference](/docs/reference/typescript/v4/functions/rate-limit)) The maximum number of functions to run in the given time period. The time period of which to set the limit. The period begins when the first matching event is received. Current permitted values are from `1s` to `60s`. A unique key expression to apply the limit to. The expression is evaluated for each triggering event. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Rate limit per customer id: `'event.data.customer_id'` * Rate limit per account and email address: `'event.data.account_id + "-" + event.user.email'` Options to configure function debounce ([reference](/docs/reference/typescript/v4/functions/debounce)) The time period of which to set the limit. The period begins when the first matching event is received. Current permitted values are from `1s` to `7d` (`168h`). A unique key expression to apply the debounce to. The expression is evaluated for each triggering event. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Debounce per customer id: `'event.data.customer_id'` * Debounce per account and email address: `'event.data.account_id + "-" + event.user.email'` Options to configure how to prioritize functions An expression which must return an integer between -600 and 600 (by default), with higher return values resulting in a higher priority. Examples: * Return the priority within an event directly: `event.data.priority` (where `event.data.priority` is an int within your account's range) * Rate limit by a string field: `event.data.plan == 'enterprise' ? 180 : 0` See [reference](/docs/reference/typescript/v4/functions/run-priority) for more information. Configure how the function should consume batches of events ([reference](/docs/guides/batching)) The maximum number of events a batch can have. Current limit is `100`. How long to wait before invoking the function with the batch even if it's not full. Current permitted values are from `1s` to `60s`. A unique key expression to apply the batching to. The expression is evaluated for each triggering event. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Batch events per customer id: `'event.data.customer_id'` * Batch events per account and email address: `'event.data.account_id + "-" + event.user.email'` A boolean expression to conditionally batch events that evaluate to true on this expression. The expression is evaluated for each triggering event. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Batch events for free account types: `'event.data.account_type == "free"'` Configure the number of times the function will be retried from `0` to `20`. Default: `4` A function that will be called only when this Inngest function fails after all retries have been attempted ([reference](/docs/reference/typescript/v4/functions/handling-failures)) Define events that can be used to cancel a running or sleeping function ([reference](/docs/reference/typescript/v4/functions/cancel-on)) The event that will be used to cancel. This can be a string event name (e.g. `"app/user.deleted"`) or an event object created with `eventType()` The property to match the event trigger and the cancelling event, using dot-notation, for example, `data.userId`. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. An expression on which to conditionally match the original event trigger (`event`) and the wait event (`async`). Cannot be combined with `match`. Expressions are defined using the Common Expression Language (CEL) with the events accessible using dot-notation. Read our [guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * `event.data.userId == async.data.userId && async.data.billing_plan == 'pro'` The amount of time to wait to receive the cancelling event. A time string compatible with the [ms](https://npm.im/ms) package, e.g. `"30m"`, `"3 hours"`, or `"2.5d"` Options to configure timeouts for cancellation ([reference](/docs/features/inngest-functions/cancellation/cancel-on-timeouts)) The timeout for starting a function run. If the time between scheduling and starting a function exceeds this duration, the function will be cancelled. Examples are: `10s`, `45m`, `18h30m`. The timeout for executing a run. If a run takes longer than this duration to execute, the run will be cancelled. This does not include the time waiting for the function to start (see `timeouts.start`). Examples are: `10s`, `45m`, `18h30m`. #### Triggers You can use [trigger helper functions](/docs/reference/typescript/v4/functions/triggers) to define typed triggers with optional runtime validation: ```ts eventType("shop/order.placed", { schema: z.object({ orderId: z.string(), total: z.number() }), }); inngest.createFunction( { id: "process-order", triggers: [orderPlaced, cron("0 * * * *")], }, async ({ event, step }) => { // event.data is fully typed } ); ``` Alternatively, you can specify an array of up to 10 of the following triggers to invoke your function with multiple events or crons. See the [Multiple Triggers](/docs/guides/multiple-triggers) guide. Cron triggers with overlapping schedules for a single function will be deduplicated. The event that will trigger this function to run. This can be a string event name (e.g. `"app/user.created"`) or an event object created with `eventType()`. A [unix-cron](https://crontab.guru/) compatible schedule string.
Optional timezone prefix, e.g. `TZ=Europe/Paris 0 12 * * 5`.
When using an `event` trigger, you can optionally combine it with the `if` option to filter events: A comparison expression that returns true or false whether the function should handle or ignore a given matching event. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * `'event.data.action == "published"'` * `'event.data.priority >= 4'` ### Handler The handler is your code that runs whenever the trigger occurs. Every function handler receives a single object argument which can be deconstructed. The key arguments are `event` and `step`. Note, that scheduled functions that use a `cron` trigger will not receive an `event` argument. ```ts function handler({ event, events, step, runId, logger, attempt }) {/* ... */} ``` #### `event` The event payload `object` that triggered the given function run. The event payload object will match what you send with [`inngest.send()`](/docs/reference/typescript/v4/events/send). Below is an example event payload object: ```ts { name: "app/account.created", data: { userId: "1234567890" }, v: "2023-05-12.1", ts: 1683898268584 } ``` #### `events` `events` is an array of `event` payload objects that's accessible when the `batchEvents` is set on the function configuration. If batching is not configured, the array contains a single event payload matching the `event` argument. #### `step` The `step` object has methods that enable you to define - [`step.run()`](/docs/reference/typescript/v4/functions/step-run) - Run synchronous or asynchronous code as a retriable step in your function - [`step.sleep()`](/docs/reference/typescript/v4/functions/step-sleep) - Sleep for a given amount of time - [`step.sleepUntil()`](/docs/reference/typescript/v4/functions/step-sleep-until) - Sleep until a given time - [`step.invoke()`](/docs/reference/typescript/v4/functions/step-invoke) - Invoke another Inngest function as a step, receiving the result of the invoked function - [`step.waitForEvent()`](/docs/reference/typescript/v4/functions/step-wait-for-event) - Pause a function's execution until another event is received - [`step.sendEvent()`](/docs/reference/typescript/v4/functions/step-send-event) - Send event(s) reliably within your function. Use this instead of `inngest.send()` to ensure reliable event delivery from within functions. #### `runId` The unique ID for the given function run. This can be useful for logging and looking up specific function runs in the Inngest dashboard. #### `logger` The `logger` object exposes the following interfaces. ```ts export interface Logger { info(...args: any[]): void; warn(...args: any[]): void; error(...args: any[]): void; debug(...args: any[]): void; } ``` It is a proxy object that is either backed by `console` or the logger you provided ([reference](/docs/guides/logging)). #### `attempt` The current zero-indexed attempt number for this function execution. The first attempt will be 0, the second 1, and so on. The attempt number is incremented every time the function throws an error and is retried. # Debounce functions Source: https://www.inngest.com/docs/reference/typescript/v4/functions/debounce Description: Delay execution and collapse rapid event sequences into a single function run. metaTitle = "Debounce | Deduplicate Events (TypeScript SDK v4)" Debounce delays a function run for the given `period`, and reschedules functions for the given `period` any time new events are received while the debounce is active. The function run starts after the specified `period` passes and no new events have been received. Functions use the last event as their input data. See the [Debounce guide](/docs/guides/debounce) for more information about how this feature works. ```ts export default inngest.createFunction( { id: "handle-webhook", debounce: { key: "event.data.account_id", period: "5m", }, triggers: { event: "intercom/company.updated" }, }, async ({ event, step }) => { // This function will only be scheduled 5m after events have stopped being received with the same // `event.data.account_id` field. // // `event` will be the last event in the series received. } ); ``` Options to configure how to debounce function execution The time delay to delay execution. The period begins when the first matching event is received. Current permitted values are from `1s` to `7d` (`168h`). An optional unique key expression to apply the limit to. The expression is evaluated for each triggering event, and allows you to debounce against event data. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Rate limit per customer id: `'event.data.customer_id'` * Rate limit per account and email address: `'event.data.account_id + "-" + event.user.email'` The maximum time that a debounce can be extended before running. Functions will run using the last event received as the input data. Debounce cannot be combined with [batching](/docs/guides/batching). # Deferred Functions Source: https://www.inngest.com/docs/reference/typescript/v4/functions/deferred-functions Description: Trigger independent background tasks with typed data without waiting for a return value. metaTitle = "Deferred Functions | Fire-and-Forget Background Tasks" A deferred function is an Inngest function that runs in the background as a side effect of another run. Instead of the usual triggers, a parent run launches it by calling `defer("some-id", { function, data })`. The parent doesn't wait, doesn't see a result, and keeps executing. The deferred run is fully independent: its own retries, concurrency, step state, and typed payload. Deferred functions are experimental. `createDefer` is imported from `inngest/experimental` and the API may change before GA. `defer(...)` doesn't yet work with [encryption middleware](/docs/reference/typescript/v4/middleware/encryption) — payloads passed to a deferred function aren't encrypted. Support is planned; in the meantime, avoid passing encrypted data through `defer(...)`. Use deferred functions for work that should happen *because of* a run, not *as part of* it: scoring an agent's output, sending a notification, logging a side effect, queueing follow-up work. Any function can call the same deferred function, and each call is its own run. ## When to use | Tool | Returns to caller? | Independent execution? | | ------------------------------- | ------------------- | ------------------------- | | `step.invoke(id, { function, data })` | Yes (awaits result) | No (caller blocks) | | `step.sendEvent(...)` | No | Yes (any matching fn) | | `defer(id, { function, data })` | No | Yes (single typed target) | A common use case is an [LLM scorer](/docs/features/inngest-functions/steps-workflows/deferred-scoring) that runs against the output of an agent run. --- ## Defining a deferred function Use `createDefer` to define a deferred function: ```ts sendEmail = createDefer( inngest, { id: "send-email", schema: z.object({ to: z.string(), body: z.string() }), concurrency: { limit: 5 }, }, async ({ event, step }) => { event.data.to; // typed from `schema` event.data.body; } ); ``` A deferred function is a regular Inngest function with its own retries, concurrency, and step state. Register it alongside your other functions in the serve handler: ```ts serve({ client: inngest, functions: [...myFunctions, sendEmail] }); ``` ### `createDefer(client, config, handler): DeferredFunction` `createDefer` mirrors [`inngest.createFunction`](/docs/reference/typescript/v4/functions/create) with a few differences: - The client is the first positional argument. - `triggers` is not accepted — the function is triggered implicitly by `defer(...)`. - `schema` describes the payload that callers send. - `onFailure` and `batchEvents` are not currently supported. Your Inngest client instance. Function configuration. Accepts the same options as `inngest.createFunction` (`concurrency`, `throttle`, `rateLimit`, etc.) except `triggers`, `onFailure`, and `batchEvents`, and adds `schema`. A unique identifier for the function. A [Standard Schema](https://standardschema.dev/) describing the payload that callers send via `defer(...)`. Optional. When present, `data` is validated on both the caller and receiver side and types `event.data` in the handler. Without a schema, `data` falls back to `Record`. The async handler. Receives the same arguments as a normal Inngest function handler. --- ## Calling `defer` `defer` is always available on the handler context of any Inngest function: ```ts inngest.createFunction( { id: "order-placed", triggers: { event: "order/placed" } }, async ({ defer }) => { defer("send", { function: sendEmail, data: { to: "a@b.com", body: "hi" }, }); } ); ``` `defer(...)` is **synchronous and fire-and-forget**. The parent run continues immediately and never sees a result; the deferred run starts when the parent run finalizes. The call returns a [`DeferHandle`](#defer-handle-abort-void) whose `abort()` cancels the deferred run. It also works inside `step.run()`: ```ts await step.run("notify", async () => { defer("send", { function: sendEmail, data: { to, body } }); }); ``` ### `defer(id, options): DeferHandle` A unique identifier for this call. Must be unique within the parent run — unlike step IDs, no implicit index is appended to dedupe. (This is because `defer` can be used inside `step.run()`, where an implicit index would change on re-entry.) The deferred function to trigger. Payload to send to the deferred function. Typed from `function.schema` when present. Event metadata shared with the deferred run. [Sessions](/docs/features/events-triggers/sessions?ref=docs-reference-deferred-functions) to add to or override on the deferred run. Manually set sessions take precedence over sessions inherited from the calling run. A `null` value clears an inherited session for that key; setting `sessions` itself to `null` clears every inherited session. Numbers are normalized to strings. ### `deferHandle.abort(): void` `defer(...)` returns a handle scoped to that call's deferred run. Calling `abort()` cancels the run: ```ts defer("send", { function: sendEmail, data: { to, body } }); // ...later in the same parent run: handle.abort(); ``` Like `defer(...)` itself, `abort()` is synchronous, fire-and-forget, and idempotent: - An abort called at any point before the parent run finalizes prevents the deferred run from starting. Deferred runs only start once the parent finalizes, so there is no window where the run has already begun. - It never throws into the surrounding handler. Repeated aborts, or aborting a `defer(...)` call that was skipped (for example, after a schema validation failure), log a warning and do nothing. - Other `defer(...)` calls in the same run are unaffected. `abort()` works inside a `step.run()` closure: the abort ships together with the step's result, so if the result didn't reach Inngest the closure re-runs on retry and re-registers the abort. --- ## Schemas When `schema` is provided on the deferred function: - `data` is validated at the call site (synchronously). - `data` is validated again on the receiver side. This catches serialization round-trips that change the shape (e.g. a `Date` becoming an ISO string). - The same schema types `event.data` in the handler. Call-site validation must be synchronous because `defer(...)` itself is sync. If the schema's `validate` returns a Promise, the SDK logs an error and the call is skipped. Receiver-side validation is async, so async validators work there. --- ## Sessions A deferred run inherits the [sessions](/docs/features/events-triggers/sessions?ref=docs-reference-deferred-functions) of the run that called `defer(...)`, so the two are grouped together in the dashboard. Pass `meta.sessions` to add or override sessions on the deferred run: ```ts defer("score", { function: feedbackScorer, data: { ticketId }, meta: { sessions: { conversation_id: "conv_1234", // add or override internal_trace_id: null, // clear an inherited session }, }, }); ``` - Manually set sessions win over inherited ones, per key. - `meta.sessions: null` clears every inherited session. - Session IDs must be strings or finite numbers; numbers are normalized to strings. An invalid value is a call-site error — the whole `defer(...)` call is logged and skipped. - Inheritance is controlled by the client-level [`sessionPropagation`](/docs/reference/typescript/v4/client/create#configuration) option. Sessions passed explicitly in `meta.sessions` are sent regardless of that setting. --- ## Error handling `defer(...)` is fire-and-forget, so a bad call should not derail the surrounding handler. - **Call-site errors** (for example, a synchronous schema failure, or an invalid `meta.sessions` value) are logged via the internal logger and the call is silently skipped. The parent run continues normally and the deferred function does not fire. - **Receiver-side errors** (the deferred run reading invalid `event.data`, or the handler throwing) fail the deferred run itself with normal retry semantics. --- ## Sharing across parent functions A deferred function is one Inngest function in the backend. Multiple parents can hold a reference to it; each `defer(...)` call triggers an independent run with its own retries and concurrency state. # Fetch: performing API requests or fetching data Source: https://www.inngest.com/docs/reference/typescript/v4/functions/fetch Description: Make retryable, checkpointed HTTP requests using step.fetch() in TypeScript SDK v4. Automatic retries on failure without re-executing prior steps. metaTitle = "step.fetch() | Durable HTTP Requests (TypeScript SDK v4)" The Inngest TypeScript SDK provides a `step.fetch()` API and a `fetch()` utility, enabling you to make requests to third-party APIs or fetch data in a durable way by offloading them to the Inngest Platform: - `step.fetch()` is a shorthand for making HTTP requests from within an Inngest function, and it also makes it easier to start parallel HTTP requests. - The `fetch()` utility can be passed to packages that accept a custom `fetch` implementation, such as `axios`. ![Using Fetch offloads the HTTP request to the Inngest Platform](/assets/docs/features/inngest-functions/steps-workflows/fetch/step-fetch.png) ## Using `step.fetch()` You can use `step.fetch()` to make HTTP requests within an Inngest function. `step.fetch()` offloads the HTTP request to the Inngest Platform, so your service does not need to be active and waiting for the response. ```ts {{ title: "src/inngest/functions.ts" }} retrieveTextFile = inngest.createFunction( { id: "retrieveTextFile", triggers: { event: "textFile/retrieve" } }, async ({ step }) => { // The fetching of the text file is offloaded to the Inngest Platform await step.fetch( "https://example-files.online-convert.com/document/txt/example.txt" ); // The Inngest function run is resumed when the HTTP request is complete await step.run("extract-text", async () => { await response.text(); text.match(/example/g); return exampleOccurences?.length; }); } ); ``` See the complete step.fetch() example including the source code and other use cases. `step.fetch()` is useful: - In serverless environments, to offload long-running HTTP requests that might trigger timeouts. - As a shorthand for making HTTP requests within an Inngest function, making it easier to start parallel HTTP requests using `Promise.all()`. - As a best practice to ensure that all HTTP requests are durable and can be inspected in the Inngest Platform or Dev Server. ### `step.fetch()` observability All `step.fetch()` calls are visible in your [Inngest Traces](/docs/platform/monitor/observability-metrics), allowing you to monitor and debug your HTTP requests: ![Inngest Traces showing a step.fetch() call](/assets/blog/announcing-step-fetch/step-fetch-trace.png) ## Using the `fetch()` utility A Fetch API-compatible function is exported, allowing you to make any HTTP requests durable if they're called within an Inngest function. For example, a `MyProductApi` class that relies on axios can take a `fetch` parameter: ```ts {{ title: "TypeScript" }} new MyProductApi({ fetch }); // A call outside an Inngest function will fall back to the global fetch await api.getProduct(1); // A call from inside an Inngest function will be made durable and offloaded to the Inngest Platform inngest.createFunction( { id: "my-fn", triggers: { event: "product/activated" } }, async () => { await api.getProduct(1); }, ); ``` ⚠️ `fetch()` and `step.run()` Inngest's `fetch()` calls should not be performed inside of `step.run()` blocks. Doing so will result in `fetch()` to fallback to the global `fetch` implementation. Why? The `fetch()` utility transforms the `fetch` calls into `step.run()` calls, [which cannot be nested](/docs/sdk/eslint#inngest-no-nested-steps). ### Within steps By default, using Inngest's `fetch` retains all the functionality of requests made outside of an endpoint, but ensures that those made from inside are durable. ```ts {{ title: "TypeScript" }} // The AI SDK's createAnthropic objects can be passed a custom fetch implementation createAnthropic({ fetch: inngestFetch, }); // NOTE - Using this fetch outside of an Inngest function will fall back to the global fetch await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), prompt: 'Hello, world!', }); // A call from inside an Inngest function will be made durable inngest.createFunction( { id: "generate-summary", triggers: { event: "post.created" } }, async ({ event }) => { // This will use step.fetch automatically! await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), prompt: `Summarize the following post: ${event.data.content}`, }); }, ); ``` ### Using with AI SDK: Disable AI SDK retries **Important:** When using Inngest's `fetch` with the AI SDK, disable the AI SDK's built-in retry mechanism and let Inngest handle retries instead. The AI SDK has built-in retry logic that can interfere with Inngest's retry handling, especially when working with long-running models or serverless platforms with timeout limits (e.g., Vercel's 15-minute max timeout). **Why this matters:** - When AI SDK retries are enabled alongside Inngest's retry mechanism, the combined retry duration can exceed your platform's timeout limits - For long-running models (like OpenAI's o3), this is especially problematic - When a timeout occurs, the error messages can be confusing: Vercel cancels the request, but it appears in Inngest as an "Internal server error" (from Inngest, not Vercel), making debugging difficult **Recommended configuration:** ```ts {{ title: "generateText() with maxRetries: 0" }} createOpenAI({ fetch: inngestFetch, }); inngest.createFunction( { id: "generate-with-o3", triggers: { event: "content/generate" } }, async ({ event }) => { // Disable AI SDK retries - let Inngest handle them instead await generateText({ model: openai('o3'), prompt: event.data.prompt, maxRetries: 0, // Critical: Set to 0 to let Inngest handle retries }); return response; }, ); ``` ```ts {{ title: "streamText() with maxRetries: 0" }} createAnthropic({ fetch: inngestFetch, }); inngest.createFunction( { id: "stream-completion", triggers: { event: "completion/stream" } }, async ({ event, step }) => { // Disable AI SDK retries for streaming operations await streamText({ model: anthropic('claude-3-5-sonnet-20240620'), prompt: event.data.prompt, maxRetries: 0, // Let Inngest handle retries }); return result; }, ); ``` By setting `maxRetries: 0` in your AI SDK calls, you: - Avoid timeout issues on serverless platforms - Get clearer error messages when failures occur - Leverage Inngest's retry mechanism, which is designed for long-running operations - Benefit from Inngest's observability to see exactly what happened during each retry attempt However, the same `fetch` is also exported as `step.fetch`, allowing you to create your APIs isolated within the function instead: ```ts {{ title: "TypeScript" }} createAnthropic({ fetch: inngestFetch, }); inngest.createFunction( { id: "generate-summary", triggers: { event: "post.created" } }, async ({ step }) => { await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), prompt: `Summarize the following post: ${event.data.content}`, }); }, ); ``` ### Fallbacks By default, it will gracefully fall back to the global `fetch` if called outside of an Inngest function, though you can also set a custom fallback using the `config` method: ```ts {{ title: "TypeScript" }} new MyProductApi({ fetch: fetch.config({ fallback: myCustomFetch }), }); ``` You can also disable the fallback entirely: ```ts {{ title: "TypeScript" }} new MyProductApi({ fetch: fetch.config({ fallback: undefined }), }); ``` ### How it works Inngest's `fetch` function uses some of the basic building blocks of Inngest to allow seamless creation of optionally durable code. When it's called, it will: - Check the context in which it's running - If not in an Inngest function, optionally use the fallback; otherwise, - Report the request to Inngest - Inngest makes the request - Inngest continues the function with the `Response` received from your request Critically, this means that your service does not have to be active for the duration of the call; we'll continue your function when we have a result, while also keeping it durable! # `group.experiment()` Source: https://www.inngest.com/docs/reference/typescript/v4/functions/group-experiment Description: group.experiment() in TypeScript SDK v4 runs reproducible A/B experiments within durable functions to compare models, prompts, or strategies. metaTitle = "group.experiment() | A/B Testing in Functions (SDK v4)" Selects and executes a single variant from a set of options. The selection is memoized as a durable step, so the same variant runs on retries and replays. --- ## `group.experiment(id, options): Promise` A unique identifier for the experiment. Used in logs and to memoize the variant selection across retries and replays. Configuration for the experiment: A map of variant names to callbacks. Each callback should contain one or more `step.*` calls. Only the selected variant's callback is executed. A selection strategy that determines which variant to run. Use one of the built-in strategies: `experiment.fixed()`, `experiment.weighted()`, `experiment.bucket()`, or `experiment.custom()`. ```ts await group.experiment("my-experiment", { variants: { a: () => step.run("variant-a", () => doA()), b: () => step.run("variant-b", () => doB()), }, select: experiment.weighted({ a: 50, b: 50 }), }); ``` ```ts await group.experiment("my-experiment", { variants: { a: () => step.run("variant-a", () => doA()), b: () => step.run("variant-b", () => doB()), }, select: experiment.fixed("a"), }); // variant === "a" ``` ## Return value Resolves to an object with the selected variant's output and a reference for scoring: The value returned by the selected variant's callback. The name of the variant that was selected and run. A stable handle to the selected variant (`{ experimentName, variant }`). Pass it to [`inngest.score.experiment()`](/docs/reference/typescript/v4/functions/scoring) or [`defer()`](/docs/reference/typescript/v4/functions/deferred-functions) to attach a score to this experiment. Every variant callback **must** invoke at least one `step.*` tool (e.g., `step.run()`). Code that runs outside of a step is not memoized and will re-execute on every replay. The SDK throws a `NonRetriableError` if a variant completes without calling any step tools. ## Selection strategies Import the `experiment` object from the `inngest` package: ```ts ``` ### `experiment.fixed(variantName)` Always selects the specified variant. Useful for manual overrides or testing a specific code path. ```ts select: experiment.fixed("control") ``` ### `experiment.weighted(weights)` Weighted random selection, seeded with the current Inngest run ID. Deterministic: the same run always gets the same variant, even across retries. ```ts select: experiment.weighted({ control: 80, treatment: 20 }) ``` Weights are relative, not percentages. `{ a: 1, b: 3 }` gives `a` a 25% chance and `b` a 75% chance. ### `experiment.bucket(value, options?)` Consistent hashing. The same input value always maps to the same variant. Useful for user-level bucketing where a user should see the same variant across multiple runs. ```ts select: experiment.bucket(event.data.userId, { weights: { control: 70, treatment: 30 }, }) ``` When `weights` are omitted, equal weights are derived from the variant names: ```ts select: experiment.bucket(event.data.userId) ``` If `value` is `null` or `undefined`, the SDK hashes an empty string and attaches a warning to the step metadata. ### `experiment.custom(fn)` Provide your own selection logic. The function can be synchronous or asynchronous. The result is memoized durably, so it only runs once per function run. ```ts select: experiment.custom(async () => { await getFeatureFlag("checkout-variant"); return flag; // Must return a key from `variants` }) ``` The `custom` function must return a string that matches one of the keys in `variants`. If it returns an unknown variant name, the SDK throws a `NonRetriableError`. ## Observability The selection step carries experiment metadata: the experiment name, selected variant, strategy, available variants, and weights. This metadata is visible in the Inngest dashboard. Steps executed within the selected variant's callback also carry experiment context, so you can trace which experiment and variant produced each step in a run. # Handling Failures Source: https://www.inngest.com/docs/reference/typescript/v4/functions/handling-failures Description: Define a handler that runs when a function exhausts all retries for cleanup, alerting, or compensation. metaTitle = "onFailure Handler | TypeScript SDK v4 Reference" Define any failure handlers for your function with the [`onFailure`](/docs/reference/typescript/v4/functions/create#configuration) option. This function will be automatically called when your function fails after its maximum number of retries. Alternatively, you can use the [`"inngest/function.failed"`](/docs/reference/system-events/inngest-function-failed) system event to handle failures across all functions. ```ts export default inngest.createFunction( { id: "import-product-images", triggers: { event: "shop/product.imported" }, onFailure: async ({ error, event, step }) => { // This is the failure handler which can be used to // send an alert, notification, or whatever you need to do }, }, async ({ event, step, runId }) => { // This is the main function handler's code } ); ``` The failure handler is very useful for: * Sending alerts to your team * Sending metrics to a third party monitoring tool (e.g. Datadog) * Send a notification to your team or user that the job has failed * Perform a rollback of the transaction (i.e. undo work partially completed by the main handler) _Failures_ should not be confused with _Errors_ which will be retried. Read the [error handling & retries documentation](/docs/features/inngest-functions/error-retries/inngest-errors) for more context. --- ## How `onFailure` works The `onFailure` handler is a helper that actually creates a separate Inngest function used specifically for handling failures for your main function handler. The separate Inngest function utilizes an [`"inngest/function.failed"`](/docs/reference/system-events/inngest-function-failed) system event that gets sent to your account any time a function fails. The function created with `onFailure` will appear as a separate function in your dashboard with the name format: `" (failure)"`. ## `onFailure({ error, event, step, runId })` The `onFailure` handler function has the same arguments as [the main function handler](/docs/reference/typescript/v4/functions/create#handler) when creating a function, but also receives an `error` argument. ### `error` The JavaScript [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error) object as thrown from the last retry in your main function handler. The Inngest SDK attempts to serialize and deserialize the `Error` object to the best of its ability and any custom error classes (e.g. `Prisma.PrismaClientKnownRequestError` or `MyCustomErrorType`) that may be thrown will be deserialized as the default `Error` object. This means you _cannot_ use `instance` of within `onFailure` to infer the type of error. ### `event` The [`"inngest/function.failed"`](/docs/reference/system-events/inngest-function-failed) system event payload object. This object is similar to any event payload, but it contains data specific to the failed function's final retry attempt. [See the complete reference for this event payload here](/docs/reference/system-events/inngest-function-failed). ### `step` [See the `step` reference in the create function documentation](/docs/reference/typescript/v4/functions/create#step). ### `runId` This will be the function run ID for the error handling function, _not the function that failed_. To get the failed function's run ID, use `event.data.run_id`. [Learn more about `runId` here](/docs/reference/typescript/v4/functions/create#run-id). ## Examples ### Send a Slack notification when a function fails In this example, the function attempts to sync all products from a Shopify store, and if it fails, it sends a message to the team's _#eng-alerts_ Slack channel using the Slack Web Api's `chat.postMessage` ([docs](https://api.slack.com/methods/chat.postMessage)) API. ```ts export default inngest.createFunction( { id: "sync-shopify-products", triggers: { event: "shop/product_sync.requested" }, // Your handler should be an async function: onFailure: async ({ error, event }) => { event.data.event; // Post a message to the Engineering team's alerts channel in Slack: await client.chat.postMessage({ token: process.env.SLACK_TOKEN, channel: "C12345", blocks: [ { type: "section", text: { type: "mrkdwn", text: `Sync Shopify function failed for Store ${ originalEvent.storeId }: ${error.toString()}`, }, }, ], }); return result; }, }, async ({ event, step, runId }) => { // This is the main function handler's code await step.run("fetch-products", async () => { event.data.storeId; // The function might fail here or... }); await step.run("save-products", async () => { // The function might fail here after the maximum number of retries }); } ); ``` ### Capture all failure errors with Sentry Similar to the above example, you can capture and all failed functions' errors and send them to a singular place. Here's an example using [Sentry's node.js library](https://docs.sentry.io) to capture and send all failure errors to Sentry. ```ts Sentry.init({ dsn: "https://examplePublicKey@o0.ingest.sentry.io/0", }); export default inngest.createFunction( { name: "Send failures to Sentry", id: "send-failed-function-errors-to-sentry", triggers: { event: "inngest/function.failed" }, }, async ({ event, step }) => { // The error is serialized as JSON, so we must re-construct it for Sentry's error handling: event.data.error; new Error(error.message); // Set the name in the newly created event: // You can even customize the name here if you'd like, // e.g. `Function Failure: ${event.data.function_id} - ${error.name}` reerror.name; // Add the stack trace to the error: reerror.stack; // Capture the error with Sentry and append any additional tags or metadata: Sentry.captureException(reconstructedEvent,{ extra: { function_id: event.data.function_id, }, }); // Flush the Sentry queue to ensure the error is sent: return await Sentry.flush(); } ); ``` ### Additional examples # Metadata reference Source: https://www.inngest.com/docs/reference/typescript/v4/functions/metadata Description: Attach custom key-value data to function runs and steps for scoring, labeling, and observability. metaTitle = "step.metadata() | Attach Metadata to Runs (SDK v4)" `step.metadata()` is a step tool, like `step.run()`, `step.sleep()`, or `step.waitForEvent()`. It attaches custom key-value data to your function runs. Like every step tool, it takes a memoization ID, executes exactly once, and appears as its own step in the trace. The data you attach shows up in the Inngest dashboard trace view. Use it for tracking processing status, recording business-level metrics, or annotating runs with contextual information. This page covers the API surface. For practical usage examples, see the [metadata how-to guide](/docs/features/inngest-functions/steps-workflows/step-metadata-how-to). ## Setup Add `metadataMiddleware()` to your Inngest client: ```ts new Inngest({ id: "my-app", middleware: [metadataMiddleware()], }); ``` This makes `step.metadata()` available inside function handlers and `inngest.metadata` available on the client instance. ## `step.metadata(memoId)` Creates a step that attaches metadata to the current run. Like `step.run("my-id", ...)` or `step.sleep("my-id", ...)`, the `memoId` is a unique step ID that ensures the update runs exactly once, even if the function re-executes. | Method | Signature | Description | |--------|-----------|-------------| | `.run(id?)` | `(id?: string) => MetadataBuilder` | Scope to a run. Omit `id` for the current run. | | `.step(id?)` | `(id?: string) => MetadataBuilder` | Scope to a step. Omit `id` for the current step. Defaults to the current or latest attempt. | | `.span(id)` | `(id: string) => MetadataBuilder` | Scope to an extended trace span. | | `.update(values, kind?)` | `(values: Record, kind?: string) => Promise` | Send the metadata update. `kind` defaults to `"default"`. | | `.do(fn)` | `(fn: (builder: MetadataBuilder) => Promise) => Promise` | Batch multiple updates in a single memoized step. | ## `inngest.metadata` The client-level metadata builder. Same scoping methods as `step.metadata()`, but without `.do()`. Can be called inside `step.run()`, in the function body, or from external contexts. When called inside a `step.run()` callback, the metadata update is batched with the step's output. This makes it durable without creating an additional step. | Method | Signature | Description | |--------|-----------|-------------| | `.run(id?)` | `(id?: string) => MetadataBuilder` | Scope to a run. | | `.step(id?)` | `(id?: string) => MetadataBuilder` | Scope to a step. Defaults to the current or latest attempt. | | `.span(id)` | `(id: string) => MetadataBuilder` | Scope to an extended trace span. | | `.update(values, kind?)` | `(values: Record, kind?: string) => Promise` | Send the metadata update. | ## Scoping Metadata attaches at different levels of granularity. Builder methods narrow the scope. They are chainable and each one removes itself from the available methods, preventing invalid combinations. | Scope | Builder method | Description | Default when... | |-------|---------------|-------------|------------------| | `run` | `.run(id?)` | Attached to the entire function run | Outside `step.run()` | | `step` | `.step(id?)` | Attached to a specific step attempt | Inside `step.run()` | | `extended_trace` | `.span(id)` | Attached to an extended trace span | Never (must be explicit) | When you call `.update()` without setting a scope, the builder auto-detects: inside `step.run()` it defaults to the current step's attempt; outside `step.run()` it defaults to `run` scope. Metadata is always attached at the step attempt level. When a step retries, each attempt gets its own metadata. Previous attempts' metadata is preserved. ## Custom kinds The second argument to `.update()` specifies a metadata **kind**. Kinds are used to group related metadata together. When multiple updates share the same kind and scope, their values are merged. ```ts // Default kind: stored as "userland.default", displayed as "User Metadata" await step.metadata("id-1").update({ status: "processing" }); // Custom kind: stored as "userland.billing", displayed as "User Metadata (billing)" await step.metadata("id-2").update( { invoiceId: "inv_123", total: 99.99 }, "billing", ); // Another custom kind: stored as "userland.analytics", displayed as "User Metadata (analytics)" await step.metadata("id-3").update( { source: "organic", campaign: "spring-2025" }, "analytics", ); ``` | Kind string | Stored as | Dashboard label | |-------------|-----------|------------------| | *(omitted or `"default"`)* | `userland.default` | User Metadata | | `"billing"` | `userland.billing` | User Metadata (billing) | | `"analytics"` | `userland.analytics` | User Metadata (analytics) | The `inngest.*` namespace is reserved for system use. Custom kinds must not use this prefix. ## Built-in metadata kinds Inngest automatically attaches system metadata when applicable. These are read-only and managed by the platform. The `inngest.*` namespace is reserved for system use. | Kind | Label in dashboard | Description | |------|-------------------|-------------| | `inngest.ai` | AI Metadata | LLM/AI metadata, including input/output tokens, model, latency, and cost when available | | `inngest.http` | HTTP Metadata | HTTP request details: method, status code, request/response sizes | | `inngest.http.timing` | HTTP Timing | Timing breakdown: DNS, TCP, TLS, TTFB, transfer | | `inngest.response_headers` | Response Headers | HTTP response header key-value pairs | | `inngest.warnings` | Warnings | Warning messages from the execution engine | The TypeScript SDK's automatic extraction of AI metadata from OpenTelemetry spans is enabled by default. Set `aiMetadata: false` on the [Inngest client](/docs/reference/typescript/v4/client/create#aiMetadata) to opt out. ## Size limits Metadata is subject to the following size limits, enforced server-side: | Limit | Maximum size | Description | |-------|-------------|-------------| | **Per update** | **64 KB** | Maximum size of a single metadata update (span) | | **Per run** | **1 MB** | Cumulative size of all metadata across the entire function run | ### How size is calculated Size is calculated as the sum of each key's byte length plus the JSON-serialized byte length of each value: ``` size = sum(len(key) + len(JSON.stringify(value))) for each entry ``` For example, `{ "status": "ok" }` costs approximately `6 + 4 = 10 bytes` (key `"status"` = 6 bytes, value `"ok"` serialized = 4 bytes including quotes). ### Error handling When a size limit is exceeded, the server returns an HTTP `413` error: | Error | Message | |-------|---------| | Per-update exceeded | `"Metadata span exceeds maximum size of 64KB"` | | Per-run exceeded | `"Cumulative metadata size exceeds limit"` | If you're working with large metadata payloads, consider storing the data externally (e.g., in a database or object store) and attaching only a reference ID or URL as metadata. ## Limitations - **TypeScript SDK only.** Python and Go SDK support is planned but not yet available. - **Merge-only.** The only supported operation is `merge`, which combines values of the same kind. You cannot delete or replace individual metadata keys. - **Unique memoization IDs.** Each `step.metadata(memoId)` creates a step in the run. The `memoId` must be unique within the function, just like any other step ID. - **Server-side enforcement.** Size limits are enforced by the server, not the SDK. Exceeding limits returns HTTP 413 errors that surface as exceptions in your function. # Rate limit function execution Source: https://www.inngest.com/docs/reference/typescript/v4/functions/rate-limit Description: Skip function runs that exceed a configured rate over a time window to prevent abuse. metaTitle = "Rate Limit | TypeScript SDK v4 Reference" Set a _hard limit_ on how many function runs can start within a time period. Events that exceed the rate limit are _skipped_ and do not trigger functions to start. See the [Rate Limiting guide](/docs/guides/rate-limiting) for more information about how this feature works. ```ts export default inngest.createFunction( { id: "synchronize-data", triggers: { event: "intercom/company.updated" }, rateLimit: { key: "event.data.company_id", limit: 1, period: "4h", }, }, async ({ event, step }) => { // This function will be rate limited // It will only run 1 once per 4 hours for a given event payload with matching company_id } ); ``` ## Configuration Options to configure how to rate limit function execution The maximum number of functions to run in the given time period. The time period of which to set the limit. The period begins when the first matching event is received. Current permitted values are from `1s` to `24h`. A unique key expression to apply the limit to. The expression is evaluated for each triggering event. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Rate limit per customer id: `'event.data.customer_id'` * Rate limit per account and email address: `'event.data.account_id + "-" + event.user.email'` ## Examples ### Limiting synchronization triggered by webhook events In this example, we use events from the Intercom webhook. The webhook can be overly chatty and send multiple `intercom/company.updated` events in a short time window. We also only really care to sync the user's data from Intercom no more than 4 times per day, so we set our limit to `6h`: ```ts /** Example event payload: { name: "intercom/company.updated", data: { company_id: "123456789", company_name: "Acme, Inc." } } */ export default inngest.createFunction( { id: "synchronize-data", triggers: { event: "intercom/company.updated" }, rateLimit: { key: "event.data.company_id", limit: 1, period: "4h", }, }, async ({ event, step }) => { await step.run( "fetch-latest-company-data-from-intercom", async () => { return await client.companies.find({ companyId: event.data.company_id, }); } ); await step.run("update-company-data-in-database", async () => { return await database.companies.upsert({ id: company.id }, company); }); } ); ``` ### Send at most one email for multiple alerts over an hour When there is an issue in your system, you may want to send your user an email notification, but don't want to spam them. The issue may repeat several times within the span of few minutes, but the user really just needs one email. You can ```ts /** Example event payload: { name: "service/check.failed", data: { incident_id: "01HB9PWHZ4CZJYRAGEY60XEHCZ", issue: "HTTP uptime check failed at 2023-09-26T21:23:51.515631317Z", user_id: "user_aW5uZ2VzdF9pc19mdWNraW5nX2F3ZXNvbWU=", service_name: "api", service_id: "01HB9Q2EFBYG2B7X8VCD6JVRFH" }, user: { external_id: "user_aW5uZ2VzdF9pc19mdWNraW5nX2F3ZXNvbWU=", email: "user@example.com" } } */ export default inngest.createFunction( { id: "send-check-failed-notification", triggers: { event: "service/check.failed" }, rateLimit: { // Don't send duplicate emails to the same user for the same service over 1 hour key: `event.data.user_id + "-" + event.data.service_id`, limit: 1, period: "1h", }, }, async ({ event, step }) => { await step.run("send-alert-email", async () => { return await resend.emails.send({ from: "notifications@myco.com", to: event.user.email, subject: `ALERT: ${event.data.issue}`, text: `Dear user, ...`, }); }); } ); ``` # Referencing functions Source: https://www.inngest.com/docs/reference/typescript/v4/functions/references Description: Look up a registered Inngest function by ID using inngest.getFunction() for dynamic invocation. metaTitle = "Function References | inngest.getFunction() (SDK v4)" Using [`step.invoke()`](/docs/reference/typescript/v4/functions/step-invoke), you can directly call one Inngest function from another and handle the result. You can use this with `referenceFunction` to call Inngest functions located in other apps, or to avoid importing dependencies of functions within the same app. ```ts // @/inngest/compute.ts // Create a local reference to a function without importing dependencies computePi = referenceFunction({ functionId: "compute-pi", }); // Create a reference to a function in another application computeSquare = referenceFunction({ appId: "my-python-app", functionId: "compute-square", // Schemas are optional, but provide types for your call if specified schemas: { data: z.object({ number: z.number(), }), return: z.object({ result: z.number(), }), }, }); ``` ```ts // @/inngest/someFn.ts // import the referenece // square.result is typed as a number await step.invoke("compute-square-value", { function: computeSquare, data: { number: 4 }, // input data is typed, requiring input if it's needed }); ``` ## How to use `referenceFunction` The simplest reference just contains a `functionId`. When used, this will invoke the function with the given ID in the same app that is used to invoke it. The input and output types are `unknown`. ```ts await step.invoke("start-process", { function: referenceFunction({ functionId: "some-fn", }), }); ``` If referencing a function in a different application, specify an `appId` too: ```ts await step.invoke("start-process", { function: referenceFunction({ functionId: "some-fn", appId: "some-app", }), }); ``` You can optionally provide `schemas`, which are a collection of [Standard Schemas](https://standardschema.dev/) used to provide typing to the input and output of the referenced function. In the future, this will also _validate_ the input and output. ```ts await step.invoke("start-process", { function: referenceFunction({ functionId: "some-fn", appId: "some-app", schemas: { data: z.object({ foo: z.string(), }), return: z.object({ success: z.boolean(), }), }, }), }); ``` Even if functions are within the same app, this can also be used to avoid importing the dependencies of one function into another, which is useful for frameworks like Next.js where edge and serverless logic can be colocated but require different dependencies. ```ts // import only the type await step.invoke("start-process", { function: referenceFunction({ functionId: "some-fn", }), }); ``` ## Configuration The ID of the function to reference. This can be either a local function ID or the ID of a function that exists in another app. If the latter, `appId` must also be provided. If `appId` is not provided, the function ID will be assumed to be a local function ID (the app ID of the calling app will be used). The ID of the app that the function belongs to. This is only required if the function being refenced exists in another app. The schemas of the referenced function, providing typing to the input `data` and `return` of invoking the referenced function. If not provided and a local function type is not being passed as a generic into `referenceFunction()`, the schemas will be inferred as `unknown`. The [Standard Schema](https://standardschema.dev/) to use to provide typing to the `data` payload required by the referenced function. The [Standard Schema](https://standardschema.dev/) to use to provide typing to the return value of the referenced function when invoked. # Function run priority Source: https://www.inngest.com/docs/reference/typescript/v4/functions/run-priority Description: Dynamically rank function runs using a CEL expression over event or environment data. metaTitle = "Function Run Priority | TypeScript SDK v4 Reference" You can prioritize specific function runs above other runs **within the same function**. See the [Priority guide](/docs/guides/priority) for more information about how this feature works. ```ts export default inngest.createFunction( { id: "ai-generate-summary", triggers: { event: "ai/summary.requested" }, priority: { // For enterprise accounts, a given function run will be prioritized // ahead of functions that were enqueued up to 120 seconds ago. // For all other accounts, the function will run with no priority. run: "event.data.account_type == 'enterprise' ? 120 : 0", }, }, async ({ event, step }) => { // This function will be prioritized based on the account type } ); ``` ## Configuration Options to configure how to prioritize functions An expression which must return an integer between -600 and 600 (by default), with higher return values resulting in a higher priority. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Return the priority within an event directly: `event.data.priority` (where `event.data.priority` is an int within your account's range) * Prioritize by a string field: `event.data.plan == 'enterprise' ? 180 : 0` Return values outside of your account's range (by default, -600 to 600) will automatically be clipped to your max bounds. An invalid expression will evaluate to 0, as in "no priority". # Scoring Source: https://www.inngest.com/docs/reference/typescript/v4/functions/scoring Description: Attach numeric or boolean quality scores to AI function runs, steps, and experiment variants to track LLM-as-a-judge evals and performance across model versions. metaTitle = "inngest.score() | Score AI Outputs (TypeScript SDK v4)" Score function runs and steps to track how well your workflows perform over time. Scoring is in beta. Install the latest SDK with `npm install inngest@latest`. --- ## `inngest.score(options)` Score a run or step. Can be called inside or outside a function. A label for the score. Use consistent names across runs for aggregation. Examples: `"accuracy"`, `"user-feedback"`, `"guardrail-pass"`. The score value. A finite number or a boolean. Use `0`-`1` for percentages, integers for counts, booleans for pass/fail guardrails, or any range that fits your use case. The run to score. Required when scoring from outside a function. Omit when scoring the current run from inside a function. The step to score. Optional. When provided, the score attaches to that specific step in the trace view. ```ts // Score the current run (inside a function) await inngest.score({ name: "accuracy", value: 0.9 }); // Score a specific run (from anywhere) await inngest.score({ name: "accuracy", value: 0.9, runId: "01ABC..." }); // Score a specific step await inngest.score({ name: "accuracy", value: 0.9, runId: "01ABC...", stepId: "generate-summary", }); ``` --- ## `inngest.score.experiment(options)` Attribute a score to the experiment variant that produced a result. `group.experiment()` returns an `experimentRef`, which identifies the experiment and the variant that was served. Pass it here to credit the score to that variant. The signal you want to score often arrives much later, from somewhere else entirely: a click, a rating, an LLM-judge verdict. Because you pass the `experimentRef` yourself, you can credit any of these back to the variant that produced the output, even hours later from a separate run. Score label. Score value. A finite number or a boolean. The `experimentRef` returned by `group.experiment()`. Identifies the experiment and the served variant. Shape: `{ experimentName: string, variant: string }`. The run to attach the score to. Required when scoring from outside the run that produced the result; omit to use the current run. Scoring an experiment from a different run than the one that served the variant? Pass that run's `runId` so the score shows up in the experiment view — otherwise it attaches to the run you're scoring from and won't appear under the experiment. ```ts // Inside the function that runs the experiment await group.experiment("summary-strategy", { variants: { /* ... */ }, select: experiment.weighted({ questionLed: 50, factLed: 50 }), }); // Persist experimentRef + runId (e.g. on your record) so later runs can score it // From a separate, later run (a click, rating, or deferred judge) await inngest.score.experiment({ name: "clickthrough", value: 1, experiment: experimentRef, // restored from your store runId: summarizeRunId, }); ``` --- ## `step.score(id, options)` Score the current run from within a step context, as a durable, memoized step. `runId` is automatically set to the current run. Requires `scoreMiddleware()` (from `inngest/experimental`) on the client. A durable step ID used to memoize this score write, so a replay or retry can't write it twice. Must be unique within the run. Score label. Score value. A finite number or a boolean. ```ts await step.score("score-guardrail-pass", { name: "guardrail-pass", value: true }); ``` --- ## `createScorer(client, config, handler)` Create a reusable deferred scorer: a separate function, triggered via `defer()`, that evaluates a result without blocking or slowing the run that produced it. Use it when scoring is expensive or slow, such as an LLM-as-a-judge call or waiting on a signal that arrives later. Imported from `inngest/experimental`. Your Inngest client instance. Unique identifier for the scorer function. A [Standard Schema](https://standardschema.dev) (e.g. Zod) describing the input data the scorer expects. Optional; when set, the `data` passed to `defer()` is validated against it and the handler's `event.data` is typed from it. Async function that receives `event`, `step`, and `parents`. Return `{ name, value }` and the score is written for you, attributed to the parent run, and to the served variant when the scorer was deferred with an `experiment` ref. Return `null` or `undefined` to write nothing. The handler's `event.data` is the payload sent via `defer()`, typed from `schema`. The simplest scorer returns one score and lets the SDK attribute it: ```ts verbosityScorer = createScorer( inngest, { id: "verbosity-scorer", schema: z.object({ text: z.string() }) }, async ({ event }) => { event.data.text.split(" ").length; return { name: "verbosity", value: wordCount }; } ); ``` To emit several scores, or to attribute explicitly, write them yourself with `inngest.score.experiment()`. The parent run and its served variant are on `ctx.parents[0]`, so there's no need to persist them: ```ts judgeScorer = createScorer( inngest, { id: "judge-summary", schema: z.object({ summary: z.string() }) }, async ({ event, step, parents }) => { await step.run("judge", () => judge(event.data.summary)); parents[0]; await inngest.score.experiment({ name: "faithfulness", value: judged.faithfulness, experiment, runId }); await inngest.score.experiment({ name: "spoiler", value: judged.spoiler, experiment, runId }); return null; // scores already written above } ); ``` --- ## `defer(id, options)` Trigger a deferred scorer from inside a function. Available as a handler argument. A deterministic identifier for this deferred call. The scorer function to trigger. Input data matching the scorer's schema. The `experimentRef` from `group.experiment()`. Pass it to attribute the scorer's result to the served variant; surfaced on the scorer's `ctx.parents[0].experiment`. `defer()` is fire-and-forget: it returns `void`, so there's nothing to `await`. ```ts defer("score-feedback", { function: feedbackScorer, data: { ticketId: "tk_123" }, experiment: experimentRef, // optional; credits the score to the served variant }); ``` --- ## Further reading - [Score a function run](/docs/features/inngest-functions/steps-workflows/scoring) - [Build a deferred scorer](/docs/features/inngest-functions/steps-workflows/deferred-scoring) - [Run experiments](/docs/patterns/ai-evals/run-experiments-in-production) # Ensure exclusive execution of a function Source: https://www.inngest.com/docs/reference/typescript/v4/functions/singleton Description: Ensure only one run executes at a time; new invocations are queued or skipped until the current run completes. metaTitle = "Singleton (Exclusive Execution) | TypeScript SDK v4" Ensure that only a single run of a function (_or a set of specific functions, based on specific event properties_) is running at a time. See the [Singleton Functions guide](/docs/guides/singleton) for more information about how this feature works. ```ts export default inngest.createFunction( { id: "data-sync", triggers: { event: "data-sync.start" }, singleton: { key: "event.data.user_id", mode: "skip", }, }, async ({ event }) => { // This function will be skipped if another run of the same function is already running for the same user } ); ``` ## Configuration Options to configure exclusive execution of a function. A unique key expression to which the limit is applied. This expression is evaluated for each triggering event. Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Ensure exclusive execution of a function per customer ID: `'event.data.customer_id'` * Ensure exclusive execution of a function per account and email address: `'event.data.account_id + "-" + event.user.email'` The mode to use for the singleton function: * `"skip"`: Skip the new run. * `"cancel"`: Cancel the existing run and start the new one. ## Examples ### Ensure executing only upon the latest event In this example, the active run of our `data-sync` function will be cancelled if another event with the same `user_id` is received: ```ts // Example event payload: // { // name: "data-sync.start", // data: { // user_id: "123456789", // } // } export default inngest.createFunction( { id: "data-sync", triggers: { event: "data-sync.start" }, singleton: { key: "event.data.user_id", mode: "cancel", }, }, async ({ event, step }) => { await step.run( "fetch-latest-data-from-source", async () => { return await client.fetchData(event.data.user_id); } ); await step.run("update-data-in-database", async () => { return await database.upsert({ id: company.id }, company); }); } ); ``` While similar to [Debounce](/docs/guides/debounce), Singleton Functions are designed to ensure that only a single run of a function is happening at a time, whereas Debounce ensures that only a single event is processed within a given time window. Refer to the [Singleton Functions guide](/docs/guides/singleton) for more information about how this feature works. # Invoke Source: https://www.inngest.com/docs/reference/typescript/v4/functions/step-invoke Description: Call another Inngest function from within a step using step.invoke() in TypeScript SDK v4 and await its typed return value. metaTitle = "step.invoke() | TypeScript SDK v4 Reference" Use `step.invoke()` to asynchronously call another function and handle the result. Invoking other functions allows you to easily re-use functionality and compose them to create more complex workflows or map-reduce type jobs. `step.invoke()` returns a `Promise` that resolves with the return value of the invoked function. ```ts // Some function we'll call inngest.createFunction( { id: "compute-square", triggers: { event: "calculate/square" }, }, async ({ event }) => { return { result: event.data.number * event.data.number }; // Result typed as { result: number } } ); // In this function, we'll call `computeSquare` inngest.createFunction( { id: "main-function", triggers: { event: "main/event" }, }, async ({ step }) => { await step.invoke("compute-square-value", { function: computeSquare, data: { number: 4 }, // input data is typed, requiring input if it's needed }); return `Square of 4 is ${square.result}.`; // square.result is typed as number } ); ``` Use the [`invoke()` trigger helper](/docs/reference/typescript/v4/functions/triggers#invoke) to define a typed schema for `step.invoke()` input data on the target function. ## `step.invoke(id, options): Promise` The ID of the invocation. This is used in logs and to keep track of the invocation's state across different versions. Options for the invocation: A local instance of a function or a reference to a function to invoke. Optional data to pass to the invoked function. Will be required and typed if it can be. Optional user context for the invocation. Optional event metadata for the invocation. [Sessions](/docs/features/events-triggers/sessions?ref=docs-reference-step-invoke) to add to or override on the invoked run, which otherwise inherits the calling run's sessions. A `null` value clears an inherited session for that key; setting `sessions` itself to `null` clears every inherited session. Numbers are normalized to strings. {/* Purposefully not mentioning the default timeout of 1 year, as we expect to lower this very soon. */} How long to wait for the invoked function to complete. Either: - A duration string compatible with the [ms](https://npm.im/ms) package, e.g. `"30m"`, `"3 hours"`, or `"2.5d"`, - A `number` of milliseconds, - An absolute `Date`, - A [`Temporal.Duration`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration) for a relative wait, or - A [`Temporal.Instant`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant) or [`Temporal.ZonedDateTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime) for an absolute deadline. If the timeout is reached, the step will throw an error. See [Error handling](#error-handling) below. Note that the invoked function will continue to run even if this step times out. Throwing errors within the invoked function will be reflected in the invoking function. ```ts await step.invoke("invoke-by-definition", { function: anotherFunction, data: { ... }, }); ``` ```ts await step.invoke("invoke-by-reference", { function: referenceFunction(...), data: { ... }, }); ``` ```ts await step.invoke("invoke-with-timeout", { function: anotherFunction, data: { ... }, timeout: "1h", }); ``` ## How to call `step.invoke()` Handling `step.invoke()` is similar to handling any other Promise in JavaScript: ```ts // Using the "await" keyword await step.invoke("invoke-function", { function: someInngestFn, data: { ... }, }); // Using `then` for chaining step .invoke("invoke-function", { function: someInngestFn, data: { ... } }) .then((result) => { // further processing }); // Running multiple invocations in parallel Promise.all([ step.invoke("invoke-first-function", { function: firstFunctionReference, data: { ... }, }), step.invoke("invoke-second-function", { function: secondFn, data: { ... }, }), ]); ``` ## Using function references Instead of directly importing a local function to invoke, [`referenceFunction()`](/docs/functions/references) can be used to call an Inngest function located in another app, or to avoid importing the dependencies of a function within the same app. ```ts // Create a local reference to a function without importing dependencies referenceFunction({ functionId: "compute-pi", }); // Create a reference to a function in another application referenceFunction({ appId: "my-python-app", functionId: "compute-square", }); // square.result is typed as a number await step.invoke("compute-square-value", { function: computePi, data: { number: 4 }, // input data is typed, requiring input if it's needed }); ``` See [Referencing functions](/docs/functions/references) for more information. ## When to use `step.invoke()` Use of `step.invoke()` to call an Inngest function directly is more akin to traditional RPC than Inngest's usual event-driven flow. While this tool still uses events behind the scenes, you can use it to help break up your codebase into reusable workflows that can be called from anywhere. Use `step.invoke()` in tasks that need specific settings like concurrency limits. Because it runs with its own configuration, distinct from the invoker's, you can provide a tailored configuration for each function. If you don't need to define granular configuration or if your function won't be reused across app boundaries, use `step.run()` for simplicity. ## Internal behaviour When a function object is passed as an argument, internally, the SDK retrieves the function's ID automatically. For cross-app invocation, use [`referenceFunction()`](/docs/functions/references) to create a typed reference. See [Error handling](#error-handling) for more information. When Inngest receives the request to invoke a function, it'll do so and wait for an `inngest/function.finished` event, which it will use to fulfil the data (or error) for the step. ## Return values and serialization Similar to `step.run()`, all data returned from `step.invoke()` is serialized as JSON. This is done to enable the SDK to return a valid serialized response to the Inngest service. ## Timeout If not explicitly configured, the default timeout for `step.invoke` is 1 year. ## Retries The invoked function will be executed as a regular Inngest function: it will have its own set of retries and can be seen as a brand new run. If a `step.invoke()` fails for any of the reasons below, it will throw a `NonRetriableError`. This is to combat compounding retries, such that chains of invoked functions can be executed many more times than expected. For example, if A invokes B which invokes C, which invokes D, on failure D would be run 27 times (`retryCount^n`). ## Error handling ### Function not found If Inngest could not find a function to invoke using the given ID (see [Internal behaviour](#internal-behaviour) above), an `inngest/function.finished` event will be sent with an appropriate error and the step will fail with a `NonRetriableError`. ### Invoked function fails If the function exhausts all retries and fails, an `inngest/function.finished` event will be sent with an appropriate error and the step will fail with a `NonRetriableError`. ### Invoked function times out If the `timeout` has been reached and the invoked function is still running, the step will fail with a `NonRetriableError`. ### Invoked function is rate limited If the called function has a rate limit configuration and is skipped, the step will fail with a `NonRetriableError`. It's recommended to wrap the `step.invoke` with a `try catch` if the invoked function is expected to be executing occasionally. ### Invoked function is debounced If the called function has a debounce configuration and is skipped, the step will fail with a `NonRetriableError` after the timeout has been reached. It is preferable to always set a meaningful timeout when invoking a function with debounce configuration. ## Usage limits See [usage limits][usage-limits] for more details. [usage-limits]: /docs/usage-limits/inngest#functions # Run Source: https://www.inngest.com/docs/reference/typescript/v4/functions/step-run Description: step.run() in TypeScript SDK v4 wraps any logic in a memoized, retriable, observable step visible in the Inngest dashboard run trace. metaTitle = "step.run() | Execute a Step (TypeScript SDK v4)" Use `step.run()` to run synchronous or asynchronous code as a retriable step in your function. `step.run()` returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves with the return value of your handler function. ```ts export default inngest.createFunction( { id: "import-product-images", triggers: { event: "shop/product.imported" }, }, async ({ event, step }) => { await step.run("copy-images-to-s3", async () => { return copyAllImagesToS3(event.data.imageURLs); }); } ); ``` --- ## `step.run(id, handler): Promise` The ID of the step. This will be what appears in your function's logs and is used to memoize step state across function versions. The function that code that you want to run and automatically retry for this step. Functions can be: * A synchronous function * An `async` function * Any function that returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) Throwing errors within the handler function will trigger the step to be retried ([reference](/docs/features/inngest-functions/error-retries/inngest-errors)). ```ts // Steps can have async handlers await step.run("get-api-data", async () => { // Steps should return data used in other steps return fetch("...").json(); }); // Steps can have synchronous handlers await step.run("transform", () => { return transformData(result); }); // Returning data is optional await step.run("insert-data", async () => { db.insert(data); }); ``` ## How to call `step.run()` As `step.run()` returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), you will need to handle it like any other Promise in JavaScript. Here are some ways you can use `step.run()` in your code: ```ts // Use the "await" keyword to wait for the promise to fulfil await step.run("create-user", () => {/* ... */}); await step.run("create-user", () => {/* ... */}); // Use `then` (or similar) step.run("create-user", () => {/* ... */}) .then((user) => { // do something else }); // Use with a Promise helper function to run in parallel Promise.all([ step.run("create-subscription", () => {/* ... */}), step.run("add-to-crm", () => {/* ... */}), step.run("send-welcome-email", () => {/* ... */}), ]); ``` ## Retries Each `step.run()` call has its own independent retry counter. When a step throws an error, it will be retried according to your function's retry configuration. The retry configuration applies to each individual step, not as a shared pool across all steps in your function. For example, if your function is configured with `retries: 4`, each `step.run()` will be retried up to 4 times independently (5 total attempts including the initial attempt). If you have multiple steps in your function, each step gets its own full set of retries. Learn more about [configuring retries](/docs/features/inngest-functions/error-retries/retries). ## Return values and serialization All data returned from `step.run` is serialized as JSON. This is done to enable the SDK to return a valid serialized response to the Inngest service. ```ts await step.run("create-user", () => { return { id: new ObjectId(), createdAt: new Date() }; }); /* { "id": "647731d1759aa55be43b975d", "createdAt": "2023-05-31T11:39:18.097Z" } */ ``` ## Usage limits See [usage limits][usage-limits] for more details. [usage-limits]: /docs/usage-limits/inngest#functions # Send Event Source: https://www.inngest.com/docs/reference/typescript/v4/functions/step-send-event Description: Send events from inside a function step using step.sendEvent() in TypeScript SDK v4. Events are sent atomically as part of the step lifecycle. metaTitle = "step.sendEvent() | Send Events from a Step (SDK v4)" Use to send event(s) reliably within your function. Use this instead of [`inngest.send()`](/docs/reference/typescript/v4/events/send) to ensure reliable event delivery from within functions. This is especially useful when [creating functions that fan-out](/docs/guides/fan-out-jobs). ```ts export default inngest.createFunction( { id: "user-onboarding", triggers: { event: "app/user.signup" }, }, async ({ event, step }) => { // Do something await step.sendEvent("send-activation-event", { name: "app/user.activated", data: { userId: event.data.userId }, }); // Do something else } ); ``` To send events from outside of the context of a function, use [`inngest.send()`](/docs/reference/typescript/v4/events/send). Use [`eventType().create()`](/docs/reference/typescript/v4/functions/triggers#with-inngestsend-and-stepsendevent) to build fully typed event payloads for `step.sendEvent()`. --- ## `step.sendEvent(id, eventPayload | eventPayload[]): Promise<{ ids: string[] }>` The ID of the step. This will be what appears in your function's logs and is used to memoize step state across function versions. An event payload object or an array of event payload objects. [See the documentation for `inngest.send()`](/docs/reference/typescript/v4/events/send#inngest-send-event-payload-event-payload-promise) for the event payload format. Events sent from within a run inherit the run's [sessions](/docs/features/events-triggers/sessions?ref=docs-reference-step-send-event). Use `meta.sessions` on the payload to add or override them. ```ts // Send a single event await step.sendEvent("send-activation-event", { name: "app/user.activated", data: { userId: "01H08SEAXBJFJNGTTZ5TAWB0BD" }, }); // Send an array of events await step.sendEvent("send-invoice-events", [ { name: "app/invoice.created", data: { invoiceId: "645e9e024befa68763f5b500" }, }, { name: "app/invoice.created", data: { invoiceId: "645e9e08f29fb563c972b1f7" }, }, ]); ``` `step.sendEvent()` must be called using `await` or some other Promise handler to ensure your function sleeps correctly. ### Return values The function returns a promise that resolves to an object with an array of Event IDs that were sent. These events can be used to look up the event in the Inngest dashboard or via [the REST API](https://api-docs.inngest.com/v1/events/GetEvent). ```ts await step.sendEvent("send-invoices", [ { name: "app/invoice.created", data: { invoiceId: "645e9e024befa68763f5b500" } }, { name: "app/invoice.created", data: { invoiceId: "645e9e08f29fb563c972b1f7" } }, ]); /** * ids = [ * "01HQ8PTAESBZPBDS8JTRZZYY3S", * "01HQ8PTFYYKDH1CP3C6PSTBZN5" * ] */ ``` # Sleep until `step.sleepUntil()` Source: https://www.inngest.com/docs/reference/typescript/v4/functions/step-sleep-until Description: Pause an Inngest function until a specific Date or ISO timestamp using step.sleepUntil() in TypeScript SDK v4. Supports durations up to 1 year. metaTitle = "step.sleepUntil() | Sleep to a Date (TypeScript SDK v4)" Use `step.sleepUntil()` to pause your function's execution until a specific date and time. This is useful when you need to wait until a known point in time, such as the end of a trial period or a scheduled deadline. ## `step.sleepUntil(id, datetime): Promise` The ID of the step. This will be what appears in your function's logs and is used to memoize step state across function versions. The datetime at which to continue execution of your function. This can be: * A [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object * Any date time `string` in [the format accepted by the `Date` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format), i.e. `YYYY-MM-DDTHH:mm:ss.sssZ` or simplified forms like `YYYY-MM-DD` or `YYYY-MM-DDHH:mm:ss` * [`Temporal.Instant`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant) * [`Temporal.ZonedDateTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime) ```ts // Sleep until the new year await step.sleepUntil("happy-new-year", "2024-01-01"); // Sleep until September ends await step.sleepUntil("wake-me-up", "2023-09-30T11:59:59"); // Sleep until the end of the this week dayjs().endOf("week").toDate(); await step.sleepUntil("wait-for-end-of-the-week", date); // Sleep until tea time in London Temporal.ZonedDateTime.from("2025-05-01T16:00:00+01:00[Europe/London]"); await step.sleepUntil("british-tea-time", teaTime); // Sleep until the end of the day Temporal.Now.instant(); now.round({ smallestUnit: "day", roundingMode: "ceil" }); await step.sleepUntil("done-for-today", endOfDay); ``` `step.sleepUntil()` must be called using `await` or some other Promise handler to ensure your function sleeps correctly. # Sleep `step.sleep()` Source: https://www.inngest.com/docs/reference/typescript/v4/functions/step-sleep Description: Pause an Inngest function for a duration using step.sleep() in TypeScript SDK v4. The function resumes without re-running prior steps or consuming compute. metaTitle = "step.sleep() | Pause a Function (TypeScript SDK v4)" Use `step.sleep()` to pause your function's execution for a specified duration. This is useful for adding delays between steps, such as waiting before sending a follow-up email or polling an external service at intervals. ## `step.sleep(id, duration): Promise` The ID of the step. This will be what appears in your function's logs and is used to memoize step state across function versions. The duration of time to sleep: * `number` of milliseconds * `string` compatible with the [ms](https://npm.im/ms) package, e.g. `"30m"`, `"3 hours"`, or `"2.5d"` * [`Temporal.Duration`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration) ```ts // Sleep for 30 minutes Temporal.Duration.from({ minutes: 30 }); await step.sleep("wait-with-temporal", thirtyMins); await step.sleep("wait-with-string", "30m"); await step.sleep("wait-with-string-alt", "30 minutes"); await step.sleep("wait-with-ms", 30 * 60 * 1000); ``` `step.sleep()` must be called using `await` or some other Promise handler to ensure your function sleeps correctly. # Wait for event Source: https://www.inngest.com/docs/reference/typescript/v4/functions/step-wait-for-event Description: Pause a function and resume when a matching event arrives using step.waitForEvent() in SDK v4. Configure timeout, correlation expression, and event data types. metaTitle = "step.waitForEvent() | TypeScript SDK v4 Reference" Use `step.waitForEvent()` to pause your function's execution until a matching event is received or a timeout is reached. This is useful for building [human-in-the-loop workflows](/docs/ai-patterns/human-in-the-loop), waiting for approvals, or coordinating between separate functions. ## `step.waitForEvent(id, options): Promise` The ID of the step. This will be what appears in your function's logs and is used to memoize step state across function versions. Options for configuring how to wait for the event. The name of a given event to wait for, or an [`eventType()`](/docs/reference/typescript/v4/functions/triggers#eventtype) result for typed return values. How long to wait to receive the event. Either: - A duration string compatible with the [ms](https://npm.im/ms) package, e.g. `"30m"`, `"3 hours"`, or `"2.5d"`, - A `number` of milliseconds, - An absolute `Date`, - A [`Temporal.Duration`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration) for a relative wait, or - A [`Temporal.Instant`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant) or [`Temporal.ZonedDateTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime) for an absolute deadline. The property to match the event trigger and the wait event, using dot-notation, e.g. `data.userId`. Cannot be combined with `if`. An expression on which to conditionally match the original event trigger (`event`) and the wait event (`async`). Cannot be combined with `match`. Expressions are defined using the Common Expression Language (CEL) with the events accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * `event.data.userId == async.data.userId && async.data.billing_plan == 'pro'` ```ts // Wait 7 days for an approval and match invoice IDs await step.waitForEvent("wait-for-approval", { event: "app/invoice.approved", timeout: "7d", match: "data.invoiceId", }); // Wait 30 days for a user to start a subscription // on the pro plan await step.waitForEvent("wait-for-subscription", { event: "app/subscription.created", timeout: "30d", if: "event.data.userId == async.data.userId && async.data.billing_plan == 'pro'", }); // Wait until a specific deadline await step.waitForEvent("wait-for-response", { event: "app/response.received", timeout: new Date("2026-05-01T00:00:00Z"), match: "data.requestId", }); // Use eventType() for typed return values eventType("app/approval.received", { schema: z.object({ approved: z.boolean() }), }); await step.waitForEvent("wait-for-approval", { event: approvalType, timeout: "7d", }); // approval?.data is typed as { approved: boolean } ``` `step.waitForEvent()` must be called using `await` or some other Promise handler to ensure your function sleeps correctly. **No `in` operator support. Use multiple equality checks with `||` instead.** CEL's `in` operator is not supported with `if` expressions, instead, use multiple equality checks (`==`) with an or operator (`||`) to achieve the same result. ```plaintext // Do not use this: // async.data.function_id in ["app-test-a", "app-test-b"] // Use this: (async.data.function_id == "app-test-a" || async.data.function_id == "app-test-b") ``` See [GitHub issue #3907](https://github.com/inngest/inngest/issues/3907) for tracking. Inside a `group.parallel()` race, a losing `step.waitForEvent()` is not cancelled: it remains an active pause and keeps the run in a `Running` state until its `timeout` is reached. See the [step parallelism guide](/docs/guides/step-parallelism) for details. # Wait for signal Source: https://www.inngest.com/docs/reference/typescript/v4/functions/step-wait-for-signal Description: Pause a function and resume when a named signal is sent using step.waitForSignal() in SDK v4. Ideal for human approval and external trigger workflows. metaTitle = "step.waitForSignal() | TypeScript SDK v4 Reference" Wait for a particular signal to be received before continuing with step.waitForSignal(). You must resume signals by calling `step.sendSignal` or `client.sendSignal` (as a step or on the Inngest client), passing the same signal string and any data you want to inject into the function run. ## `step.waitForSignal(id, options): Promise` The ID of the step. This will be what appears in your function's logs and is used to memoize step state across function versions. Options for configuring how to wait for the event. A unique identifier for the signal, used to resume this function run. How long to wait to receive the signal. Either: - A duration string compatible with the [ms](https://npm.im/ms) package, e.g. `"30m"`, `"3 hours"`, or `"2.5d"`, - A `number` of milliseconds, - An absolute `Date`, - A [`Temporal.Duration`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration) for a relative wait, or - A [`Temporal.Instant`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant) or [`Temporal.ZonedDateTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime) for an absolute deadline. If the signal is not received before this timeout, the run will resume with an undefined return value. How to handle duplicate signals. By default, step.waitForSignal requires a unique signal and duplicate signals will fail the function. Set to 'replace' to replace any other run's signal. Note that previous runs will NOT resume from this signal if replaced, and instead will time out. To resume many runs from a single signal, use `step.waitForEvent`. ```ts // Wait 7d for an approval await step.waitForSignal("wait-for-approval", { signal: "task/71651db4-9f27-466a-a6be-4759b9784b3c", timeout: "7d", }); ``` `step.waitForSignal()` must be called using `await` or some other Promise handler to ensure your function pauses correctly. `step.waitForSignal()` must be resumed by `inngest.sendSignal`, `step.sendSignal`, or a call to the signal API to be resumed with a matching signal string. # Trigger helpers Source: https://www.inngest.com/docs/reference/typescript/v4/functions/triggers Description: TypeScript SDK v4 trigger helpers: eventTrigger(), cron(), invoke(), and staticSchema() for typed, composable function trigger definitions. metaTitle = "Trigger Helpers | eventTrigger(), cron() (SDK v4)" Trigger helpers are functions that provide type-safe ways to define triggers and schemas for Inngest functions. They replace raw string-based triggers with typed, validated alternatives. ```ts ``` --- ## `eventType` Define a typed event with optional runtime validation. Use the returned object as a [trigger](/docs/reference/typescript/v4/functions/create#triggers), with [`step.waitForEvent()`](/docs/reference/typescript/v4/functions/step-wait-for-event), and with [`inngest.send()`](/docs/reference/typescript/v4/events/send) or [`step.sendEvent()`](/docs/reference/typescript/v4/functions/step-send-event). ```ts eventType("app/account.created", { schema: z.object({ userId: z.string(), email: z.string(), }), }); ``` ### Parameters The event name. We recommend lowercase dot notation with a `prefix/` namespace, e.g. `"app/account.created"`. A schema for the event's `data` payload. Accepts any [Standard Schema](https://standardschema.dev/) compatible library (Zod, Valibot, ArkType, etc.) or a [`staticSchema()`](#staticschema) for type-only validation. When you provide a runtime schema, the SDK validates event payloads at runtime. An optional version identifier for the event payload, e.g. `"2024-01-15.1"`. This sets the `v` field on sent events. ### Return value `eventType()` returns an `EventType` object. The only use case for directly interacting with this object is calling its `create` method when sending events. ### As a trigger Pass an `eventType()` result directly in the `triggers` array of `createFunction`: ```ts eventType("shop/order.placed", { schema: z.object({ orderId: z.string(), total: z.number(), }), }); inngest.createFunction( { id: "process-order", triggers: [orderPlaced], }, async ({ event }) => { // event.data is typed as { orderId: string; total: number } console.log(event.data.orderId); } ); ``` You can also combine with `if` filtering by wrapping in an object: ```ts inngest.createFunction( { id: "process-large-orders", triggers: [{ event: orderPlaced, if: "event.data.total > 100", }], }, async ({ event }) => { // Only runs for orders over 100 } ); ``` ### With `step.waitForEvent()` Pass an `eventType()` result as the `event` option to get typed return values: ```ts eventType("app/approval.received", { schema: z.object({ approved: z.boolean(), approvedBy: z.string(), }), }); inngest.createFunction( { id: "approval-workflow", triggers: [orderPlaced], }, async ({ event, step }) => { await step.waitForEvent("wait-for-approval", { event: approvalReceived, timeout: "7d", match: "data.orderId", }); if (approval) { // approval.data is typed as { approved: boolean; approvedBy: string } } } ); ``` ### With `inngest.send()` and `step.sendEvent()` Use the `.create()` method to build fully typed event payloads: ```ts // With inngest.send() await inngest.send( orderPlaced.create({ orderId: "order_123", total: 99.99, }) ); // With step.sendEvent() await step.sendEvent("notify-order", orderPlaced.create({ orderId: "order_456", total: 149.99, }) ); ``` ### Multiple event types When a function has multiple event triggers, use `event.name` to narrow the type: ```ts eventType("app/user.created", { schema: z.object({ userId: z.string() }), }); eventType("app/user.updated", { schema: z.object({ userId: z.string(), changes: z.record(z.unknown()) }), }); inngest.createFunction( { id: "sync-user", triggers: [userCreated, userUpdated], }, async ({ event }) => { if (event.name === "app/user.created") { // event.data typed as { userId: string } } if (event.name === "app/user.updated") { // event.data typed as { userId: string; changes: Record } } } ); ``` ### Wildcards You can use wildcard patterns with `eventType()` to match multiple event names: ```ts eventType("user/*"); inngest.createFunction( { id: "audit-user-events", triggers: [anyUserEvent], }, async ({ event }) => { // Triggers on "user/created", "user/updated", etc. } ); ``` Wildcard event types cannot define a schema, since the matched events may have different payload shapes. ### Schema transformations Schema transforms (e.g. Zod's `.transform()`) are not supported with `eventType()`. Only the "input" shape of a schema can be used for validation. **Why transforms aren't supported:** When you send an event using a schema with a transform, the data is transformed on the producer side before being sent to Inngest. For example, if your schema transforms `{ input: "hello" }` into `{ output: 5 }`, the platform receives `{ output: 5 }`. When that event triggers a function, the SDK tries to validate the incoming data using the same schema, but the schema expects the _input_ shape (`{ input: string }`) not the _output_ shape (`{ output: number }`). This means validation will fail because StandardSchema transforms can only validate input-shaped data. If you need to transform event data, do it inside your function handler instead: ```ts eventType("app/data.received", { schema: z.object({ input: z.string() }), }); inngest.createFunction( { id: "process-data", triggers: [rawEvent], }, async ({ event }) => { // Transform inside the handler, not in the schema event.data.input.length; } ); ``` --- ## `cron` Create a typed cron trigger for scheduled functions. ```ts inngest.createFunction( { id: "daily-cleanup", triggers: [cron("0 0 * * *")], }, async ({ step }) => { // Runs every day at midnight UTC } ); ``` ### Parameters A [unix-cron](https://crontab.guru/) compatible schedule string. Supports an optional timezone prefix, e.g. `"TZ=Europe/Paris 0 12 * * 5"`. The `cron()` helper accepts a schedule string only. To add optional fields like `jitter`, use the object trigger form directly: `{ cron: "0 * * * *", jitter: "30s" }`. See the [jitter section](/docs/guides/scheduled-functions#adding-jitter) for details. ### Cron trigger object You can also pass a cron trigger as an object, which supports additional options: A [unix-cron](https://crontab.guru/) compatible schedule string. A duration string (e.g. `"30s"`, `"5m"`) that adds a random delay after the scheduled boundary. Each occurrence fires at a random time within the jitter window. Must be between `"1s"` and `"5m"`. ### Examples ```ts {{ title: "Basic" }} inngest.createFunction( { id: "hourly-sync", triggers: [cron("0 * * * *")], }, async ({ step }) => { // Runs every hour } ); ``` ```ts {{ title: "With timezone" }} inngest.createFunction( { id: "daily-report", triggers: [cron("TZ=America/New_York 0 9 * * 1-5")], }, async ({ step }) => { // Runs at 9am ET on weekdays } ); ``` ```ts {{ title: "With jitter" }} inngest.createFunction( { id: "hourly-sync", triggers: [{ cron: "0 * * * *", jitter: "5m" }], }, async ({ step }) => { // Fires at a random time within 5 minutes after each hour } ); ``` ```ts {{ title: "Combined with eventType" }} eventType("app/sync.requested"); inngest.createFunction( { id: "data-sync", triggers: [ cron("0 */6 * * *"), manualSync, ], }, async ({ event, step }) => { // Runs every 6 hours OR when manually triggered } ); ``` --- ## `invoke` Define a typed schema for [`step.invoke()`](/docs/reference/typescript/v4/functions/step-invoke) input data. All functions can be invoked regardless of whether you use `invoke()`. This helper only defines the schema so callers get type checking on the data they pass. ```ts inngest.createFunction( { id: "compute-square", triggers: [ { event: "calculate/square" }, invoke({ schema: z.object({ number: z.number() }), }), ], }, async ({ event }) => { return { result: event.data.number * event.data.number }; } ); ``` ### Parameters A schema defining the expected `data` shape when callers invoke this function via `step.invoke()`. Accepts any [Standard Schema](https://standardschema.dev/) compatible library or a [`staticSchema()`](#staticschema). ### Example Define the invoke schema on the target function, then call it with typed data: ```ts {{ title: "Target function" }} processImage = inngest.createFunction( { id: "process-image", triggers: [ { event: "image/uploaded" }, invoke({ schema: z.object({ imageUrl: z.string().url(), width: z.number(), }), }), ], }, async ({ event }) => { // event.data.imageUrl and event.data.width are typed return { processed: true }; } ); ``` ```ts {{ title: "Calling function" }} await step.invoke("resize-image", { function: processImage, data: { imageUrl: "https://example.com/photo.jpg", width: 800, // ^ The invoke schema types and validates this }, }); ``` --- ## `staticSchema` A type-only schema utility for when you want TypeScript type checking without runtime validation. Use this as an alternative to a Standard Schema library like Zod. ```ts type OrderData = { orderId: string; total: number; }; eventType("shop/order.placed", { schema: staticSchema(), }); ``` Use `staticSchema()` when you don't need runtime validation. If you want payloads validated at runtime, use a Standard Schema library like [Zod](https://zod.dev/), [Valibot](https://valibot.dev/), or [ArkType](https://arktype.io/) instead. ### Type parameters The TypeScript type representing the event's `data` shape. The compiler uses this type for compile-time type checking only — it does not perform runtime validation. ### Examples ```ts {{ title: "With eventType" }} type UserPayload = { userId: string; email: string; }; eventType("app/user.created", { schema: staticSchema(), }); inngest.createFunction( { id: "on-user-created", triggers: [userCreated], }, async ({ event }) => { // event.data typed as { userId: string; email: string } } ); ``` ```ts {{ title: "With invoke" }} type ResizeInput = { imageUrl: string; width: number; }; inngest.createFunction( { id: "resize-image", triggers: [ invoke({ schema: staticSchema() }), ], }, async ({ event }) => { // event.data typed as { imageUrl: string; width: number } return { resized: true }; } ); ``` # TypeScript SDK v4 Source: https://www.inngest.com/docs/reference/typescript/v4/intro Description: Inngest TypeScript SDK v4: the latest release with improved type safety, native realtime, new step APIs, and updated middleware. metaTitle = "TypeScript SDK v4 | Inngest Reference"; Upgrading from v3? Check out the [migration guide](/docs/reference/typescript/v4/migrations/v3-to-v4). ## Introduction Inngest is an event-driven durable execution platform that lets you write reliable background jobs, scheduled tasks, and multi-step workflows in TypeScript without any additional infrastructure. Simply define functions as code; Inngest handles retries, concurrency, rate limiting, and observability. The TypeScript SDK v4 delivers a major upgrade to speed and developer experience: - **Rewritten middleware** — Hooks are more intuitive, less overloaded, and enable new use cases. - **Better schemas** — Runtime event data validation with Standard Schema support (not just Zod!). - **Faster by default** — Parallel step optimization and checkpointing are both enabled by default, leading to fewer requests and lower latency. - **Improved logging** — Structured logging (Pino-style) and cleaner separation of internal vs. app logs. - **Cleaner API** — Triggers in the options object, lazy init for edge runtimes, and more compile-time safety. ## Installing ```shell {{ title: "npm" }} npm install inngest ``` ```shell {{ title: "pnpm" }} pnpm add inngest ``` ```shell {{ title: "yarn" }} yarn add inngest ``` ## Build with v4 Start with the [client setup](/docs/reference/typescript/v4/client/create) and [function creation](/docs/reference/typescript/v4/functions/create) references, then use the v4 feature docs that match your application: - [Checkpointing](/docs/setup/checkpointing) for low-latency resumptions across durable steps. - [Realtime](/docs/features/realtime) for publishing workflow progress to users. - [Durable Endpoints](/docs/learn/durable-endpoints) for durable HTTP responses and streaming APIs. - [Middleware](/docs/reference/typescript/v4/middleware/lifecycle) for cross-cutting concerns like observability, serialization, encryption, and Sentry. - [v3 to v4 migration guide](/docs/reference/typescript/v4/migrations/v3-to-v4) for upgrading existing TypeScript projects. ## Source code Our TypeScript SDK and its related packages are open source and available on Github: [ inngest/inngest-js](https://github.com/inngest/inngest-js). ## Official libraries - [inngest](https://www.npmjs.com/package/inngest) - the Inngest SDK - [@inngest/eslint-plugin](https://www.npmjs.com/package/@inngest/eslint-plugin) - specific ESLint rules for Inngest - [@inngest/middleware-encryption](https://www.npmjs.com/package/@inngest/middleware-encryption) - middleware providing E2E encryption ## Examples ### Frameworks - [Astro](https://github.com/inngest/inngest-js/tree/main/examples/framework-astro) - [Bun.serve()](https://github.com/inngest/inngest-js/tree/main/examples/bun) - [Fastify](https://github.com/inngest/inngest-js/tree/main/examples/framework-fastify) - [Koa](https://github.com/inngest/inngest-js/tree/main/examples/framework-koa) - [NestJS](https://github.com/inngest/inngest-js/tree/main/examples/framework-nestjs) - [Next.js (app router)](https://github.com/inngest/inngest-js/tree/main/examples/framework-nextjs-app-router) - [Next.js (pages router)](https://github.com/inngest/inngest-js/tree/main/examples/framework-nextjs-pages-router) - [Nuxt](https://github.com/inngest/inngest-js/tree/main/examples/framework-nuxt) - [Remix](https://github.com/inngest/inngest-js/tree/main/examples/framework-remix) - [SvelteKit](https://github.com/inngest/inngest-js/tree/main/examples/framework-sveltekit) ### Middleware - [E2E Encryption](https://github.com/inngest/inngest-js/tree/main/examples/middleware-e2e-encryption) ## Community libraries Explore our collection of community-created libraries, offering unofficial but valuable extensions and integrations to enhance Inngest's functionality with various frameworks and systems. Want to be added to the list? [Contact us!](https://app.inngest.com/support) - [nest-inngest](https://github.com/thawankeane/nest-inngest) - strongly typed Inngest module for NestJS projects - [nuxt-inngest](https://www.npmjs.com/package/nuxt-inngest) - Inngest integration for Nuxt # Logging Source: https://www.inngest.com/docs/reference/typescript/v4/logging Description: Pass a custom logger to the client to route function logs to your preferred logging provider. metaTitle = "Logging | TypeScript SDK v4 Reference" The Inngest SDK uses **Pino-style object-first** logging, where structured data is passed before the message string: ```ts logger.info({ userId: "abc123" }, "Processing user event"); ``` A `logger` is available on the function context as `ctx.logger`. It provides `.info()`, `.warn()`, `.error()`, and `.debug()` methods. ```ts export default inngest.createFunction( { id: "process-upload", triggers: { event: "app/file.uploaded" }, }, async ({ event, step, logger }) => { logger.info({ fileId: event.data.fileId }, "Starting upload processing"); await step.run("process", () => { logger.debug({ fileId: event.data.fileId }, "Processing file"); return processFile(event.data.fileId); }); logger.info({ fileId: event.data.fileId, result }, "Upload processed"); return result; } ); ``` ## Logger interface Any object that implements these four methods can be used as a logger: ```ts interface Logger { info(...args: any[]): void; warn(...args: any[]): void; error(...args: any[]): void; debug(...args: any[]): void; } ``` ## Setting a logger Pass a logger to the `logger` option on the [Inngest client](/docs/reference/typescript/v4/client/create). This logger will be available on `ctx.logger` in all functions. ```ts {{ title: "Pino" }} pino({ level: "debug" }); inngest = new Inngest({ id: "my-app", logger: logger, }); ``` ```ts {{ title: "Winston" }} winston.createLogger({ level: "info", format: winston.format.json(), transports: [new winston.transports.Console()], }); // Winston uses string-first logging, so wrap it for compatibility inngest = new Inngest({ id: "my-app", logger: wrapStringFirstLogger(logger), }); ``` ### Object-first vs string-first loggers The SDK expects **object-first** loggers (like [Pino](https://github.com/pinojs/pino)), where structured data comes before the message: ```ts // Object-first (Pino style) - works out of the box logger.info({ userId: "abc" }, "User created"); ``` Some loggers like [Winston](https://github.com/winstonjs/winston) use **string-first** conventions, where the message comes first: ```ts // String-first (Winston style) - needs wrapping logger.info("User created", { userId: "abc" }); ``` For string-first loggers, use `wrapStringFirstLogger` to adapt them: ```ts winston.createLogger({ level: "info", format: winston.format.json(), transports: [new winston.transports.Console()], }); wrapStringFirstLogger(winstonLogger); ``` ## Default logger If no `logger` is set, the SDK uses a built-in `ConsoleLogger` that defaults to `"info"` level. You can customize the level: ```ts inngest = new Inngest({ id: "my-app", logger: new ConsoleLogger({ level: "debug" }), }); ``` `ConsoleLogger` is intended for local development. For production, use a structured logger like Pino. ## Internal logger The SDK produces two categories of logs: - **Function logs** - your logs via `ctx.logger` inside Inngest functions - **SDK internal logs** - registration, request handling, middleware errors, etc. By default, both use the same `logger`. Set `internalLogger` to route SDK internal logs separately: ```ts pino(); inngest = new Inngest({ id: "my-app", logger: logger, internalLogger: logger.child({ component: "inngest-sdk" }), }); ``` This is useful for filtering or routing SDK internals to a different destination without affecting your function logs. ```ts {{ title: "Separate destinations" }} pino({ level: "info" }); pino({ level: "warn" }); inngest = new Inngest({ id: "my-app", logger: appLogger, internalLogger: sdkLogger, }); ``` ```ts {{ title: "Suppress SDK logs" }} pino({ level: "info" }); inngest = new Inngest({ id: "my-app", logger: appLogger, // Silence all SDK internal logs internalLogger: new ConsoleLogger({ level: "silent" }), }); ``` ```ts {{ title: "Winston with internal logger" }} winston.createLogger({ level: "info", format: winston.format.json(), transports: [new winston.transports.Console()], }); winston.createLogger({ level: "error", format: winston.format.json(), transports: [new winston.transports.Console()], }); inngest = new Inngest({ id: "my-app", logger: wrapStringFirstLogger(appLogger), internalLogger: wrapStringFirstLogger(sdkLogger), }); ``` If `internalLogger` is not set, it falls back to `logger`. # Encryption middleware Source: https://www.inngest.com/docs/reference/typescript/v4/middleware/encryption Description: Encrypt and decrypt step inputs and outputs automatically so sensitive data is never stored in plaintext. metaTitle = "Encryption Middleware | TypeScript SDK v4 Reference" The encryption middleware provides end-to-end encryption for events, step output, and function output. **Only encrypted data is sent to Inngest servers**: encryption and decryption happen within your infrastructure. ## Installation Install the [`@inngest/middleware-encryption` package](https://www.npmjs.com/package/@inngest/middleware-encryption) ([GitHub](https://github.com/inngest/inngest-js/tree/main/packages/middleware-encryption#readme)) and configure it as follows: ```ts new Inngest({ id: "my-app", middleware: [ encryptionMiddleware({ // your encryption key string should not be hard coded key: process.env.MY_ENCRYPTION_KEY, }), ], }); ``` By default, the following will be encrypted: - All step data - All function output - Event data placed inside `data.encrypted` ## `encryptionMiddleware(options)` Returns a `Middleware.Class` that can be passed to the `middleware` array on a client or function. ### Options The encryption key used to encrypt and decrypt data. This should be a secret value stored in an environment variable. When `true`, disables encryption but continues to decrypt existing data. Useful when migrating away from encryption. The field within `event.data` to encrypt. Defaults to `"encrypted"`. Additional keys to try when decrypting. Useful during key rotation. A custom encryption service instance. If not provided, the default LibSodium-based service is used. Options for backward compatibility with v0 AES encryption format. ## Changing the encrypted `event.data` field Only select pieces of event data are encrypted. By default, only the `data.encrypted` field. This can be customized using the `eventEncryptionField` option: ```ts encryptionMiddleware({ key: process.env.MY_ENCRYPTION_KEY, eventEncryptionField: "sensitive", }); ``` ## Decrypt only mode To disable encryption but continue decrypting, set `decryptOnly: true`. This is useful when you want to migrate away from encryption but still need to process older events. ```ts encryptionMiddleware({ key: process.env.MY_ENCRYPTION_KEY, decryptOnly: true, }); ``` ## Key rotation To attempt decryption with multiple keys, set the `fallbackDecryptionKeys` option. This is useful when rotating keys, since older events may have been encrypted with a different key: ```ts // 1. Start with the current key encryptionMiddleware({ key: process.env.MY_ENCRYPTION_KEY, }); // 2. Deploy all services with the new key as a decryption fallback encryptionMiddleware({ key: process.env.MY_ENCRYPTION_KEY, fallbackDecryptionKeys: ["new"], }); // 3. Deploy all services using the new key for encryption encryptionMiddleware({ key: process.env.MY_ENCRYPTION_KEY_V2, fallbackDecryptionKeys: ["current"], }); // 4. Once all data using the old key has passed, phase it out encryptionMiddleware({ key: process.env.MY_ENCRYPTION_KEY_V2, }); ``` ## Cross-language support This middleware is compatible with our encryption middleware in the Python SDK. Encrypted events can be sent from Python and decrypted in TypeScript, and vice versa. # Middleware examples Source: https://www.inngest.com/docs/reference/typescript/v4/middleware/examples Description: Example middleware for Inngest TypeScript SDK v4: logging, dependency injection, Sentry integration, custom serialization, and data transformation patterns. metaTitle = "Middleware Examples | TypeScript SDK v4" Real-world examples using the v4 class-based middleware API. See [Lifecycle](/docs/reference/typescript/v4/middleware/lifecycle) for the full hook reference. ## Dependency injection Inject a [Prisma](https://www.prisma.io/) client into all functions via `transformFunctionInput`. Types are automatically inferred, so your functions see a typed `prisma` property. ```ts new PrismaClient(); class PrismaMiddleware extends Middleware.BaseMiddleware { id = "prisma"; transformFunctionInput(args: Middleware.TransformFunctionInputArgs) { return { ...args, ctx: { ...args.ctx, prisma, }, }; } } new Inngest({ id: "my-app", middleware: [PrismaMiddleware], }); ``` ```ts // prisma is typed and available in every function inngest.createFunction( { id: "create-audit-log", triggers: { event: "app/user.loggedin" } }, async ({ prisma, event }) => { await prisma.auditTrail.create({ data: { userId: event.data.userId, action: "login" }, }); } ); ``` ## Request headers to context Use `wrapRequest` to capture incoming HTTP headers, then `transformFunctionInput` to expose them in function context. ```ts class HeadersMiddleware extends Middleware.BaseMiddleware { id = "headers"; private headers: Record = {}; async wrapRequest({ next, requestInfo }: Middleware.WrapRequestArgs) { this.headers = { ...requestInfo.headers }; return await next(); } transformFunctionInput(args: Middleware.TransformFunctionInputArgs) { return { ...args, ctx: { ...args.ctx, headers: this.headers, }, }; } } ``` ## Observability Log function and step lifecycle events for monitoring and metrics. ```ts class ObservabilityMiddleware extends Middleware.BaseMiddleware { id = "o11y"; onRunStart({ functionInfo }: Middleware.OnRunStartArgs) { console.log(`[run:start] ${functionInfo.id}`); } onRunComplete({ functionInfo, output }: Middleware.OnRunCompleteArgs) { console.log(`[run:complete] ${functionInfo.id}`, output); } onRunError({ functionInfo, error, isFinalAttempt }: Middleware.OnRunErrorArgs) { console.error(`[run:error] ${functionInfo.id}`, error); if (isFinalAttempt) { // Send to error tracking service } } onStepComplete({ functionInfo, stepInfo }: Middleware.OnStepCompleteArgs) { console.log(`[step:complete] ${functionInfo.id} > ${stepInfo.hashedId}`); } onStepError({ functionInfo, stepInfo, error }: Middleware.OnStepErrorArgs) { console.error(`[step:error] ${functionInfo.id} > ${stepInfo.hashedId}`, error); } } ``` ## Error handling Use `wrapStepHandler` to catch step errors and convert them to `NonRetriableError`. ```ts class ErrorHandlingMiddleware extends Middleware.BaseMiddleware { id = "error-handling"; async wrapStepHandler({ next }: Middleware.WrapStepHandlerArgs) { try { return await next(); } catch (err) { // Convert specific errors to non-retriable if (err instanceof ValidationError) { throw new NonRetriableError(err.message, { cause: err }); } throw err; } } } ``` `wrapStepHandler` is used here instead of `wrapStep` because it runs on every errored attempt, while `wrapStep` only runs after the step has exhausted all retries. ## Custom serialization Inngest serializes and deserializes data as JSON. This means that non-JSON types like `Date`, `Map`, or `Set` are lost. However, you can preserve them using serializer middleware. Serializer middleware creates JSON-valid representations of non-JSON types, allowing for seamless preservation across step boundaries, event sends, and function invocations. See the [Custom serialization](/docs/reference/typescript/v4/middleware/serialization) guide for a full walkthrough. ## Inserting steps Use `wrapFunctionHandler` to run steps before or after the function handler. Because wrapping hooks have access to `ctx`, you can call `step.run()` directly. ```ts class InsertStepsMiddleware extends Middleware.BaseMiddleware { id = "insert-steps"; async wrapFunctionHandler({ ctx, next }: Middleware.WrapFunctionHandlerArgs) { // Run a step before the function handler await ctx.step.run("setup", async () => { // e.g. initialize resources }); await next(); // Run a step after the function handler await ctx.step.run("cleanup", async () => { // e.g. send a notification, update a status record }); return functionOutput; } } ``` The same pattern works with `wrapStep` to insert steps around individual step executions. ```ts class StepAuditMiddleware extends Middleware.BaseMiddleware { id = "step-audit"; async wrapStep({ ctx, next, stepInfo }: Middleware.WrapStepArgs) { try { return await next(); } catch (err) { // Record the failure in a durable step await ctx.step.run(`audit-failure-${stepInfo.hashedId}`, async () => { await auditLog.write({ step: stepInfo.hashedId, error: err }); }); throw err; } } } ``` Steps inserted by a middleware's `wrapStep` will not trigger that same middleware's `wrapStep` again. The SDK prevents this to avoid infinite loops. # Middleware lifecycle Source: https://www.inngest.com/docs/reference/typescript/v4/middleware/lifecycle Description: Inngest middleware lifecycle in TypeScript SDK v4: onFunctionRun, transformInput, transformOutput, and beforeResponse hook signatures. metaTitle = "Middleware Lifecycle | TypeScript SDK v4 Reference" Middleware lets you hook into function execution to add cross-cutting concerns like logging, error handling, and dependency injection. Middleware is class-based: you extend `Middleware.BaseMiddleware` and define hook methods. ## Creating middleware Extend `Middleware.BaseMiddleware` and override the hooks you need: ```ts class MyMiddleware extends Middleware.BaseMiddleware { id = "my-middleware"; // Override hook methods here } ``` Register middleware at the client level (applies to all functions) or the function level (applies to one function): ```ts // Client level new Inngest({ id: "my-app", middleware: [MyMiddleware], }); // Function level inngest.createFunction({ id: "my-fn", middleware: [MyMiddleware], triggers: { event: "app/user.created" }, }, async ({ event }) => { // ... }); ``` A fresh middleware instance is created for every request, so you can safely use instance properties (`this`) to store per-request state without worrying about leaks between runs. ## Execution lifecycle The following shows the order in which hooks are called during a request. This is the key mental model for understanding middleware: 1. **`wrapRequest()`** - Outermost wrapper around the entire HTTP request 2. **`transformFunctionInput()`** - Modify `ctx` before the function handler runs 3. **`wrapFunctionHandler()`** - Wrap function execution (e.g. for `AsyncLocalStorage`) 4. **`onMemoizationEnd()`** - After all memoized steps resolve 5. **`onRunStart()`** - First attempt only (attempt 0, no memoized steps) 6. Per step: - `transformStepInput()` → `wrapStep()` → `onStepStart()` → `wrapStepHandler()` → execute → `onStepComplete()` / `onStepError()` 7. **`onRunComplete()`** / **`onRunError()`** - When the function finishes Event sending (`transformSendEvent()` → `wrapSendEvent()`) is not part of this fixed sequence. It runs whenever `inngest.send()` or `step.sendEvent()` is called. Hooks you don't define have zero overhead: the SDK skips them entirely. Only override the hooks you need. ## Observable hooks Observable hooks (`on*`) are call-and-forget. They receive read-only arguments and do not return a value. Use them for logging, metrics, and side effects. Errors thrown in observable hooks are caught and logged, not propagated to the run or step. --- ### `onRunStart` Called once on the very first attempt of a run (attempt 0, no memoized steps). Not called on subsequent retries or replays. Will not call if the first attempt fails to reach the app (e.g. a network error). The function context. Metadata about the function being executed. --- ### `onRunComplete` Called when a function completes successfully. The function context. Metadata about the function. The successful return value of the function. --- ### `onRunError` Called each time a function throws an error. The function context. The error that was thrown. Metadata about the function. Whether this is the last retry attempt before the run permanently fails. --- ### `onStepStart` Called before a step handler runs. Only called for `step.run` and `step.sendEvent`. The function context. Metadata about the function. Metadata about the step being executed. --- ### `onStepComplete` Called when a step succeeds. Only called for `step.run` and `step.sendEvent`. Never called for memoized steps. The function context. Metadata about the function. The successful return value of the step. Metadata about the step. --- ### `onStepError` Called when a step throws an error. Only called for `step.run` and `step.sendEvent`. Never called for memoized steps. The function context. The error that was thrown. Metadata about the function. Whether this is the last retry attempt for this step. Metadata about the step. --- ### `onMemoizationEnd` Called once per request after all memoized steps have resolved. On the first request (no memoized steps), called immediately. Use cases for this hook are limited. It's primarily useful for logging or metrics that should run after all memoized steps have resolved. The function context. Metadata about the function. ## Wrapping hooks Wrapping hooks (`wrap*`) follow an onion model: you **must** call `next()` to continue processing. Code before `next()` runs on the way in, code after runs on the way out. ```ts class MyMiddleware extends Middleware.BaseMiddleware { id = "my-middleware"; async wrapFunctionHandler({ next }: Middleware.WrapFunctionHandlerArgs) { console.log("before"); await next(); console.log("after"); return result; } } ``` With multiple middleware, they nest: middleware 1 wraps middleware 2 wraps the inner handler. --- ### `wrapRequest` Wraps the entire HTTP request Example use cases: auth, top-level metrics, or error boundaries. Metadata about the function. Must call to continue processing. Returns the HTTP response. The incoming HTTP request metadata (headers, URL, method). The ID of the current run. --- ### `wrapFunctionHandler` Wraps function execution. `next()` resolves when the function completes. Example use cases: `AsyncLocalStorage`, error transformation, timing, or [inserting steps](/docs/reference/typescript/v4/middleware/examples#inserting-steps). `next()` only resolves when the function fully completes or errors. When a new step is discovered, `next()` never resolves for that request. It intentionally hangs until garbage collection deletes it. The function context. Metadata about the function. Must call to execute the function handler. --- ### `wrapStep` Wraps every step, including memoized steps and all step kinds (`step.run`, `step.sleep`, `step.invoke`, etc.). Example use cases: deserialize memoized data, [insert steps](/docs/reference/typescript/v4/middleware/examples#inserting-steps). If the step is not memoized, `next()` never resolves for that request. The method intentionally hangs until garbage collection deletes it. The function context. Metadata about the function. Must call to continue processing the step. Metadata about the step. Check `stepInfo.memoized` to differentiate memoized vs fresh. --- ### `wrapStepHandler` Wraps step handler execution. Only called for `step.run` and `step.sendEvent`. Example use cases: serialize step output, [error handling](/docs/reference/typescript/v4/middleware/examples#error-handling), or timing. The function context. Metadata about the function. Must call to execute the step handler. Metadata about the step. --- ### `wrapSendEvent` Wraps event sending via `inngest.send()` or `step.sendEvent()`. Example use cases: backup on send failure, metrics. The events being sent. Metadata about the function, or `null` if called outside a function (e.g. `inngest.send()`). Must call to send the events. ## Transform hooks Transform hooks (`transform*`) receive arguments and return a modified copy. Use them to inject dependencies, modify inputs, or enrich events. --- ### `transformFunctionInput` Modify the function context before the handler runs. Example use cases: dependency injection, deserialize event data. The function context. Add properties here to inject them into the function handler. Metadata about the function. A record of memoized step data keyed by hashed step ID. --- ### `transformStepInput` Modify step options or input before a step runs. Example use cases: serialize `step.invoke` data, bust memoization cache (i.e. change step ID). Metadata about the function. Partial step metadata. The options passed to the step (first argument). Arguments passed to the step function (after ID and handler). --- ### `transformSendEvent` Modify events before they are sent. Example use cases: serialize event data, add metadata. The events being sent. Return a modified copy to change them. Metadata about the function, or `null` if called outside a function. ## Output type transforms Output type transforms are `declare` properties that control how TypeScript types function and step return values. They have no runtime behavior — they only affect the type system. By default, the SDK assumes all return values are serialized to JSON, so types like `Date` become `string`. If your middleware changes serialization behavior at runtime (e.g. via [`wrapStepHandler`](#wrapstephandler)), declare a corresponding type transform so TypeScript reflects the actual runtime types. Type transforms only affect types. You are responsible for ensuring the declared transform matches your runtime transformation. Both transforms use the `Middleware.StaticTransform` pattern to imitate higher-kinded types. For example, a normal generic type that preserves `Date` instead of Jsonifying it: ```ts // A normal generic type type PreserveDate = In extends Date ? Date : Jsonify; ``` As a `StaticTransform`, this becomes an interface where `this["In"]` replaces the generic parameter and `Out` is the result: ```ts // The same logic as a StaticTransform interface PreserveDate extends Middleware.StaticTransform { Out: this["In"] extends Date ? Date : Jsonify; } ``` The original return type. Set automatically by the SDK — do not set this yourself. The transformed type. Define this to compute the output type based on `In`. When multiple middleware declare transforms, they are chained in registration order: the `Out` of one becomes the `In` of the next. --- ### `functionOutputTransform` Declares how function return types are transformed. By default, return types are Jsonified (e.g. `Date` becomes `string`). ```ts interface PreserveDate extends Middleware.StaticTransform { Out: this["In"] extends Date ? Date : Jsonify; } class MyMiddleware extends Middleware.BaseMiddleware { id = "my-middleware"; declare functionOutputTransform: PreserveDate; } ``` --- ### `stepOutputTransform` Declares how step output types are transformed. By default, output types are Jsonified (e.g. `Date` becomes `string`). ```ts interface PreserveDate extends Middleware.StaticTransform { Out: this["In"] extends Date ? Date : Jsonify; } class MyMiddleware extends Middleware.BaseMiddleware { id = "my-middleware"; declare stepOutputTransform: PreserveDate; } ``` --- ## Static hooks ### `onRegister` Called once when the middleware class is registered with a client or function. Example use cases: one-time setup. ```ts class MyMiddleware extends Middleware.BaseMiddleware { id = "my-middleware"; static onRegister({ client, functionInfo }: Middleware.OnRegisterArgs) { // One-time setup } } ``` The Inngest client instance. Metadata about the function, or `null` for client-level middleware. ## Important notes - **Performance** - Undefined hooks have zero overhead. The SDK checks for hook presence and skips entirely if not defined. - **Immutability** - Observable and wrapping hook arguments are deeply read-only. Do not mutate them. - **`next()` is required** - Wrapping hooks must call `next()` or the request will hang. - **Instance per request** - A new middleware instance is created for each request, so instance state is safe to use within a single request. # Sentry middleware Source: https://www.inngest.com/docs/reference/typescript/v4/middleware/sentry Description: Automatically capture function exceptions in Sentry with trace context and step metadata. metaTitle = "Sentry Middleware | TypeScript SDK v4 Reference" The Sentry middleware captures exceptions, adds tracing, and includes useful context (function ID, event names) for each function run. See the [Sentry middleware guide](/docs/features/middleware/sentry-middleware) for setup and usage instructions. # Custom serialization Source: https://www.inngest.com/docs/reference/typescript/v4/middleware/serialization Description: Control how step inputs and outputs are serialized and deserialized for custom data types. metaTitle = "Custom Serialization Middleware | TypeScript SDK v4" Inngest sends step output, function output, and event data as JSON. This means non-JSON types like `Date`, `Map`, `Set`, or custom classes are lost during serialization. Custom serializer middleware lets you preserve these types by converting them to a JSON-safe format on the way out and restoring them on the way in. The default type-level transform for that JSON boundary is `Jsonify`. For why composing it twice used to silently collapse step return types—and how we fixed it—see [Adding a second middleware broke our typescript types](/blog/adding-a-second-middleware-broke-our-typescript-types?ref=docs-middleware-serialization). ## How it works A serializer middleware hooks into multiple points in the lifecycle: - **`wrapStepHandler`** and **`wrapFunctionHandler`** - Serialize output before it's sent to Inngest - **`wrapStep`** - Deserialize memoized step data when it's returned to your function - **`transformFunctionInput`** - Deserialize event data before your function handler runs - **`transformSendEvent`** - Serialize event data before it's sent to Inngest - **`transformStepInput`** - Serialize invoke step input before it's sent to the server This ensures that your custom types are preserved across step boundaries, event sends, and function invocations. ## Building a serializer In this example, we'll build a serializer that preserves `Date` objects. The recommended approach is to build an abstract base class that handles the recursive traversal and lifecycle hooks, then create concrete implementations for each type you want to serialize. `BaseSerializerMiddleware` will eventually be a first-class part of the SDK, but for now it's a pattern you can copy into your own codebase. ### Base class This base class recursively walks all data flowing through the middleware, calling your `serialize` and `deserialize` methods on matching values. You can copy-paste it into your codebase, since it works with any custom serializer.
**`BaseSerializerMiddleware` full source** ```ts abstract class BaseSerializerMiddleware< TSerialized, > extends Middleware.BaseMiddleware { // Implement these four methods in your subclass protected abstract serialize(value: unknown): TSerialized; protected abstract deserialize(value: TSerialized): unknown; protected abstract needsSerialize(value: unknown): boolean; protected abstract isSerialized(value: unknown): value is TSerialized; // Set to false to only serialize top-level values protected readonly recursive: boolean = true; // Serialize a value (optionally recursive) private _serialize(value: unknown): unknown { if (this.needsSerialize(value)) { return this.serialize(value); } if (!this.recursive) { return value; } if (isRecord(value)) { return Object.fromEntries( Object.entries(value).map(([key, v]) => [key, this._serialize(v)]), ); } if (Array.isArray(value)) { return value.map((v) => this._serialize(v)); } return value; } // Deserialize a value (optionally recursive) private _deserialize(value: unknown): unknown { if (this.isSerialized(value)) { return this.deserialize(value); } if (!this.recursive) { return value; } if (isRecord(value)) { return Object.fromEntries( Object.entries(value).map(([key, v]) => [key, this._deserialize(v)]), ); } if (Array.isArray(value)) { return value.map((v) => this._deserialize(v)); } return value; } // Deserialize event data before Inngest function handler runs transformFunctionInput( arg: Middleware.TransformFunctionInputArgs, ): Middleware.TransformFunctionInputArgs { return { ...arg, ctx: { ...arg.ctx, event: { ...arg.ctx.event, data: this._deserialize(arg.ctx.event.data), }, events: arg.ctx.events.map((event) => ({ ...event, data: this._deserialize(event.data), })), }, }; } // Serialize function output before sending it to Inngest async wrapFunctionHandler({ next, }: Middleware.WrapFunctionHandlerArgs) { await next(); return this._serialize(output); } // Serialize `step.invoke` input before sending it to Inngest transformStepInput( arg: Middleware.TransformStepInputArgs, ): Middleware.TransformStepInputArgs { if (arg.stepInfo.stepType === "invoke") { arg.input = arg.input.map((i) => this._serialize(i)); } return arg; } // Serialize step output before sending it to Inngest async wrapStepHandler({ next }: Middleware.WrapStepHandlerArgs) { await next(); return this._serialize(output); } // Deserialize step input before returning it into the Inngest function // handler async wrapStep({ next }: Middleware.WrapStepArgs) { return this._deserialize(await next()); } // Serialize event data before sending it to Inngest transformSendEvent(arg: Middleware.TransformSendEventArgs) { return { ...arg, events: arg.events.map((event) => { let data = undefined; if (event.data) { data = this._serialize(event.data) as Record; } return { ...event, data }; }), }; } } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } ```
### Date serializer Here's a concrete implementation that preserves `Date` objects. It converts dates to a tagged JSON format on serialization and restores them on deserialization: ```ts // How dates are represented in JSON "__date__"; type Serialized = { [MARKER]: true; value: string }; class DateSerializerMiddleware extends BaseSerializerMiddleware { readonly id = "date-serializer"; // Tell TypeScript that Date objects are preserved in step/function output declare stepOutputTransform: PreserveDate; declare functionOutputTransform: PreserveDate; protected needsSerialize(value: unknown): boolean { return value instanceof Date; } protected serialize(value: unknown): Serialized { return { [MARKER]: true, value: (value as Date).toISOString() }; } protected isSerialized(value: unknown): value is Serialized { return isRecord(value) && MARKER in value; } protected deserialize(value: Serialized): Date { return new Date(value.value); } } // Recursively preserves Date, JSON-serializes everything else type _PreserveDate = T extends Date // Keep Date as Date ? Date : T extends Array // Recurse into arrays ? Array<_PreserveDate> : T extends Record // Recurse into object values ? { [K in keyof T]: _PreserveDate } // Jsonify all other types : Jsonify; // Higher-kinded type for the middleware interface PreserveDate extends Middleware.StaticTransform { Out: _PreserveDate; } ``` In the above example, `_PreserveDate` looks like a normal type but `PreserveData` may look strange. The `PreserveDate` interface is a higher-kinded type that middleware uses to transform function and step return types. ## Using the middleware Register the middleware at the client level to apply it to all functions, or at the function level for specific functions: ```ts {{ title: "Client level" }} new Inngest({ id: "my-app", middleware: [DateSerializerMiddleware], }); // All functions created with this client will serialize/deserialize Dates inngest.createFunction( { id: "my-fn", triggers: { event: "app/task.created" } }, async ({ step }) => { await step.run("get-date", () => { return { createdAt: new Date(), count: 42 }; }); // result.createdAt is a Date, not a string console.log(result.createdAt.getFullYear()); } ); ``` ```ts {{ title: "Function level" }} new Inngest({ id: "my-app" }); // Only this function uses the serializer inngest.createFunction( { id: "my-fn", middleware: [DateSerializerMiddleware], triggers: { event: "app/task.created" }, }, async ({ step }) => { await step.run("get-date", () => { return { createdAt: new Date(), count: 42 }; }); console.log(result.createdAt.getFullYear()); } ); ``` ## Serializing other types You can follow the same pattern for any non-JSON type. Create a new subclass of `BaseSerializerMiddleware` for each type. For example, here's a serializer that preserves `Set` objects: ```ts "__set__"; type SerializedSet = { [SET_MARKER]: true; values: unknown[] }; class SetSerializerMiddleware extends BaseSerializerMiddleware { readonly id = "set-serializer"; declare stepOutputTransform: PreserveSet; declare functionOutputTransform: PreserveSet; protected needsSerialize(value: unknown): boolean { return value instanceof Set; } protected serialize(value: unknown): SerializedSet { return { [SET_MARKER]: true, values: [...(value as Set)] }; } protected isSerialized(value: unknown): value is SerializedSet { return isRecord(value) && SET_MARKER in value; } protected deserialize(value: SerializedSet): Set { return new Set(value.values); } } type _PreserveSet = T extends Set ? T : T extends Array ? Array<_PreserveSet> : T extends Record ? { [K in keyof T]: _PreserveSet } : Jsonify; interface PreserveSet extends Middleware.StaticTransform { Out: _PreserveSet; } ``` Use multiple serializer middleware together by registering them both: ```ts new Inngest({ id: "my-app", middleware: [DateSerializerMiddleware, SetSerializerMiddleware], }); ``` Each serializer uses a unique marker key to tag its serialized format, so multiple serializers won't conflict with each other. # TypeScript SDK Migration Guide: v3 to v4 Source: https://www.inngest.com/docs/reference/typescript/v4/migrations/v3-to-v4 Description: Complete migration guide for upgrading the Inngest TypeScript SDK from v3 to v4. Covers all breaking changes, new APIs, and step-by-step upgrade instructions. metaTitle = "Migrate TypeScript SDK v3 → v4" This guide helps you migrate your Inngest TypeScript SDK from v3 to v4. ## New features - Middleware is more powerful and intuitive ([more info](/docs/reference/typescript/v4/middleware/lifecycle)) - Trigger helpers ([more info](/docs/reference/typescript/v4/functions/triggers)) - Separate internal and user-facing logs with the `internalLogger` option ([more info](/docs/reference/typescript/v4/logging#internal-logger)) - Use a worker thread in Connect ([more info](/docs/reference/typescript/v4/migrations/v3-to-v4#connect-worker-thread)) ## Upgrade prompt You can use your coding agent of choice to upgrade to v4. Here's a suggested prompt: ```plaintext Upgrade Inngest TypeScript SDK from v3 to v4 Read the migration guide at https://www.inngest.com/docs-markdown/reference/typescript/v4/migrations/v3-to-v4 and apply every breaking change to this codebase. Pay special attention to: - Triggers moving into the options object (1st arg of createFunction) - EventSchemas being replaced with eventType() and staticSchema() - `event.user` being removed; move required fields into `event.data` - Serve options (signingKey, baseUrl, etc.) moving to the client constructor - step.invoke() no longer accepting string function IDs - Set `maxRuntime` option for `checkpointing` if running on serverless. Install the latest v4 package and verify TypeScript compilation passes afterward. ``` ## Breaking changes ### Middleware rewrite The middleware system was completely rewritten. To see the new API, see the [middleware](/docs/reference/typescript/v4/middleware/lifecycle) documentation. This guide covers how to migrate between the v3 and the v4 version of the `inngest` package. ### Default mode is now "cloud" The default mode is now `cloud` instead of `dev`. This prevents accidental production deployments in development mode and aligns with all other Inngest SDKs. **What this means:** - In `cloud` mode, a signing key is required (via `INNGEST_SIGNING_KEY` or the `signingKey` option) - For local development, explicitly set `isDev: true` on your client or set `INNGEST_DEV=1` ```typescript // Local development new Inngest({ id: "my-app", isDev: true }); // Production (signing key required via env or option) new Inngest({ id: "my-app" }); ``` or ```sh INNGEST_DEV=1 pnpm run dev ``` You have encountered this issue if your error looks something like: ``` Error: Inngest error: A signing key is required to run in Cloud mode, but no signing key was found. To fix this, choose one of the following: - For local development, set INNGEST_DEV=1 to use the Dev Server (e.g. INNGEST_DEV=1 npm run dev) - For production, set the INNGEST_SIGNING_KEY environment variable Find your keys at https://app.inngest.com ``` ### Event trigger is now in options In prior versions of the SDK, you specified triggers for events as the second argument of `createFunction`. We changed it because the triggers make more implicit sense as options, _and_ the case of specifying a function with no triggers required you to send an empty array which we did not love. For example, the old syntax: ```ts // Old (v3) inngest.createFunction( { id: "fn-id" }, { event: "fn/trigger-event" }, async ({ event }) => { // ... } ) // New (v4) inngest.createFunction( { id: "fn-id", triggers: { event: "fn/trigger-event" } }, async ({ event }) => { // ... } ) ``` Multiple triggers can be passed as an array, a la: ```ts triggers: [ { event: "fn/trigger-event" }, { cron: "1 */2 * * *" } ] ``` A triggerless function is as simple as not providing a trigger. ### Event schemas replaced with event types The centralized schemas option on the Inngest client (`EventSchemas` class) has been removed. Instead, use the `eventType()` function to define event types that are shared between sending events, waiting for events, and event triggers. See the [trigger helpers reference](/docs/reference/typescript/v4/functions/triggers) for full documentation on `eventType()`, `cron()`, `invoke()`, and `staticSchema()`. ```ts // Old (v3) - centralized type-only schemas on the client new Inngest({ id: "my-app", schemas: new EventSchemas().fromRecord<{ "user/created": { data: { userId: string; email: string } }; }>(), }); inngest.createFunction( { id: "on-user-created" }, { event: "user/created" }, async ({ event }) => { // event.data typed from centralized schema } ); // New (v4) - decentralized event types with optional runtime validation new Inngest({ id: "my-app" }); eventType("user/created", { schema: z.object({ userId: z.string(), email: z.string() }), }); inngest.createFunction( { id: "on-user-created", triggers: [userCreated] }, async ({ event }) => { // event.data typed as { userId: string; email: string } } ); ``` Using a runtime schema library (e.g. Zod) will result in a runtime type check. If you don't want runtime type checking, use `staticSchema()` instead: ```ts type UserCreatedPayload = { userId: string; email: string }; eventType("user/created", { schema: staticSchema(), }); ``` Event types can also be used with `step.waitForEvent` and `inngest.send`: ```ts // Return value is inferred and validated using `userCreated`'s schema await step.waitForEvent("wait", { event: userCreated, timeout: "7d", }); // Sent data is validated using `userCreated`'s schema await inngest.send(userCreated.create({ userId: "1", email: "a@b.com" })); ``` Please note: `staticSchema` expects a `type`, not an `interface`. You may need to convert existing interfaces to types. ### Remove `event.user` The deprecated `event.user` field has been removed. If you previously sent user information on the top-level `user` field or read it from `event.user` inside a function, move the data your function needs into `event.data` instead. ```ts // Old (v3) await inngest.send({ name: "user/created", user: { id: "user_123" }, }); inngest.createFunction( { id: "sync-user-profile" }, { event: "user/created" }, async ({ event }) => { await syncUserProfile(event.user.id); } ); // New (v4) await inngest.send({ name: "user/created", data: { userId: "user_123" }, }); inngest.createFunction( { id: "sync-user-profile", triggers: { event: "user/created" } }, async ({ event }) => { await syncUserProfile(event.data.userId); } ); ``` `event.user` was left over from a deprecated feature and is not saved in the event store. This made it incompatible with features that reload stored event payloads, such as function run replay, where `event.user` was always an empty object. If you used `event.user` to keep sensitive values encrypted, use [encryption middleware](/docs/reference/typescript/v4/middleware/encryption) instead. ### Rename `serveHost` to `serveOrigin` Rename the `serveHost` option to `serveOrigin` to better reflect its purpose. Using "host" was actually a misnomer because the scheme and port can be specified, while a "host" is only the domain or IP. The `INNGEST_SERVE_HOST` environment variable is still supported for backward compatibility but will log a deprecation warning. Please migrate to `INNGEST_SERVE_ORIGIN`. ### Serve options moved to client Many of the options previously passed to the `serve` function were moved up to the `client` level. These properties make more sense at this level and, because it only involves potentially reorganizing where you're setting values, should be a very straightforward migration. The options that you may need to reorganize are: - **baseUrl** - **fetch** - **signingKey** - **signingKeyFallback** If you are passing any of these values to the `serve` function, or the `createServer` function, you will need to modify your code so that they are instead provided to the client. ```typescript // Old (v3) new Inngest({ id: "my-app" }); app.use( "/api/inngest", serve({ client: inngest, functions, signingKey: "my-signing-key", signingKeyFallback: "my-fallback-key", baseUrl: "https://my-inngest-instance.example.com", }) ); // New (v4) new Inngest({ id: "my-app", signingKey: "my-signing-key", signingKeyFallback: "my-fallback-key", baseUrl: "https://my-inngest-instance.example.com", }); app.use("/api/inngest", serve({ client: inngest, functions })); ``` If you were relying on environment variables (e.g., `INNGEST_SIGNING_KEY`) rather than passing these options explicitly, no changes are required—the client will automatically read from the environment. ### Remove `logLevel` option Log level is now purely the responsibility of the logger object passed to the client's `logger` option. The default logger level is `info`. If you'd like to change that, you can manually pass our default logger: ```ts inngest = new Inngest({ id: "my-app", logger: new ConsoleLogger({ level: "debug" }) }); ``` ### Simplify `streaming` option The `streaming` option in `serve()` has been simplified from `"allow" | "force" | false` to `true | false`. - `"force"` → `true` (enable streaming; throws error if handler doesn't support it) - `"allow"` → removed (use `true` instead) - `false` → `false` (unchanged) ```typescript // Old (v3) serve({ client, functions, streaming: "force" }); // New (v4) serve({ client, functions, streaming: true }); ``` If the `serve` function does not support streaming then it throws an error. Previously, it silently ignored the option. ### Optimized parallelism enabled by default "Optimized parallelism" significantly reduces the number of requests necessary to run parallel steps (steps in `Promise.all`, `Promise.allSettled`, etc.). This decrease in requests can dramatically improve CPU usage, memory usage, and function run duration. The primary downside is the change in `Promise.race` behavior. `Promise.race` will wait for *all* promises to settle before resolving. The correct "winner" is returned, but the `Promise.race` will *not* immediately resolve on the first winner. If you were relying on the early resolution behavior, use the new `group.parallel()` helper. This will disable optimized parallelism for groups of steps. Note that early resolution does not cancel the remaining steps; see the [step parallelism guide](/docs/guides/step-parallelism) for details: ```typescript // Old behavior (no longer works as expected with optimized parallelism) await Promise.race([ step.run("a", () => "a"), step.run("b", () => "b"), ]); // New approach using group.parallel() from the function context await group.parallel(async () => { return Promise.race([ step.run("a", () => "a"), step.run("b", () => "b"), ]); }); ``` If you'd like to disable optimized parallelism altogether at either the client or function level, set `optimizeParallelism: false`. ### Checkpointing enabled by default [Checkpointing](/docs/setup/checkpointing) is now enabled by default for all functions. This means multiple steps can execute within a single request, resulting in dramatically lower latency and bandwidth usage. If your functions run on serverless platforms, like Vercel, you should configure the `maxRuntime` option to slightly below your function's maximum duration: ```ts new Inngest({ id: 'my-app', checkpointing: { maxRuntime: '50s', // 50s might be a good option if your max duration is 60s } }) ``` Many platforms, like Vercel, allow you to configure the maximum duration per function, e.g. on your `/api/inngest` endpoint. We recommend setting the `maxRuntime` to 60-80% of your maximum duration. For Vercel applications, you should explicitly set your `maxDuration` ([docs](https://vercel.com/docs/functions/configuring-functions/duration)) on the `/api/inngest`. For example: ```ts app/api/inngest/route.ts // This endpoint can run for a maximum of 300 seconds maxDuration = 300; export default serve({ client: inngest, functions }); ``` Learn more about configuring Vercel [here](). To disable checkpointing, set `checkpointing: false` on your client or on individual functions: ```typescript // Disable for all functions new Inngest({ id: "my-app", checkpointing: false }); // Disable per-function inngest.createFunction( { id: "my-fn", checkpointing: false }, async ({ step }) => { // ... } ); ``` ### Connect: `rewriteGatewayEndpoint` replaced with `gatewayUrl` The `rewriteGatewayEndpoint` callback option has been removed from `connect()`. Use the `gatewayUrl` string option or the `INNGEST_CONNECT_GATEWAY_URL` environment variable instead. ```typescript // Old (v3) await connect({ apps: [...], rewriteGatewayEndpoint: (url) => { new URL(url); clusterUrl.host = 'my-cluster-host:8289'; return clusterUrl.toString(); }, }); // New (v4) await connect({ apps: [...], gatewayUrl: "wss://my-cluster-host:8289/v0/connect", }); ``` ### Connect worker thread Connect internals (e.g. the WebSocket connection) are now in a worker thread. This solves an issue where event loop starvation (e.g. CPU heavy work) blocked heartbeats, tricking the Inngest server into thinking the worker died. This change _shouldn't_ break any user facing behavior, but it's good to be aware of. If you do notice an issue with Connect, you can move the WebSocket connection back to the main thread by setting `isolateExecution: false` in your `connect()` options or by setting the `INNGEST_CONNECT_ISOLATE_EXECUTION` environment variable to `false`. ### Edge environment improvements Fetch and configuration are now resolved lazily at first use rather than eagerly at client construction. This means you no longer need to manually bind `globalThis.fetch` before creating an Inngest client in edge environments (Cloudflare Workers, Vercel Edge, Deno, etc.). ### Remove support for string function IDs in `step.invoke()` Passing a raw string to `step.invoke()` is no longer supported. Use `referenceFunction()` or pass an imported function instance instead. ```typescript // Old (v3) - No longer works await step.invoke("my-step", { function: "my-app-other-fn", data: { foo: "bar" }, }); // New (v4) - Use referenceFunction for cross-app invocation await step.invoke("my-step", { function: referenceFunction({ appId: "my-app", functionId: "other-fn" }), data: { foo: "bar" }, }); // Or pass an imported function instance directly await step.invoke("my-step", { function: otherFn, data: { foo: "bar" }, }); ``` The `referenceFunction()` helper provides type safety and avoids the footgun of manually constructing the `appId-functionId` string. # Channels & topics Source: https://www.inngest.com/docs/reference/typescript/v4/realtime/channels Description: realtime.channel() in TypeScript SDK v4 defines typed channels and topics for streaming structured updates from Inngest functions to subscribers. metaTitle = "Channels & Topics | Realtime SDK v4 Reference" Channels are the top-level scope for realtime messages. Each channel has one or more **topics**: typed message streams with schemas that provide end-to-end type safety from publishing to subscribing. ```ts realtime.channel({ name: "system:alerts", topics: { alert: { schema: z.object({ message: z.string(), severity: z.enum(["info", "warn", "error"]) }) }, }, }); ``` --- ## `realtime.channel(options)` Creates a channel definition. The returned value is either a channel instance (static name) or a factory function (parameterized name). The channel name. Use a static string for a fixed channel, or a function that returns a string for parameterized channels. The function receives a single object argument with your parameters. An object mapping topic names to their configuration. Each topic must have a `schema` property: any [Standard Schema](https://github.com/standard-schema/standard-schema) (Zod, Valibot, ArkType) or `staticSchema()` for type-only schemas. ```ts // Static channel realtime.channel({ name: "system:alerts", topics: { alert: { schema: z.object({ message: z.string() }) }, }, }); // Parameterized channel realtime.channel({ name: ({ threadId }: { threadId: string }) => `chat:${threadId}`, topics: { message: { schema: z.object({ text: z.string() }) }, typing: { schema: staticSchema<{ userId: string }>() }, }, }); ``` ## Static channels When `name` is a string, `realtime.channel()` returns a channel instance directly. You can use it without calling it as a function. ```ts realtime.channel({ name: "system:alerts", topics: { alert: { schema: z.object({ message: z.string() }) }, }, }); // Use directly, no instantiation needed alerts.name; // "system:alerts" alerts.alert; // TopicRef for the "alert" topic alerts.alert.channel // "system:alerts" alerts.alert.topic // "alert" ``` ## Parameterized channels When `name` is a function, `realtime.channel()` returns a factory. Call it with your parameters to get a channel instance. ```ts realtime.channel({ name: ({ threadId }: { threadId: string }) => `chat:${threadId}`, topics: { message: { schema: z.object({ text: z.string(), sender: z.string() }) }, status: { schema: z.object({ typing: z.boolean() }) }, }, }); // Instantiate with parameters chat({ threadId: "abc123" }); ch.name; // "chat:abc123" ch.message; // TopicRef for "message" on "chat:abc123" ch.message.channel; // "chat:abc123" ch.message.topic; // "message" ``` Parameterized channels isolate subscribers to a specific instance. A subscriber to `chat:abc123` won't receive messages published to `chat:def456`. Channel parameters are yours to name and yours to generate. Use a domain-specific term within your your product, such as `threadId`, `contentId`, `documentId`, or `userId`. ## Topic schemas Every topic requires a `schema` property. The schema serves two purposes: 1. **Type inference**: TypeScript infers the data type for publishing and subscribing 2. **Runtime validation**: data is validated against the schema when publishing and (optionally) when subscribing ### Standard Schema libraries Any library implementing the [Standard Schema](https://github.com/standard-schema/standard-schema) spec works: Zod, Valibot, ArkType, and others. ```ts {{ title: "Zod" }} realtime.channel({ name: "pipeline", topics: { status: { schema: z.object({ message: z.string() }) }, }, }); ``` ```ts {{ title: "Valibot" }} realtime.channel({ name: "pipeline", topics: { status: { schema: v.object({ message: v.string() }) }, }, }); ``` ```ts {{ title: "ArkType" }} realtime.channel({ name: "pipeline", topics: { status: { schema: type({ message: "string" }) }, }, }); ``` ### `staticSchema()` For type-only schemas with zero runtime validation cost. Useful when you trust the data source and only want compile-time type checking. ```ts realtime.channel({ name: "metrics", topics: { usage: { schema: staticSchema<{ tokens: number; latencyMs: number }>() }, }, }); ``` You can mix `staticSchema` and runtime schemas on the same channel: ```ts realtime.channel({ name: "pipeline", topics: { // Runtime validation with Zod status: { schema: z.object({ message: z.string() }) }, // Type-only, no validation cost usage: { schema: staticSchema<{ tokens: number }>() }, }, }); ``` ## Topic accessors Each topic defined on a channel becomes a property on the channel instance. These accessors return a `TopicRef`, a lightweight reference carrying the channel name, topic name, and schema config. ```ts chat({ threadId: "abc123" }); // Each accessor is a TopicRef ch.message.channel; // "chat:abc123" ch.message.topic; // "message" ch.message.config; // { schema: ... } // Pass topic refs to publish and subscribe await inngest.realtime.publish(ch.message, { text: "Hello!", sender: "alice" }); ``` Topic refs are the primary way to reference a specific topic on a specific channel instance. They're used by [`inngest.realtime.publish`](/docs/reference/typescript/v4/realtime/publishing#inngest-realtime-publish-topic-ref-data), [`step.realtime.publish`](/docs/reference/typescript/v4/realtime/publishing#step-realtime-publish-id-topic-ref-data), and [`getClientSubscriptionToken`](/docs/reference/typescript/v4/realtime/subscribing). ## Type inference Channel definitions expose type utilities for extracting topic data types and parameter types. ```ts realtime.channel({ name: ({ contentId }: { contentId: string }) => `pipeline:${contentId}`, topics: { status: { schema: z.object({ message: z.string() }) }, tokens: { schema: staticSchema<{ token: string }>() }, }, }); // Infer topic data types type StatusData = typeof pipeline.$infer.status; // { message: string } type TokenData = typeof pipeline.$infer.tokens; // { token: string } // Infer channel parameters (parameterized channels only) type Params = typeof pipeline.$params; // { contentId: string } ``` ## Portability Channel definitions are plain objects with no server-side dependencies. Define them in a shared file and import them on both server and client: ```ts {{ title: "src/inngest/channels.ts" }} pipelineChannel = realtime.channel({ name: ({ contentId }: { contentId: string }) => `pipeline:${contentId}`, topics: { status: { schema: z.object({ message: z.string() }) }, tokens: { schema: staticSchema<{ token: string }>() }, }, }); ``` Import from your function code, your API routes (for token minting), and your React components. The same definition provides type safety everywhere. # Realtime Source: https://www.inngest.com/docs/reference/typescript/v4/realtime/index Description: Stream real-time updates from Inngest functions using typed channels, topics, and subscriptions built into TypeScript SDK v4. No separate package required. metaTitle = "Realtime | TypeScript SDK v4 Reference" The v4 SDK includes a built-in realtime system for streaming updates from your Inngest functions to client applications. There's no separate package to install: channels, publishing, and subscriptions are all part of `inngest`. Realtime is built into the v4 SDK. If you're using v3, see the [v3 realtime docs](/docs/reference/typescript/v3/realtime). ## Key concepts - **Channels** define a named scope for messages. They can be static or parameterized with runtime values like a `contentId` or `threadId`. - **Topics** are typed message streams within a channel, each with a schema. - **Publishing** sends data to a topic with `inngest.realtime.publish()` for non-durable messages, or `step.realtime.publish()` for durable publishes inside a function. - **Subscribing** consumes messages via `useRealtime`, `subscribe()`, or the client aliases `inngest.realtime.subscribe()` and `inngest.realtime.token()`. As a rule of thumb, prefer `step.realtime.publish()` for publishes inside functions. It is memoized and durable, unlike `inngest.realtime.publish()`, which will fire again if the function retries. ## Imports | What | Import from | |------|------------| | `realtime`, `staticSchema` | `"inngest"` | | `useRealtime`, `getClientSubscriptionToken` | `"inngest/react"` | | `subscribe`, `getSubscriptionToken` | `"inngest/realtime"` | | `step.realtime.publish` | Function handler context (no import needed) | | `inngest.realtime.publish`, `inngest.realtime.subscribe`, `inngest.realtime.token` | Your `Inngest` client instance | ## Quick start ### 1. Define a channel ```ts {{ title: "src/inngest/channels.ts" }} pipelineChannel = realtime.channel({ name: ({ contentId }: { contentId: string }) => `pipeline:${contentId}`, topics: { status: { schema: z.object({ message: z.string(), step: z.string().optional() }), }, tokens: { schema: staticSchema<{ token: string }>(), }, }, }); ``` ### 2. Publish from a function ```ts {{ title: "src/inngest/functions.ts" }} generate = inngest.createFunction( { id: "generate", triggers: [{ event: "app/generate" }] }, async ({ event, step }) => { pipelineChannel({ contentId: event.data.contentId }); // // Non-durable on purpose. Use this only when replay on retry is OK. await inngest.realtime.publish(ch.status, { message: "Starting..." }); await step.run("stream-output", async () => { for (const token of ["Hello", " ", "world"]) { // // Non-durable on purpose. Token streams may replay on retry. await inngest.realtime.publish(ch.tokens, { token }); } }); // // Prefer the durable publish for important state changes. await step.realtime.publish("final-status", ch.status, { message: "Done!", step: "complete", }); }, ); ``` ### 3. Subscribe from React ```tsx {{ title: "src/app/page.tsx" }} "use client"; export default function PipelinePage({ contentId }: { contentId: string }) { pipelineChannel({ contentId }); ["status", "tokens"] as const; useRealtime({ channel: ch, topics, token: () => fetch(`/api/realtime-token?contentId=${contentId}`).then((res) => res.json() ), }); return (

Connection: {connectionStatus} | Run: {runStatus}

{messages.byTopic.status && (

Status: {messages.byTopic.status.data.message}

)}
    {messages.all.map((msg, i) => (
  • [{msg.topic}] {JSON.stringify(msg.data)}
  • ))}
); } ``` ## Next steps - [Channels & topics](/docs/reference/typescript/v4/realtime/channels): defining channels, parameterized names, schemas, and type inference - [Publishing](/docs/reference/typescript/v4/realtime/publishing): `publish()`, `inngest.realtime.publish()`, and `step.realtime.publish()` - [`useRealtime`](/docs/reference/typescript/v4/realtime/use-realtime): full React hook API reference - [Subscribing](/docs/reference/typescript/v4/realtime/subscribing): `getClientSubscriptionToken()`, `subscribe()`, and client aliases # Publishing Source: https://www.inngest.com/docs/reference/typescript/v4/realtime/publishing Description: Publish typed messages to realtime channels from Inngest functions or server-side code with inngest.realtime.publish() and step.realtime.publish() in TypeScript SDK v4. metaTitle = "Publishing Realtime Messages | TypeScript SDK v4" There are two ways to publish realtime messages in the v4 SDK. Prefer `step.realtime.publish()` whenever you can. It is durable and memoized, so it will not run again if the function retries. Reach for `inngest.realtime.publish()` only when you specifically need non-durable behavior, such as high-frequency token streaming or publishing from code outside a function. | Method | Durable | Usable outside functions | Step ID | Best for | |--------|---------|------------------------|---------|----------| | `inngest.realtime.publish()` | No | Yes | - | High-frequency progress, and publishing from routes, webhooks, or server-side code | | `step.realtime.publish()` | Yes | No | Required | State transitions, final results, deduped publishes | --- ## `inngest.realtime.publish(topicRef, data)` Publishes from your `Inngest` client. Use this anywhere you already have the client available, such as API routes, webhooks, or other server-side code. Inside a function run, `inngest.realtime.publish()` also attaches the current run ID automatically. A topic accessor from a channel instance. The message payload. Must match the topic's schema. Returns `Promise`. ```ts export async function POST(req: Request) { await req.json(); // // Non-durable by design. Prefer step.realtime.publish() when this work // lives inside a function and duplicates on retry would be a problem. await inngest.realtime.publish(alertsChannel.alert, { message: body.message, severity: body.severity, }); return new Response("OK"); } ``` `inngest.realtime.publish()` works at the top level of your function handler and inside `step.run()` blocks. It is non-durable in both cases, so it fires again on retry. Inside a function, this is the method to reach for when publishing at high frequency, such as streaming model output token by token: ```ts inngest.createFunction( { id: "stream-tokens", triggers: [{ event: "app/generate" }] }, async ({ event, step }) => { pipelineChannel({ contentId: event.data.contentId }); await step.run("generate", async () => { await openai.responses.create({ model: "gpt-5", input: [{ role: "user", content: event.data.prompt }], stream: true, }); let full = ""; for await (const chunk of stream) { if (chunk.type === "response.output_text.delta") { full += chunk.delta; // // Non-durable on purpose. This can run again on retry, which is // usually acceptable for token streams. await inngest.realtime.publish(ch.tokens, { token: chunk.delta }); } } return full; }); }, ); ``` ## `step.realtime.publish(id, topicRef, data)` A durable step that memoizes the publish. If the function retries past this step, the publish won't re-fire. The message appears in the function's execution graph. Best for important state transitions and final results. A unique step ID. Used for memoization and appears in function logs. A topic accessor from a channel instance. The message payload. Must match the topic's schema. Returns `Promise`, the published data. ```ts inngest.createFunction( { id: "process-upload", triggers: [{ event: "app/upload" }] }, async ({ event, step }) => { uploadsChannel({ uploadId: event.data.uploadId }); // // This status update is ephemeral, so non-durable publish is fine. await inngest.realtime.publish(ch.status, { message: "Processing..." }); await step.run("process", async () => { return processUpload(event.data); }); // // Prefer the durable publish for important state that should not // duplicate if the function retries. await step.realtime.publish("publish-result", ch.result, { success: true, url: result.url, }); }, ); ``` ## Choosing a publish method Prefer **`step.realtime.publish()`** by default when the publish happens inside a function and duplicates would be incorrect or noisy. Use **`inngest.realtime.publish()`** when: - Streaming tokens, progress percentages, or log lines - The data is ephemeral and duplicates on retry are fine - You want minimum latency (no step overhead) - Publishing from outside a function (API routes, webhooks, cron jobs) Use **`step.realtime.publish()`** when: - Publishing a final result or state transition - You need exactly-once delivery semantics (memoized) - The publish should appear in the function's execution graph ## Type safety Both methods validate data against the topic's schema at both compile time and runtime: ```ts pipelineChannel({ contentId: "abc" }); // TypeScript error - missing required field await inngest.realtime.publish(ch.status, { message: "ok" }); // ✓ await inngest.realtime.publish(ch.status, { wrong: "field" }); // ✗ compile error // Runtime validation - throws if data doesn't match Zod schema await inngest.realtime.publish(ch.status, someUntypedData); // validated at runtime ``` # Subscribing Source: https://www.inngest.com/docs/reference/typescript/v4/realtime/subscribing Description: Mint subscription tokens and consume streams using getClientSubscriptionToken() and subscribe(). metaTitle = "Server-Side Realtime Subscribing | TypeScript SDK v4" Server-side subscription covers two operations: **minting tokens** for client use and **consuming streams** on the server. The v4 SDK exposes both standalone helpers and client aliases: - `getClientSubscriptionToken(app, ...)` for minting client-safe tokens - `subscribe({ app, ... })` or `inngest.realtime.subscribe(...)` for server-side streams --- ## `getClientSubscriptionToken(app, options)` Mints a scoped subscription token on the server and returns a serializable object safe to pass to the client. The returned object includes the token key and the resolved API base URL, so `useRealtime` automatically connects to the right environment (cloud or dev server) without any client-side env var configuration. Your Inngest client instance. The channel to authorize. Can be a channel instance or a plain string. The topics the token grants access to. The client can only subscribe to these topics. Returns `Promise` with `key` (the JWT string) and `apiBaseUrl` (the resolved server URL). `inngest/react` also exports this shape as the `ClientSubscriptionToken` type alias. ```ts pipelineChannel({ contentId: "abc123" }); await getClientSubscriptionToken(inngest, { channel: ch, topics: ["status", "tokens"], }); // token.key - JWT string for the client // token.apiBaseUrl - resolved API URL (e.g. cloud or localhost) ``` A client token does not include `channel` or `topics`. When passing it to `useRealtime`, pass the same channel and topics as top-level hook options so the hook can build the subscription and preserve typed message inference. Always mint tokens on the server. Never expose your Inngest signing key to the client. The token is scoped to the specified channel and topics, so a client cannot use it to access other channels. The server resolves `INNGEST_DEV` and passes the correct API URL through the token. Your client code does not need `NEXT_PUBLIC_INNGEST_DEV`, `VITE_INNGEST_DEV`, or any other browser-side env var. ### Framework examples ```ts {{ title: "Next.js Server Action" }} // app/actions.ts "use server"; export async function getRealtimeToken(contentId: string) { return getClientSubscriptionToken(inngest, { channel: pipelineChannel({ contentId }), topics: ["status", "tokens"], }); } ``` ```ts {{ title: "Express" }} express(); app.get("/api/realtime-token", async (req, res) => { req.query; await getClientSubscriptionToken(inngest, { channel: pipelineChannel({ contentId: contentId as string }), topics: ["status", "tokens"], }); res.json(token); }); ``` ```ts {{ title: "TanStack Start" }} getRealtimeToken = createServerFn({ method: "GET" }) .validator((contentId: string) => contentId) .handler(async ({ data: contentId }) => { return getClientSubscriptionToken(inngest, { channel: pipelineChannel({ contentId }), topics: ["status", "tokens"], }); }); ``` --- ## `subscribe(options)` Creates a server-side subscription to a realtime channel. Without `onMessage`, it returns a `ReadableStream`-based subscription object. With `onMessage`, it returns a callback subscription handle. ### Stream subscription Without `onMessage`, `subscribe` returns a stream subscription. Your Inngest client instance. Used to resolve connection details. The channel to subscribe to. The topics to subscribe to. A pre-minted JWT token key. If not provided, `app` is used to mint a token automatically. Enable schema validation on incoming messages. Defaults to `true`. ```ts pipelineChannel({ contentId: "abc123" }); await subscribe({ app: inngest, channel: ch, topics: ["status", "tokens"], }); // ReadableStream - use getReader() stream.getReader(); while (true) { await reader.read(); if (done) break; console.log(value.topic, value.data); } ``` The same API is available as `inngest.realtime.subscribe({ channel, topics })`, which omits the `app` option because the client is already known. ### Stream methods The returned stream has additional helper methods: Returns a new `ReadableStream` that emits parsed JSON messages. Each call creates a fresh reader view of future messages only. Returns a new `ReadableStream` with SSE-formatted `Uint8Array` chunks (`data: {...}\n\n`). Useful for piping the subscription through a streaming HTTP response. Closes the underlying WebSocket connection. Alias for `close()`. ```ts // Stream JSON to another consumer stream.getJsonStream(); new Response(jsonStream); // Stream as SSE stream.getEncodedStream(); return new Response(sseStream, { headers: { "Content-Type": "text/event-stream" }, }); // Clean up stream.close(); ``` ### Callback subscription Pass `onMessage` to use an event-driven pattern instead of streams. Called for each incoming message. Called when a connection error occurs. Returns `Promise<{ close, unsubscribe }>`. ```ts pipelineChannel({ contentId: "abc123" }); await subscribe({ app: inngest, channel: ch, topics: ["status"], onMessage: (message) => { console.log(`[${message.topic}]`, message.data); }, onError: (err) => { console.error("Subscription error:", err); }, }); // Clean up when done sub.close(); ``` ## Message shape Each message received from a subscription includes: The topic name this message was published to. The resolved channel name. The message payload, typed according to the topic's schema. The message kind. Most topic publishes are `"data"`. Run lifecycle updates arrive as `"run"`. The Inngest function run ID, if the message was published from within a function. The Inngest function ID. When the message was created. Run messages are platform lifecycle events. Their `data` is intentionally broad, while topic messages remain typed from your channel schema. ## Server-side stream example A complete example using `subscribe` to monitor a long-running function and react to its output: ```ts async function monitorWorkflow(workflowId: string) { workflowChannel({ workflowId }); await subscribe({ app: inngest, channel: ch, topics: ["status", "result"], }); stream.getReader(); while (true) { await reader.read(); if (done) break; if (value.topic === "status") { console.log("Status:", value.data.message); } if (value.topic === "result") { console.log("Result:", value.data); stream.close(); } } } ``` # useRealtime Source: https://www.inngest.com/docs/reference/typescript/v4/realtime/use-realtime Description: Subscribe to Inngest realtime channels in React or Next.js with typed message payloads. metaTitle = "useRealtime() Hook | TypeScript SDK v4 Reference" The `useRealtime` hook subscribes to realtime messages from Inngest functions in React components. It manages the WebSocket connection, reconnection, buffering, and provides typed access to messages by topic. ```tsx function Pipeline({ contentId }: { contentId: string }) { pipelineChannel({ contentId }); ["status", "tokens"] as const; useRealtime({ channel: ch, topics, token: () => fetch(`/api/realtime-token?contentId=${contentId}`).then((r) => r.json()), }); return (

Connection: {connectionStatus} | Run: {runStatus}

{messages.byTopic.status && (

{messages.byTopic.status.data.message}

)}

Messages received: {messages.all.length}

{result &&
{JSON.stringify(result, null, 2)}
}
); } ``` --- ## `useRealtime(options)` The channel to subscribe to. Can be a channel instance from `realtime.channel()` or a plain string. When you use a channel instance, topic data is typed automatically. The topics to subscribe to within the channel. Required when your `token` is a `ClientSubscriptionToken` from `getClientSubscriptionToken()` or when your token factory returns only a token key string. Authentication for the subscription. Pass a `ClientSubscriptionToken` from `getClientSubscriptionToken()`, a full token object, or an async factory that returns a token key string, `ClientSubscriptionToken`, or full token object. The async factory pattern is recommended because it runs on mount and on reconnect. Optional subscription identity key. Change it to force the hook to reset its retained state and reconnect even if `channel` and `topics` are unchanged. Whether the subscription is active. Set to `false` to pause without unmounting. Defaults to `true`. Enable subscriber-side schema validation on incoming messages. Defaults to `true`. Maximum number of messages to retain in `messages.all`. Set to `null` for unbounded history. Defaults to `100`. Milliseconds to buffer incoming messages before triggering a re-render. Useful for high-frequency streams to reduce render pressure. Defaults to `0` (immediate). Automatically reconnect on disconnect. Defaults to `true`. Minimum delay between reconnect attempts in milliseconds. Defaults to `250`. Maximum delay between reconnect attempts in milliseconds (exponential backoff cap). Defaults to `5000`. Pause the subscription when the browser tab is hidden, resuming when it becomes visible. Defaults to `true`. Automatically close the subscription when the function run reaches a terminal status (`completed`, `failed`, or `cancelled`). Defaults to `true`. ## Return value The WebSocket connection status. The lifecycle status of the Inngest function run. Updated from run-level messages on the channel. Convenience boolean for `connectionStatus === "paused"`. Why the hook is paused. `hidden` means the document is hidden and `pauseOnHidden` is enabled. `disabled` means `enabled` is `false`. The latest message per subscribed topic. Access typed data with `messages.byTopic.topicName?.data`. All retained messages in chronological order, bounded by `historyLimit`. The most recently flushed message across all subscribed topics. The newest batch of flushed messages. When buffering is disabled, this is a single-message array for the latest message. The most recent connection error, or `null` if connected successfully. The function's return value, extracted from the terminal run message when the function completes. Clears the retained messages, result, and error state and resets the hook back to its initial state. ## Connection status The `connectionStatus` field tracks the WebSocket connection lifecycle: | Status | Description | |--------|-------------| | `idle` | Hook is mounted but hasn't started connecting (e.g., `enabled: false`) | | `connecting` | Establishing the WebSocket connection | | `open` | Connected and receiving messages | | `paused` | The hook is intentionally paused because `enabled` is `false` or the tab is hidden | | `closed` | Connection closed (manually or by `autoCloseOnTerminal`) | | `error` | Connection failed. Check `error` for details | ## Run status The `runStatus` field tracks the Inngest function's execution lifecycle: | Status | Description | |--------|-------------| | `unknown` | No run status received yet | | `running` | Function is actively executing | | `completed` | Function finished successfully. `result` is available | | `failed` | Function failed after exhausting retries | | `cancelled` | Function was cancelled | When `autoCloseOnTerminal` is `true` (the default), the subscription closes automatically once `runStatus` reaches `completed`, `failed`, or `cancelled`. ## Token factory pattern The recommended approach is to pass an async factory function for `token`. This function is called when the hook mounts and on each reconnect, ensuring fresh tokens. ```tsx {{ title: "Next.js Server Action" }} // app/actions.ts "use server"; export async function getToken(contentId: string) { return getClientSubscriptionToken(inngest, { channel: pipelineChannel({ contentId }), topics: ["status", "tokens"], }); } // app/page.tsx "use client"; function Pipeline({ contentId }: { contentId: string }) { ["status", "tokens"] as const; useRealtime({ channel: pipelineChannel({ contentId }), topics, token: () => getToken(contentId), }); return (

{connectionStatus}: {messages.byTopic.status?.data.message}

); } ``` ```tsx {{ title: "API Route" }} // app/api/realtime-token/route.ts export async function GET(req: Request) { new URL(req.url); searchParams.get("contentId")!; await getClientSubscriptionToken(inngest, { channel: pipelineChannel({ contentId }), topics: ["status", "tokens"], }); return Response.json(token); } // app/page.tsx "use client"; function Pipeline({ contentId }: { contentId: string }) { ["status", "tokens"] as const; useRealtime({ channel: pipelineChannel({ contentId }), topics, token: () => fetch(`/api/realtime-token?contentId=${contentId}`).then((r) => r.json()), }); return

{messages.byTopic.status?.data.message}

; } ```
## Pre-minted loader tokens If your framework passes data from a server loader into a client component, you can pass the `ClientSubscriptionToken` from `getClientSubscriptionToken()` directly. Keep `channel` and `topics` as top-level `useRealtime` options; the client token intentionally contains only `key` and `apiBaseUrl`, and the top-level channel and topics are what preserve typed message inference. ```tsx export function Pipeline() { useLoaderData(); pipelineChannel({ contentId }); ["status", "tokens"] as const; useRealtime({ channel, topics, token: realtimeToken, }); return

{connectionStatus}: {messages.byTopic.status?.data.message}

; } ``` For long-lived subscriptions, prefer a token factory or route handler so `useRealtime` can request a fresh token when it reconnects. ## Typed topic access When you pass a channel instance instead of a plain string, `messages.byTopic`, `messages.all`, `messages.last`, and `messages.delta` all stay typed to the subscribed topics: ```tsx pipelineChannel({ contentId }); ["status", "tokens"] as const; useRealtime({ channel: ch, topics, token: () => getToken(contentId), }); messages.byTopic.status?.data.message; // string messages.byTopic.tokens?.data.token; // string for (const message of messages.delta) { if (message.kind === "run") continue; if (message.topic === "status") { message.data.message; // string } } ``` ## History management By default, `messages.all` retains the last 100 messages. Adjust with `historyLimit`: ```tsx // Keep last 500 messages useRealtime({ channel: ch, topics: ["status"], token: () => getToken(contentId), historyLimit: 500, }); // Keep all messages (unbounded, use with caution) useRealtime({ channel: ch, topics: ["status"], token: () => getToken(contentId), historyLimit: null, }); // Keep only 10 messages useRealtime({ channel: ch, topics: ["status"], token: () => getToken(contentId), historyLimit: 10, }); ``` ## Buffering For high-frequency streams like token-by-token AI output, use `bufferInterval` to batch re-renders: ```tsx useRealtime({ channel: ch, topics: ["tokens"], token: () => getToken(contentId), bufferInterval: 100, // Batch messages, re-render at most every 100ms }); messages.delta; // up to 100ms of new messages per flush messages.all; // retained history after each flush ``` `messages.byTopic` still tracks the latest message for each topic, while `messages.all`, `messages.last`, and `messages.delta` are flushed on the buffer interval. ## Conditional subscription Use `enabled` to start or stop the subscription without unmounting the component: ```tsx useState(null); contentId ? pipelineChannel({ contentId }) : undefined; useRealtime({ channel, topics: ["status"], token: contentId ? () => getToken(contentId) : undefined, enabled: !!contentId, }); connectionStatus; // "idle" until a run exists, then "connecting" | "open" | ... isPaused; // true when disabled or hidden pauseReason; // "disabled" | "hidden" | null ``` If your token is a `ClientSubscriptionToken`, or your token factory returns either a `ClientSubscriptionToken` or only a string key, make sure `channel` and `topics` are present. If it returns a full token object, the hook can derive them from the token instead, though top-level `channel` and `topics` are still recommended for typed message inference. # Serve Source: https://www.inngest.com/docs/reference/typescript/v4/serve/index Description: Configure signing key, base URL, streaming mode, allowed origins, and framework-specific options. metaTitle = "serve() Configuration | TypeScript SDK v4 Reference" The `serve()` API handler is used to serve your application's [functions](/docs/reference/typescript/v4/functions/create) via HTTP. This handler enables Inngest to remotely and securely read your functions' configuration and invoke your function code. This enables you to host your function code on any platform. ```ts // or your preferred framework import { importProductImages, sendSignupEmail, summarizeText, } from "./functions"; serve({ client: inngest, functions: [sendSignupEmail, summarizeText, importProductImages], }); ``` `serve` handlers are imported from convenient framework-specific packages like `"inngest/next"`, `"inngest/express"`, or `"inngest/lambda"`. [Click here for a full list of officially supported frameworks](/docs/learn/serving-inngest-functions). For any framework that is not support, you can [create a custom handler](#custom-frameworks). --- ## `serve(options)` An Inngest client ([reference](/docs/reference/typescript/v4/client/create)). An array of Inngest functions defined using `inngest.createFunction()` ([reference](/docs/reference/typescript/v4/functions/create)). The domain host of your application, _including_ protocol, e.g. `https://myapp.com`. The SDK attempts to infer this via HTTP headers at runtime, but this may be required when using platforms like AWS Lambda or when using a reverse proxy. See also [`INNGEST_SERVE_ORIGIN`](/docs/sdk/environment-variables#inngest-serve-origin). The path where your `serve` handler is hosted. The SDK attempts to infer this via HTTP headers at runtime. We recommend `/api/inngest`. See also [`INNGEST_SERVE_PATH`](/docs/sdk/environment-variables#inngest-serve-path). Enables streaming responses back to Inngest which can enable maximum serverless function timeouts. See [reference](/docs/streaming) for more information on the configuration. See also [`INNGEST_STREAMING`](/docs/sdk/environment-variables#inngest-streaming). Options like `signingKey`, `signingKeyFallback`, `logger`, `baseUrl`, and `fetch` are configured on the [Inngest client](/docs/reference/typescript/v4/client/create), not on `serve()`. We always recommend setting the [`INNGEST_SIGNING_KEY`](/docs/sdk/environment-variables#inngest-signing-key) environment variable over using the `signingKey` option directly. As with any secret, it's not a good practice to hard-code the signing key in your codebase. ## How the `serve` API handler works The API works by exposing a single endpoint at `/api/inngest` which handles different actions utilizing HTTP request methods: - `GET`: Return function metadata and render a landing page in **development only**. - `POST`: Invoke functions with the request body as incoming function state. - `PUT`: Trigger the SDK to register all functions with Inngest using the signing key. # Streaming Source: https://www.inngest.com/docs/reference/typescript/v4/serve/streaming Description: Enable HTTP streaming in the Inngest TypeScript SDK v4 serve handler to improve responsiveness on serverless platforms with execution time limits. metaTitle = "Streaming with serve() | TypeScript SDK v4 Reference" This page covers streaming responses **back to Inngest** to extend serverless timeouts. To stream data **to clients** from Durable Endpoints, see [Durable Endpoints streaming](/docs/reference/typescript/v4/durable-endpoints#streaming?ref=docs-serve-streaming). In select environments, the SDK allows streaming responses back to Inngest, hugely increasing maximum timeouts on many serverless platforms up to 15 minutes. While we add wider support for streaming to other platforms, we currently support the following: - [Cloudflare Workers](/docs/learn/serving-inngest-functions#framework-cloudflare-workers) - [Express](/docs/learn/serving-inngest-functions#framework-express) - [Next.js on Vercel Fluid Compute or Edge Functions](/docs/learn/serving-inngest-functions#framework-next-js) - [Remix on Vercel Edge Functions](/docs/learn/serving-inngest-functions#framework-remix) ## Enabling streaming Select your platform above and follow the relevant "Streaming" section to enable streaming for your application. Every Inngest serve handler provides a `streaming` option, for example: ```ts serve({ client: inngest, functions: [...fns], streaming: true, }); ``` This can be one of the following values: - `false` - Streaming will never be used. This is the default. - `true` - Streaming will be used. If the serve handler does not support streaming, an error will be thrown. In v3, streaming accepted `"allow"` and `"force"` string values. In v4, these were simplified to `true | false`. See the [migration guide](/docs/reference/typescript/v4/migrations/v3-to-v4#simplify-streaming-option) for details. # Testing Source: https://www.inngest.com/docs/reference/typescript/v4/testing/index Description: Test Inngest functions in TypeScript SDK v4 using InngestTestEngine. Run in isolation, mock step outputs, and assert on behavior without a live server. metaTitle = "Testing Inngest Functions | TypeScript SDK v4" To test your Inngest functions programmatically, use the `@inngest/test` library, available on [npm](https://www.npmjs.com/package/@inngest/test) and [JSR](https://jsr.io/@inngest/test). This allows you to mock function state, step tooling, and inputs with a Jest-compatible API supporting all major testing frameworks, runtimes, and libraries: - `jest` - `vitest` - `bun:test` (Bun) - `@std/expect` (Deno) - `chai`/`expect` ## Installation The `@inngest/test` package requires `inngest@>=4.0.0`. ```shell {{ title: "npm" }} npm install -D @inngest/test ``` ```shell {{ title: "Yarn" }} yarn add -D @inngest/test ``` ```shell {{ title: "pnpm" }} pnpm add -D @inngest/test ``` ```shell {{ title: "Bun" }} bun add -d @inngest/test ``` ```shell {{ title: "Deno" }} deno add --dev @inngest/test # or with JSR... deno add --dev jsr:@inngest/test ``` ## Unit tests Use whichever supported testing framework; `@inngest/test` is unopinionated about how your tests are run. We'll demonstrate here using `jest`. Import `InngestTestEngine`, our function to test, and create a new `InngestTestEngine` instance. ```ts describe("helloWorld function", () => { new InngestTestEngine({ function: helloWorld, }); }); ``` Now we can use the primary API for testing, `t.execute()`: ```ts test("returns a greeting", async () => { await t.execute(); expect(result).toEqual("Hello World!"); }); ``` This will run the entire function (steps and all) to completion, then return the response from the function, where we assert that it was the string `"Hello World!"`. A serialized `error` will be returned instead of `result` if the function threw: ```ts test("throws an error", async () => { await t.execute(); expect(error).toContain("Some specific error"); }); ``` When using steps that delay execution, like `step.sleep` or `step.waitForEvent`, you will need to mock them. [Learn more about mocking steps](#steps). ### Running an individual step `t.executeStep()` can be used to run the function until a particular step has been executed. This is useful to test a single step within a function or to see that a non-runnable step such as `step.waitForEvent()` has been registered with the correct options. ```ts test("runs the price calculations", async () => { await t.executeStep("calculate-price"); expect(result).toEqual(123); }); ``` Assertions can also be made on steps in any part of a run, regardless of if that's the checkpoint we've waited for. See [Assertions -> State](#assertions). ### Assertions `@inngest/test` adds Jest-compatible mocks by default that can help you assert function and step input and output. You can assert: - Function input - Function output - Step output - Step tool usage All of these values are returned from both `t.execute()` and `t.executeStep()`; we'll only show one for simplicity here. The `result` is returned, which is the output of the run or step: ```ts await t.execute(); expect(result).toEqual("Hello World!"); ``` `ctx` is the input used for the function run. This can be used to assert outputs that are based on input data such as `event` or `runId`, or to confirm that middleware is working correctly and affecting input arguments. ```ts await t.execute(); expect(result).toEqual(`Run ID was: "${ctx.runId}"`); ``` The step tooling at `ctx.step` are all Jest-compatible spy functions, so you can use them to assert that they've been called and used correctly: ```ts await t.execute(); expect(ctx.step.run).toHaveBeenCalledWith("my-step", expect.any(Function)); ``` `state` is also returned, which is a view into the outputs of all steps in the run. This allows you to test each individual step output for any given input: ```ts await t.execute(); expect(state["my-step"]).resolves.toEqual("some successful output"); expect(state["dangerous-step"]).rejects.toThrowError("something failed"); ``` ### Mocking Some mocking is done automatically by `@inngest/test`, but can be overwritten if needed. All mocks detailed below can be specified either when creating an `InngestTestEngine` instance or for each individual execution: ```ts // Set the events for every execution new InngestTestEngine({ function: helloWorld, // mocks here }); // Or for just one, which will overwrite any current event mocks t.execute({ // mocks here }); t.executeStep("my-step", { // mocks here }) ``` You can also clone an existing `InngestTestEngine` instance to encourage re-use of complex mocks: ```ts // Make a direct clone, which includes any mocks t.clone(); // Provide some more mocks in addition to any existing ones t.clone({ // mocks here }); ``` For simplicity, the following examples will show usage of `t.execute()`, but the mocks can be placed in any of these locations. #### Events The incoming event data can be mocked. They are always specified as an array of events to allow also mocking batches. ```ts t.execute({ events: [{ name: "demo/event.sent", data: { message: "Hi!" } }], }); ``` If no event mocks are given at all (or `events: undefined` is explicitly set), an `inngest/function.invoked` event will be mocked for you. #### Steps Mocking steps can help you model different paths and situations within your function. To do so, any step can be mocked by providing the `steps` option. You should always mock `sleep` and `waitForEvent` steps - [learn more here](#sleep-and-wait-for-event). Here we mock two steps, one that will run successfully and another that will model a failure and throw an error: ```ts t.execute({ steps: [ { id: "successful-step", handler() { return "We did it!"; }, }, { id: "dangerous-step", handler() { throw new Error("Oh no!"); }, }, ], }); ``` These handlers will run lazily when they are found during a function's execution. This means you can write complex mocks that respond to other information: ```ts let message = ""; t.execute({ steps: [ { id: "build-greeting", handler() { message = "Hello, "; return message; }, }, { id: "build-name", handler() { return message + " World!"; }, }, ], }); ``` #### Sleep and waitForEvent Steps that pause the function, `step.sleep`, `step.sleepUntil`, and `step.waitForEvent` should always be mocked. ```ts {{ title: 'step.sleep' }} // Given the following function that sleeps inngest.createFunction( { id: "my-function", triggers: { event: "user.created" }, }, async ({ event, step }) => { await step.sleep("one-day-delay", "1d"); return { message: "success" }; } ) // Mock the step to execute a no-op handler to return immediately t.execute({ steps: [ { id: "one-day-delay", handler() {}, // no return value necessary }, ], }); ``` ```ts {{ title: "step.waitForEvent" }} // Given the following function that sleeps inngest.createFunction( { id: "my-function", triggers: { event: "time_off.requested" }, }, async ({ event, step }) => { await step.waitForEvent("wait-for-approval", { event: "manager.approved", timeout: "1d", }); return { message: evt?.data.message }; } ) // Mock the step to return null to simulate a timeout t.execute({ steps: [ { id: "wait-for-approval", handler() { // A timeout will return null return null; }, }, ], }); // Mock the step to return an event t.execute({ steps: [ { id: "wait-for-approval", handler() { // If the event is approved, it will be returned return { name: 'manager.approved', data: { message: 'This looks great!' } }; }, }, ], }); ``` #### Modules and imports Any mocking of modules or imports outside of Inngest which your functions may rely on should be done outside of Inngest with the testing framework you're using. Here are some links to the major supported frameworks and their guidance for mocking imports: - [`jest`](https://jestjs.io/docs/mock-functions#mocking-modules) - [`vitest`](https://vitest.dev/guide/mocking#modules) - [`bun:test` (Bun)](https://bun.sh/docs/test/mocks#module-mocks-with-mock-module) - [`@std/testing` (Deno)](https://jsr.io/@std/testing/doc/mock/~) #### Custom You can also provide your own custom mocks for the function input. When instantiating a new `InngestTestEngine` or starting an execution, provide a `transformCtx` function that will add these mocks every time the function is run: ```ts new InngestTestEngine({ function: helloWorld, transformCtx: (ctx) => { return { ...ctx, event: someCustomThing, }; }, }); ``` If you wish to still add the automatic mocking from `@inngest/test` (such as the spies on `ctx.step.*`), you can import and use the automatic transforms as part of your own: ```ts new InngestTestEngine({ function: helloWorld, transformCtx: (ctx) => { return { ...mockCtx(ctx), event: someCustomThing, }; }, }); ``` # Inngest client Source: https://www.inngest.com/docs/reference/python/client/overview Description: Initialize with your app ID, API key, and environment settings to send events and register functions. metaTitle = "Inngest Python Client | inngest.Inngest() Reference" The Inngest client is used to configure your application and send events outside of Inngest functions. ```py import inngest inngest_client = inngest.Inngest( app_id="flask_example", ) ``` --- ## Configuration Override the default base URL for our REST API (`https://api.inngest.com/`). See also the [`INNGEST_EVENT_API_BASE_URL`](/docs/reference/python/overview/env-vars#inngest-event-api-base-url) environment variable. A unique identifier for your application. We recommend a hyphenated slug. The environment name. Required only when using [Branch Environments](/docs/platform/environments). Override the default base URL for sending events (`https://inn.gs/`). See also the [`INNGEST_EVENT_API_BASE_URL`](/docs/reference/python/overview/env-vars#inngest-event-api-base-url) environment variable. An Inngest event key. Alternatively, set the [`INNGEST_EVENT_KEY`](/docs/reference/python/overview/env-vars#inngest-event-key) environment variable. Whether the SDK should run in [production mode](/docs/reference/python/overview/prod-mode). See also the [`INNGEST_DEV`](/docs/reference/python/overview/env-vars#inngest-dev) environment variable. A logger object derived from `logging.Logger` or `logging.LoggerAdapter`. Defaults to using `logging.getLogger(__name__)` if not provided. A list of middleware to add to the client. Read more in our [middleware docs](/docs/reference/python/middleware/overview). The Inngest signing key. Alternatively, set the [`INNGEST_SIGNING_KEY`](/docs/reference/python/overview/env-vars#inngest-signing-key) environment variable. # Send events Source: https://www.inngest.com/docs/reference/python/client/send Description: Use client.send() to send one or more events to Inngest from Python. Supports async usage with asyncio and integrates with FastAPI, Flask, and Django. metaTitle = "Send Events | Python SDK Reference" 💡️ This guide is for sending events from *outside* an Inngest function. To send events within an Inngest function, refer to the [step.send_event](/docs/reference/python/steps/send-event) guide. Sends 1 or more events to the Inngest server. Returns a list of the event IDs. ```py import inngest inngest_client = inngest.Inngest(app_id="my_app") # Call the `send` method if you're using async/await ids = await inngest_client.send( inngest.Event(name="my_event", data={"msg": "Hello!"}) ) # Call the `send_sync` method if you aren't using async/await ids = inngest_client.send_sync( inngest.Event(name="my_event", data={"msg": "Hello!"}) ) # Can pass a list of events ids = await inngest_client.send( [ inngest.Event(name="my_event", data={"msg": "Hello!"}), inngest.Event(name="my_other_event", data={"name": "Alice"}), ] ) ``` ## `send` Only for async/await code. 1 or more events to send. Any data to associate with the event. A unique ID used to idempotently trigger function runs. If duplicate event IDs are seen, only the first event will trigger function runs. The event name. We recommend using lowercase dot notation for names (e.g. `app/user.created`) A timestamp integer representing the time (in milliseconds) at which the event occurred. Defaults to the time the Inngest receives the event. If the `ts` time is in the future, function runs will be scheduled to start at the given time. This has the same effect as sleeping at the start of the function. Note: This does not apply to functions waiting for events. Functions waiting for events will immediately resume, regardless of the timestamp. ## `send_sync` Blocks the thread. If you're using async/await then use `send` instead. Arguments are the same as `send`. # Create Function Source: https://www.inngest.com/docs/reference/python/functions/create Description: inngest_client.create_function() configures function ID, triggers, retries, concurrency, and the handler signature in the Inngest Python SDK. metaTitle = "Create a Function | Python SDK Reference" Define your functions using the `create_function` decorator. ```py import inngest @inngest_client.create_function( fn_id="import-product-images", trigger=inngest.TriggerEvent(event="shop/product.imported"), ) async def fn(ctx: inngest.Context): # Your function code ``` --- ## `create_function` The `create_function` decorator accepts a configuration and wraps a plain function. ### Configuration Configure how the function should consume batches of events ([reference](/docs/guides/batching)) The maximum number of events a batch can have. Current limit is `100`. How long to wait before invoking the function with the batch even if it's not full. Current permitted values are between 1 second and 1 minute. If you pass an `int` then it'll be interpreted in milliseconds. Define an event that can be used to cancel a running or sleeping function ([guide](/docs/guides/cancel-running-functions)) The event name which will be used to cancel A match expression using arbitrary event data. For example, `event.data.user_id == async.data.user_id` will only match events whose `data.user_id` matches the original trigger event's `data.user_id`. The amount of time to wait to receive the cancelling event. If you pass an `int` then it'll be interpreted in milliseconds. Options to configure function debounce ([reference](/docs/reference/typescript/v4/functions/debounce)) A unique key expression to apply the debounce to. The expression is evaluated for each triggering event. Expressions are defined using the Common Expression Language (CEL) with the events accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Debounce per customer id: `'event.data.customer_id'` * Debounce per account and email address: `'event.data.account_id + "-" + event.user.email'` The time period of which to set the limit. The period begins when the first matching event is received. How long to wait before invoking the function with the batch even if it's not full. If you pass an `int` then it'll be interpreted in milliseconds. A unique identifier for your function. This should not change between deploys. A name for your function. If defined, this will be shown in the UI as a friendly display name instead of the ID. A function that will be called only when this Inngest function fails after all retries have been attempted ([reference](/docs/reference/typescript/v4/functions/handling-failures)) Configure function run prioritization. An expression which must return an integer between -600 and 600 (by default), with higher return values resulting in a higher priority. Examples: * Return the priority within an event directly: `event.data.priority` (where `event.data.priority` is an int within your account's range) * Rate limit by a string field: `event.data.plan == 'enterprise' ? 180 : 0` See [reference](/docs/reference/typescript/v4/functions/run-priority) for more information. Options to configure how to rate limit function execution ([reference](/docs/reference/typescript/v4/functions/rate-limit)) A unique key expression to apply the limit to. The expression is evaluated for each triggering event. Expressions are defined using the Common Expression Language (CEL) with the events accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Rate limit per customer id: `'event.data.customer_id'` * Rate limit per account and email address: `'event.data.account_id + "-" + event.user.email'` The maximum number of functions to run in the given time period. The time period of which to set the limit. The period begins when the first matching event is received. How long to wait before invoking the function with the batch even if it's not full. Current permitted values are from 1 second to 1 minute. If you pass an `int` then it'll be interpreted in milliseconds. Configure the number of times the function will be retried from `0` to `20`. Default: `4` Options to configure how to throttle function execution The maximum number of functions to run in the given time period. A unique key expression to apply the limit to. The expression is evaluated for each triggering event. Expressions are defined using the Common Expression Language (CEL) with the events accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more info. Examples: * Rate limit per customer id: `'event.data.customer_id'` * Rate limit per account and email address: `'event.data.account_id + "-" + event.user.email'` The time period of which to set the limit. The period begins when the first matching event is received. How long to wait before invoking the function with the batch even if it's not full. Current permitted values are from 1 second to 1 minute. If you pass an `int` then it'll be interpreted in milliseconds. A key expression used to prevent duplicate events from triggering a function more than once in 24 hours. [Read the idempotency guide here](/docs/guides/handling-idempotency). Expressions are defined using the Common Expression Language (CEL) with the original event accessible using dot-notation. Read [our guide to writing expressions](/docs/guides/writing-expressions) for more information. What should trigger the function to run. Either an event or a cron schedule. Use a list to specify multiple triggers. --- ## Triggers ### `TriggerEvent` The name of the event. A match expression using arbitrary event data. For example, `event.data.user_id == async.data.user_id` will only match events whose `data.user_id` matches the original trigger event's `data.user_id`. ### `TriggerCron` A [unix-cron](https://crontab.guru/) compatible schedule string.
Optional timezone prefix, e.g. `TZ=Europe/Paris 0 12 * * 5`.
A duration string (e.g. `"30s"`, `"5m"`) that adds a random delay after the scheduled boundary. Each occurrence fires at a random time within the jitter window. Must be between `"1s"` and `"5m"`. See the [jitter guide](/docs/guides/scheduled-functions#adding-jitter) for details.
### Multiple Triggers Multiple triggers can be defined by setting the `trigger` option to a list of `TriggerEvent` or `TriggerCron` objects: ```py import inngest @inngest_client.create_function( fn_id="import-product-images", trigger=[ inngest.TriggerEvent(event="shop/product.imported"), inngest.TriggerEvent(event="shop/product.updated"), ], ) async def fn(ctx: inngest.Context): # Your function code ``` For more information, see the [Multiple Triggers](/docs/guides/multiple-triggers) guide. --- ## Handler The handler is your code that runs whenever the trigger occurs. Every function handler receives a single object argument which can be deconstructed. The key arguments are `event` and `step`. Note, that scheduled functions that use a `cron` trigger will not receive an `event` argument. ```py @inngest_client.create_function( # Function options ) async def fn(ctx: inngest.Context): # Function code ``` ### `ctx` The current zero-indexed attempt number for this function execution. The first attempt will be 0, the second 1, and so on. The attempt number is incremented every time the function throws an error and is retried. The event payload `object` that triggered the given function run. The event payload object will match what you send with [`inngest.send()`](/docs/reference/typescript/v4/events/send). Below is an example event payload object: The event payload data. Time (Unix millis) the event was received by the Inngest server. A list of `event` objects that's accessible when the `batch_events` is set on the function configuration. If batching is not configured, the list contains a single event payload matching the `event` argument. A proxy object around either the logger you provided or the default logger. The unique ID for the given function run. This can be useful for logging and looking up specific function runs in the Inngest dashboard. ### `step` The `step` object has a method for each kind of step in the Inngest platform. If your function is `async` then its type is `Step` and you can use `await` to call its methods. If your function is not `async` then its type is `SyncStep`. [Docs](/docs/reference/python/steps/run) [Docs](/docs/reference/python/steps/send-event) [Docs](/docs/reference/python/steps/sleep) [Docs](/docs/reference/python/steps/sleep-until) [Docs](/docs/reference/python/steps/parallel) # Modal Source: https://www.inngest.com/docs/reference/python/guides/modal Description: Deploy Inngest Python functions on Modal for scalable serverless GPU or CPU workloads. Configure the serve endpoint and sync your app with the Inngest platform. metaTitle = "Run Inngest Functions on Modal | Python Guide" This guide will help you use setup an Inngest app in [Modal](https://modal.com), a platform for building and deploying serverless Python applications. ## Setting up your development environment This section will help you setup your development environment. You'll have an Inngest Dev Server running locally and a FastAPI app running in Modal. ### Creating a tunnel Since we need bidirectional communication between the Dev Server and your app, you'll also need a tunnel to allow your app to reach your locally-running Dev Server. We recommend using [ngrok](https://ngrok.com) for this. ```sh # Tunnel to the Dev Server's port ngrok http 8288 ``` This should output a public URL that can reach port `8288` on your machine. The URL can be found in the `Forwarding` part of ngrok's output: ``` Forwarding https://23ef-173-10-53-121.ngrok-free.app -> http://localhost:8288 ``` ### Creating and deploying a FastAPI app Create an `.env` file that contains the tunnel URL: ``` INNGEST_DEV=https://23ef-173-10-53-121.ngrok-free.app ``` Create a dependency file that Modal will use to install dependencies. For this guide, we'll use `requirements.txt`: ``` fastapi==0.115.0 inngest==0.4.12 python-dotenv==1.0.1 ``` Create a `main.py` file that contains your FastAPI app: ```py import os from dotenv import load_dotenv from fastapi import FastAPI import inngest import inngest.fast_api import modal load_dotenv() app = modal.App("test-fast-api") # Load all environment variables that start with "INNGEST_" env: dict[str, str] = {} for k, v, in os.environ.items(): if k.startswith("INNGEST_"): env[k] = v image = ( modal.Image.debian_slim() .pip_install_from_requirements("requirements.txt") .env(env) ) fast_api_app = FastAPI() # Create an Inngest client inngest_client = inngest.Inngest(app_id="fast_api_example") # Create an Inngest function @inngest_client.create_function( fn_id="my-fn", trigger=inngest.TriggerEvent(event="my-event"), ) async def fn(ctx: inngest.Context) -> str: print(ctx.event) return "done" # Serve the Inngest endpoint (its path is /api/inngest) inngest.fast_api.serve(fast_api_app, inngest_client, [fn]) @app.function(image=image) @modal.asgi_app() def fastapi_app(): return fast_api_app ``` Deploy your app to Modal: ```sh modal deploy main.py ``` Your terminal should show the deployed app's URL: ``` └── 🔨 Created web function fastapi_app => https://test-fast-api-fastapi-app.modal.run ``` To test whether the deploy worked, send a request to the Inngest endpoint (note that we added the `/api/inngest` to the Modal URL). It should output JSON similar to the following: ```sh $ curl https://test-fast-api-fastapi-app.modal.run/api/inngest {"schema_version": "2024-05-24", "authentication_succeeded": null, "function_count": 1, "has_event_key": false, "has_signing_key": false, "has_signing_key_fallback": false, "mode": "dev"} ``` ### Syncing with the Dev Server Start the Dev Server, specifying the FastAPI app's Inngest endpoint: ```shell {{ title: "Bash installer" }} # Install the CLI - follow the steps to complete install: curl -sSfL https://cli.inngest.com/install.sh | sh # Run the dev server inngest dev -u https://test-fast-api-fastapi-app.modal.run/api/inngest --no-discovery ``` ```shell {{ title: "Docker" }} docker run -p 8288:8288 -p 8289:8289 inngest/inngest \ inngest dev -u https://test-fast-api-fastapi-app.modal.run/api/inngest --no-discovery ``` In your browser, navigate to `http://127.0.0.1:8288/apps`. Your app should be successfully synced. ## Deploying to production A production Inngest app is very similar to an development app. The only difference is with environment variables: - `INNGEST_DEV` must not be set. Alternatively, you can set it to `0`. - `INNGEST_EVENT_KEY` must be set. Its value can be found on the [event keys page](https://app.inngest.com/env/production/manage/keys). - `INNGEST_SIGNING_KEY` must be set. Its value can be found on the [signing key page](https://app.inngest.com/env/production/manage/signing-key). Once your app is deployed with these environment variables, you can sync it on our [new app page](https://app.inngest.com/env/production/apps/sync-new). For more information about syncing, please see our [docs](/docs/apps/cloud). # Pydantic Source: https://www.inngest.com/docs/reference/python/guides/pydantic Description: Use Pydantic models for type-safe event data and step outputs in Inngest Python functions. Automatic validation and serialization for structured payloads. metaTitle = "Pydantic Integration | Inngest Python SDK" This guide will help you use Pydantic to perform runtime type validation when sending and receiving events. ## Step output Steps can return Pydantic objects as long as the `output_type` parameter is set to the Pydantic model return type. ```py client = inngest.Inngest( app_id="my-app", # Must set the client serializer when using Pydantic output serializer=inngest.PydanticSerializer(), ) class User(pydantic.BaseModel): name: str async def get_user() -> User: return User(name="Alice") @client.create_function( fn_id="my-fn", trigger=inngest.TriggerEvent(event="my-event"), ) async def my_fn(ctx: inngest.Context) -> None: # user object is a Pydantic object at both runtime and compile time user = await ctx.step.run("get-user", get_user, output_type=User) ``` More complex types work as well. For example, if the `get_user` function returned an `list[Admin | User]` type, you could set the `output_type` to `list[Admin | User]`. ```py await ctx.step.run("get-person", get_users, output_type=list[User | Admin]) ``` Why do I need to set the `output_type` parameter? Since step output is transmitted as JSON back to the Inngest server, we lose the reference to the original Python class. So the `output_type` parameter is used to deserialize the JSON back into the correct type. Why can't the SDK infer the output type from my type annotations? The could sometimes work, but there are common patterns that break it. Runtime return type inference is impossible if the return type: - Is implicit (i.e. not specified but your type checker figures it out). - Is a generic. ## Function output Functions can return Pydantic objects as long as the `output_type` parameter is set to the Pydantic model return type. ```py client = inngest.Inngest( app_id="my-app", # Must set the client serializer when using Pydantic output serializer=inngest.PydanticSerializer(), ) class User(pydantic.BaseModel): name: str @client.create_function( fn_id="my-fn", output_type=User, trigger=inngest.TriggerEvent(event="my-event"), ) async def my_fn(ctx: inngest.Context) -> None: return User(name="Alice") ``` ## Sending events Create a base class that all your event classes will inherit from. This class has methods to convert to and from `inngest.Event` objects. ```py import inngest import pydantic import typing TEvent = typing.TypeVar("TEvent", bound="BaseEvent") class BaseEvent(pydantic.BaseModel): data: pydantic.BaseModel id: str = "" name: typing.ClassVar[str] ts: int = 0 @classmethod def from_event(cls: type[TEvent], event: inngest.Event) -> TEvent: return cls.model_validate(event.model_dump(mode="json")) def to_event(self) -> inngest.Event: return inngest.Event( name=self.name, data=self.data.model_dump(mode="json"), id=self.id, ts=self.ts, ) ``` Next, create a Pydantic model for your event. ```py class PostUpvotedEventData(pydantic.BaseModel): count: int class PostUpvotedEvent(BaseEvent): data: PostUpvotedEventData name: typing.ClassVar[str] = "forum/post.upvoted" ``` Since Pydantic validates on instantiation, the following code will raise an error if the data is invalid. ```py client.send( PostUpvotedEvent( data=PostUpvotedEventData(count="bad data"), ).to_event() ) ``` ## Receiving events When defining your Inngest function, use the `name` class field when specifying the trigger. Within the function body, call the `from_event` class method to convert the `inngest.Event` object to your Pydantic model. ```py @client.create_function( fn_id="handle-upvoted-post", trigger=inngest.TriggerEvent(event=PostUpvotedEvent.name), ) def fn(ctx: inngest.ContextSync) -> None: event = PostUpvotedEvent.from_event(ctx.event) ``` # Testing Source: https://www.inngest.com/docs/reference/python/guides/testing Description: Write unit tests for Inngest Python functions using the test client. Test step logic, event handling, and retry behavior without a live Inngest environment. metaTitle = "Testing Inngest Python Functions | Unit Test Guide" ## Unit testing If you'd like to unit test without an Inngest server, the `mocked` (requires `v0.4.14+`) library can simulate much of the Inngest server's behavior. The `mocked` library is experimental. It may have interface and behavioral changes that don't follow semantic versioning. Let's say you've defined this function somewhere in your app: ```python import inngest def create_message(name: object) -> str: return f"Hello, {name}!" client = inngest.Inngest(app_id="my-app") @client.create_function( fn_id="greet", trigger=inngest.TriggerEvent(event="user.login"), ) async def greet(ctx: inngest.Context) -> str: message = await ctx.step.run( "create-message", create_message, ctx.event.data["name"], ) return message ``` You can unit test it like this: ```python import unittest import inngest from inngest.experimental import mocked from .functions import greet # Mocked Inngest client. The app_id can be any string (it's currently unused) client_mock = mocked.Inngest(app_id="test") # A normal Python test class class TestGreet(unittest.TestCase): def test_greet(self) -> None: # Trigger the function with an in-memory, simulated Inngest server res = mocked.trigger( greet, inngest.Event(name="user.login", data={"name": "Alice"}), client_mock, ) # Assert that it ran as expected assert res.status is mocked.Status.COMPLETED assert res.output == "Hello, Alice!" ``` ### Limitations The `mocked` library has some notable limitations: - `ctx.step.invoke` and `ctx.step.wait_for_event` must be stubbed using the `step_stubs` parameter of `mocked.trigger`. - `step.send_event` does not send events. It returns a stubbed value. - `step.sleep` and `step.sleep_until` always sleep for 0 seconds. ### Stubbing Stubbing is required for `ctx.step.invoke` and `ctx.step.wait_for_event`. Here's an example of how to stub these functions: ```python # Real production function @client.create_function( fn_id="signup", trigger=inngest.TriggerEvent(event="user.signup"), ) def signup(ctx: inngest.ContextSync) -> bool: email_id = ctx.step.invoke( "send-email", function=send_email, ) event = ctx.step.wait_for_event( "wait-for-reply", event="email.reply", if_exp=f"async.data.email_id == '{email_id}'", timeout=datetime.timedelta(days=1), ) user_replied = event is not None return user_replied # Mocked Inngest client client_mock = mocked.Inngest(app_id="test") class TestSignup(unittest.TestCase): def test_signup(self) -> None: res = mocked.trigger( fn, inngest.Event(name="test"), client_mock, # Stub the invoke and wait_for_event steps. The keys are the step # IDs step_stubs={ "send-email": "email-id-abc123", "wait-for-reply": inngest.Event( data={"text": "Sounds good!"}, name="email.reply" ), }, ) assert res.status is mocked.Status.COMPLETED assert res.output is True ``` To simulate a `ctx.step.wait_for_event` timeout, stub the step with `mocked.Timeout`. ## Integration testing If you'd like to start and stop a real Dev Server with your integration tests, the `dev_server` (requires `v0.4.15+`) library can help. It requires `npm` to be installed on your machine. The `dev_server` library is experimental. It may have interface and behavioral changes that don't follow semantic versioning. You can use the library in your `conftest.py`: ```python import pytest from inngest.experimental import dev_server def pytest_configure(config: pytest.Config) -> None: dev_server.server.start() def pytest_unconfigure(config: pytest.Config) -> None: dev_server.server.stop() ``` This Dev Server will not automatically discover your app. You'll need to manually sync by sending a `PUT` request to your app's Inngest endpoint (`/api/inngest` by default). Since Pytest automatically discovers and runs `conftest.py` files, simply running your `pytest` command will start the Dev Server before running tests and stop the Dev Server after running tests. # Python SDK Source: https://www.inngest.com/docs/reference/python/index Description: Full reference for the Inngest Python SDK: creating functions, sending events, step methods, middleware, environment variables, and framework integrations. metaTitle = "Inngest Python SDK Reference" ## Installing ```shell pip install inngest ``` ## Quick start guide Read the Python [quick start guide](/docs/getting-started/python-quick-start) to learn how to add Inngest to a FastAPI app and run an Inngest function. ## Source code Our Python SDK is open source and available on Github: [ inngest/inngest-py](https://github.com/inngest/inngest-py). # Python middleware lifecycle Source: https://www.inngest.com/docs/reference/python/middleware/lifecycle Description: Python middleware lifecycle hooks in the Inngest SDK: before_execution, after_execution, before_send_response, and transform_input. metaTitle = "Python Middleware Lifecycle" The order of middleware lifecycle hooks is as follows: 1. [`transform_input`](#transform-input) 2. [`before_memoization`](#before-memoization) 3. [`after_memoization`](#after-memoization) 4. [`before_execution`](#before-execution) 5. [`after_execution`](#after-execution) 6. [`transform_output`](#transform-output) 7. [`before_response`](#before-response) All of these functions may be called multiple times in a single function run. For example, if your function has 2 steps then all of the hooks will run 3 times (once for each step and once for the function). Additionally, there are two hooks when sending events: 1. [`before_send_events`](#before-send-events) 2. [`after_send_events`](#after-send-events) ## Hook reference ### `transform_input` Called when receiving a request from Inngest and before running any functions. Commonly used to mutate data sent by Inngest, like decryption. `ctx` argument passed to Inngest functions. Inngest function object. Memoized step data. ### `before_memoization` Called before checking memoized step data. ### `after_memoization` Called after exhausting memoized step data. ### `before_execution` Called before executing "new code". For example, `before_execution` is called after returning the last memoized step data, since function-level code after that step is "new". ### `after_execution` Called after executing "new code". ### `transform_output` Called after a step or function returns. Commonly used to mutate data before sending it back to Inngest, like encryption. Only set if there's an error. Step or function output. Since `None` is a valid output, always call the `has_output` method before accessing the output. Step or function output. Since `None` is a valid output, always call the `has_output` method before accessing the output. Step ID. Step type enum. Useful in very rare cases. Step options. Useful in very rare cases. ### `before_response` Called before sending a response back to Inngest. ### `before_send_events` Called before sending events to Inngest. Events to send. ### `after_send_events` Called after sending events to Inngest. Error string if an error occurred. Event IDs. # Middleware Source: https://www.inngest.com/docs/reference/python/middleware/overview Description: Add middleware to Inngest Python functions to inject dependencies, log requests, transform data, or integrate with observability tools across all function runs. metaTitle = "Middleware Overview | Inngest Python SDK" Middleware allows you to run code at various points in an Inngest function's lifecycle. This is useful for adding custom behavior to your functions, like error reporting and end-to-end encryption. ```py class MyMiddleware(inngest.Middleware): async def before_send_events( self, events: list[inngest.Event]) -> None: print(f"Sending {len(events)} events") async def after_send_events(self, result: inngest.SendEventsResult) -> None: print("Done sending events") inngest_client = inngest.Inngest( app_id="my_app", middleware=[MyMiddleware], ) ``` ## Examples - [End-to-end encryption](https://github.com/inngest/inngest-py/tree/main/pkg/inngest_encryption) - [Sentry](https://github.com/inngest/inngest-py/blob/main/pkg/inngest/inngest/experimental/sentry_middleware.py) # Python SDK migration guide: v0.3 to v0.4 Source: https://www.inngest.com/docs/reference/python/migrations/v0.3-to-v0.4 Description: Upgrade guide for Inngest Python SDK v0.3 → v0.4. Covers breaking API changes and updated patterns for function creation, event sending, and client setup. metaTitle = "Python SDK Migration: v0.3 → v0.4" This guide will help you migrate your Inngest Python SDK from v0.3 to v0.4 by providing a summary of the breaking changes. ## Middleware ### Constructor Added the `raw_request` arg to the constructor. This is the raw HTTP request received by the `serve` function. Its usecase is predominately for platforms that include critical information in the request, like environment variables in Cloudflare Workers. ### `transform_input` Added the `steps` arg, which was previous in `ctx._steps`. This is useful in encryption middleware. Added the `function` arg, which is the `inngest.Function` object. This is useful for middleware that needs to know the function's metadata (like error reporting). Its return type is now `None` since modifying data should happen by mutating args. ### `transform_output` Replaced the `output` arg with `result` arg. Its type is the new `inngest.TransformOutputResult` class: ```py !snippet:path=snippets/py/v0_4/migration_to/transform_output.py ``` Its return type is now `None` since modifying data should happen by mutating args. ## Removed exports - `inngest.FunctionID` -- No use case. - `inngest.Output` -- Replaced by `inngest.TransformOutputResult`. ## Removed `async_mode` arg in `inngest.django.serve` This argument is no longer needed since async mode is inferred based on the Inngest functions you declare. If you have one or more `async` Inngest functions then async mode is enabled. ## `NonRetriableError` Removed the `cause` arg since it wasn't actually used. We'll eventually reintroduce it in a proper way. # Python SDK migration guide: v0.4 to v0.5 Source: https://www.inngest.com/docs/reference/python/migrations/v0.4-to-v0.5 Description: Migration guide for upgrading the Inngest Python SDK from v0.4 to v0.5. Covers breaking changes in step API, middleware, and client initialization. metaTitle = "Python SDK Migration: v0.4 → v0.5" This guide will help you migrate your Inngest Python SDK from v0.4 to v0.5. ## New features - First-class Pydantic support in step and function output ([docs](/docs/reference/python/guides/pydantic)) - Python 3.13 support - Function singletons ([docs](/docs/guides/singleton)) - Function timeouts ([docs](/docs/features/inngest-functions/cancellation/cancel-on-timeouts)) - Experimental step.infer ([docs](/docs/features/inngest-functions/steps-workflows/step-ai-orchestration)) - Improved parallel step performance ## Breaking changes ### Move `step` into `ctx` The `step` object will be moved to `ctx.step`. Before: ```py !snippet:path=snippets/py/v0_4/migration_from/step.py ``` After: ```py !snippet:path=snippets/py/v0_5/migration_to/step.py ``` ### Parallel steps `step.parallel` will be removed in favor of a new `ctx.group.parallel` method. This method will behave the same way, so it's a drop-in replacement for `step.parallel`. ```py !snippet:path=snippets/py/v0_5/migration_to/parallel.py ``` ### Remove `event.user` We're sunsetting `event.user`. It's already incompatible with some features (e.g. function run replay). ### Disallow mixed async-ness within Inngest functions Setting an async `on_failure` on a non-async Inngest function will throw an error: ```py !snippet:path=snippets/py/v0_5/migration_to/mixed_async_sync_fn.py ``` Setting a non-async `on_failure` on an async Inngest function will throw an error: ```py !snippet:path=snippets/py/v0_5/migration_to/mixed_async_async_fn.py ``` ### Static error when passing a non-async callback to an async `step.run` When passing a non-async callback to an async `step.run`, it will work at runtime but there will be a static type error. ```py !snippet:path=snippets/py/v0_5/migration_to/non_async_step_run_callback.py ``` ### `inngest.Function` is generic The `inngest.Function` class is now a generic that represents the return type. So if an Inngest function returns `str` then it would be `inngest.Function[str]`. ### Middleware order Use LIFO for the "after" hooks. In other words, when multiple middleware is specified then the "after" hooks are run in reverse order. For example, let's say the following middleware is defined and used: ```py !snippet:path=snippets/py/v0_5/migration_to/middleware_order.py ``` The middleware will be executed in the following order for each hook: - `before_execution` -- `A` then `B`. - `after_execution` -- `B` then `A`. The "before" hooks are: ``` before_execution before_response before_send_events transform_input ``` The "after" hooks are: ``` after_execution after_send_events transform_output ``` ### Remove middleware hooks - `before_memoization` - `after_memoization` ### Remove experimental stuff - `inngest.experimental.encryption_middleware` (it's now the [inngest-encryption](https://pypi.org/project/inngest-encryption/) package). - `experimental_execution` option on functions. We won't support native `asyncio` methods (e.g. `asyncio.gather`) going forward. ### Dependencies Drop support for Python `3.9`. Bump dependency minimum versions: ``` httpx>=0.26.0 pydantic>=2.11.0 typing-extensions>=4.13.0 ``` Bump peer dependency minimum versions: ``` Django>=5.0 Flask>=3.0.0 fastapi>=0.110.0 tornado>=6.4 ``` # Environment variables Source: https://www.inngest.com/docs/reference/python/overview/env-vars Description: Environment variables used by the Inngest Python SDK: INNGEST_API_KEY, INNGEST_SIGNING_KEY, INNGEST_BASE_URL, and dev mode settings. metaTitle = "Environment Variables | Inngest Python SDK" You can use environment variables to control some configuration. --- ## `INNGEST_API_BASE_URL` Origin for the Inngest API. Your app registers itself with this API. - Defaults to `https://api.inngest.com/`. - Can be overwritten by specifying `api_base_url` when calling a `serve` function. You likely won't need to set this. --- ## `INNGEST_DEV` - Set to `1` to disable [production mode](/docs/reference/python/overview/prod-mode). - Set to a URL if the Dev Server is not hosted at `http://localhost:8288`. For example, you may need to set it to `http://host.docker.internal:8288` when running the Dev Server within a Docker container (learn more in our [Docker guide](/docs/local-development)). Please note that URLs are not supported below version `0.4.6`. --- ## `INNGEST_ENV` Use this to tell Inngest which [branch environment](/docs/platform/environments#branch-environments) you want to send and receive events from. Can be overwritten by manually specifying `env` on the Inngest client. This is detected and set automatically for some platforms, but others will need manual action. See our [configuring branch environments](/docs/platform/environments#configuring-branch-environments) guide to check if you need this. --- ## `INNGEST_EVENT_API_BASE_URL` Origin for the Inngest Event API. The Inngest client sends events to this API. - Defaults to `https://inn.gs/`. - If set, it should be an origin (protocol, host, and optional port). For example, `http://localhost:8288` or `https://my.tunnel.com` are both valid. - Can be overwritten by specifying `base_url` when creating the Inngest client. You likely won't need to set this. But some use cases include: - Forcing a production build of your app to use the Inngest Dev Server instead of Inngest Cloud for local integration testing. You might want `http://localhost:8288` for that. - Using the Dev Server within a Docker container. You might want `http://host.docker.internal:8288` for that. Learn more in our [Docker guide](/docs/local-development). --- ## `INNGEST_EVENT_KEY` The secret key used to send events to Inngest. - Can be overwritten by specifying `event_key` when creating the Inngest client. - Not needed when using the Dev Server. --- ## `INNGEST_SIGNING_KEY` The secret key used to sign requests to and from Inngest, mitigating the risk of man-in-the-middle attacks. - Can be overwritten by specifying `signing_key` when calling a `serve` function. - Not needed when using the Dev Server. --- ## `INNGEST_SIGNING_KEY_FALLBACK` Only used during signing key rotation. When it's specified, the SDK will automatically retry signing key auth failures with the fallback key. Available in version `0.3.9` and above. # Production mode Source: https://www.inngest.com/docs/reference/python/overview/prod-mode Description: Configure the Inngest Python SDK for production. Enable signing key verification, set the API base URL, and disable Dev Server fallback for deployed apps. metaTitle = "Production Mode | Inngest Python SDK" When the SDK is in production mode it will try to connect to Inngest Cloud instead of the Inngest Dev Server. Production mode is opt-out for security reasons. ## How to opt-out You'll want to disable production mode whenever you're using the Inngest Dev Server. This is typically during local development and CI. Production mode can be disabled in 2 ways: 1. Set the `INNGEST_DEV` environment variable to `1`. 2. Set the `Inngest`'s `is_production` constructor argument to `false`. Using the `INNGEST_DEV` environment variable is the recommended way to disable production mode. But make sure that it isn't set in production! `Inngest`'s `is_production` constructor argument is useful for disabling production mode based on whatever logic you want. For example, you could control it using the `FLASK_ENV` environment variable: ```py import inngest inngest.Inngest( app_id="my_flask_app", is_production=os.environ.get("FLASK_ENV") == "production", ) ``` # Invoke Source: https://www.inngest.com/docs/reference/python/steps/invoke Description: Invoke another Inngest function from within a Python step and wait for its return value. Reference for the invoke() method, parameters, and return type. metaTitle = "step.invoke() | Python SDK Reference" Calls another Inngest function, waits for its completion, and returns its output. ## Arguments Step ID. Should be unique within the function. Invoked function. JSON-serializable data that will be passed to the invoked function as `event.data`. JSON-serializable data that will be passed to the invoked function as `event.user`. ## Examples ```py @inngest_client.create_function( fn_id="fn-1", trigger=inngest.TriggerEvent(event="app/fn-1"), ) async def fn_1(ctx: inngest.Context) -> None: return "Hello!" @inngest_client.create_function( fn_id="fn-2", trigger=inngest.TriggerEvent(event="app/fn-2"), ) async def fn_2(ctx: inngest.Context) -> None: output = await ctx.step.invoke( "invoke", function=fn_1, ) # Prints "Hello!" print(output) ``` 💡 `step.invoke` works within a single app or across apps, since the app ID is built into the function object. # Invoke by ID Source: https://www.inngest.com/docs/reference/python/steps/invoke_by_id Description: Invoke an Inngest function by its string ID from within a Python step. Useful when the target function is defined in a different codebase or SDK version. metaTitle = "step.invoke_by_id() | Python SDK Reference" Calls another Inngest function, waits for its completion, and returns its output. This method behaves identically to the [invoke](/docs/reference/python/steps/invoke) step method, but accepts an ID instead of the function object. This can be useful for a few reasons: - Trigger a function whose code is in a different codebase. - Avoid circular dependencies. - Avoid undesired transitive imports. ## Arguments Step ID. Should be unique within the function. App ID of the invoked function. ID of the invoked function. JSON-serializable data that will be passed to the invoked function as `event.data`. JSON-serializable data that will be passed to the invoked function as `event.user`. ## Examples ### Within the same app ```py @inngest_client.create_function( fn_id="fn-1", trigger=inngest.TriggerEvent(event="app/fn-1"), ) async def fn_1(ctx: inngest.Context) -> str: return "Hello!" @inngest_client.create_function( fn_id="fn-2", trigger=inngest.TriggerEvent(event="app/fn-2"), ) async def fn_2(ctx: inngest.Context) -> None: output = ctx.step.invoke_by_id( "invoke", function_id="fn-1", ) # Prints "Hello!" print(output) ``` ### Across apps ```py inngest_client_1 = inngest.Inngest(app_id="app-1") inngest_client_2 = inngest.Inngest(app_id="app-2") @inngest_client_1.create_function( fn_id="fn-1", trigger=inngest.TriggerEvent(event="app/fn-1"), ) async def fn_1(ctx: inngest.Context) -> str: return "Hello!" @inngest_client_2.create_function( fn_id="fn-2", trigger=inngest.TriggerEvent(event="app/fn-2"), ) async def fn_2(ctx: inngest.Context) -> None: output = ctx.step.invoke_by_id( "invoke", app_id="app-1", function_id="fn-1", ) # Prints "Hello!" print(output) ``` # Parallel Source: https://www.inngest.com/docs/reference/python/steps/parallel Description: Execute multiple Inngest steps concurrently in Python using step.parallel(). All steps run at the same time and results are returned as a tuple when complete. metaTitle = "step.parallel() | Run Steps Concurrently (Python SDK)" Run steps in parallel. Returns the parallel steps' result as a tuple. ## Arguments Accepts a tuple of callables. Each callable has no arguments and returns a JSON serializable value. Typically this is just a `lambda` around a `step` method. ## Examples Running two steps in parallel: ```py @inngest_client.create_function( fn_id="my-function", trigger=inngest.TriggerEvent(event="my-event"), ) async def fn(ctx: inngest.Context) -> None: user_id = ctx.event.data["user_id"] (updated_user, sent_email) = await ctx.group.parallel( ( lambda: ctx.step.run("update-user", update_user, user_id), lambda: ctx.step.run("send-email", send_email, user_id), ) ) ``` Dynamically building a tuple of parallel steps: ```py @client.create_function( fn_id="my-function", trigger=inngest.TriggerEvent(event="my-event"), ) async def fn(ctx: inngest.Context) -> None: parallel_steps = tuple[typing.Callable[[], typing.Awaitable[bool]]]() for user_id in ctx.event.data["user_ids"]: parallel_steps += tuple( [ functools.partial( ctx.step.run, f"get-user-{user_id}", functools.partial(update_user, user_id), ) ] ) updated_users = await ctx.group.parallel(parallel_steps) ``` ⚠️ Use `functools.partial` instead of `lambda` when building the tuple in a loop. If `lambda` is used, then the step functions will use the last value of the loop variable. This is due to Python's lack of block scoping. ## Frequently Asked Questions ### Do parallel steps work if I don't use `async` functions? Yes, parallel steps work with both `async` and non-`async` functions. Since our execution model uses a separate HTTP request for each step, threaded HTTP frameworks (for example, Flask) will create a separate thread for each step. ### Can I use `asyncio.gather` instead of `step.parallel`? No, `asyncio.gather` will not work as expected. Inngest's execution model necessitates a control flow interruption when it encounters a `step` method, but currently that does not work with `asyncio.gather`. ### Why does `step.parallel` accept a tuple instead of variadic arguments? To properly type-annotate `step.parallel`, the return types of the callables need to be statically "extracted". Python's type-checkers are better at doing this with tuples than with variadic arguments. Mypy still struggles even with tuples, but Pyright is able to properly infer the `step.parallel` return type. # Run Source: https://www.inngest.com/docs/reference/python/steps/run Description: step.run() in the Inngest Python SDK wraps any logic in a retriable, memoized step visible in the Inngest dashboard trace. metaTitle = "step.run() | Execute a Step (Python SDK Reference)" Turn a normal function into a durable function. Any function passed to `step.run` will be executed in a durable way, including retries and memoization. ## Arguments Step ID. Should be unique within the function. A callable that has no arguments and returns a JSON serializable value. Positional arguments for the handler. This is type-safe since we infer the types from the handler using generics. ## Examples ```py @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn(ctx: inngest.Context) -> None: # Pass a function to step.run await ctx.step.run("my_fn", my_fn) # Args are passed after the function await ctx.step.run("my_fn_with_args", my_fn_with_args, 1, "a") # Kwargs require functools.partial await ctx.step.run( "my_fn_with_args_and_kwargs", functools.partial(my_fn_with_args_and_kwargs, 1, b="a"), ) # Defining functions like this gives you easy access to scoped variables def use_scoped_variable() -> None: print(ctx.event.data["user_id"]) await ctx.step.run("use_scoped_variable", use_scoped_variable) async def my_fn() -> None: pass async def my_fn_with_args(a: int, b: str) -> None: pass async def my_fn_with_args_and_kwargs(a: int, *, b: str) -> None: pass ``` ## Retries Each `step.run()` call has its own independent retry counter. When a step raises an exception, it will be retried according to your function's retry configuration. The retry configuration applies to each individual step, not as a shared pool across all steps in your function. For example, if your function is configured with `retries=4`, each `step.run()` will be retried up to 4 times independently (5 total attempts including the initial attempt). If you have multiple steps in your function, each step gets its own full set of retries. Learn more about [configuring retries](/docs/features/inngest-functions/error-retries/retries). # Send event Source: https://www.inngest.com/docs/reference/python/steps/send-event Description: Reliably send one or more events from inside an Inngest Python function step. Events are sent as part of the step lifecycle and appear in the run trace. metaTitle = "step.send_event() | Send Events from a Step (Python)" 💡️ This guide is for sending events from *inside* an Inngest function. To send events outside an Inngest function, refer to the [client event sending](/docs/reference/python/client/send) guide. Sends 1 or more events to the Inngest server. Returns a list of the event IDs. ## Arguments Step ID. Should be unique within the function. 1 or more events to send. Any data to associate with the event. A unique ID used to idempotently trigger function runs. If duplicate event IDs are seen, only the first event will trigger function runs. The event name. We recommend using lowercase dot notation for names (e.g. `app/user.created`) A timestamp integer representing the time (in milliseconds) at which the event occurred. Defaults to the time the Inngest receives the event. If the `ts` time is in the future, function runs will be scheduled to start at the given time. This has the same effect as sleeping at the start of the function. Note: This does not apply to functions waiting for events. Functions waiting for events will immediately resume, regardless of the timestamp. ## Examples ```py @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn(ctx: inngest.Context) -> list[str]: return await ctx.step.send_event("send", inngest.Event(name="foo")) ``` # Sleep until Source: https://www.inngest.com/docs/reference/python/steps/sleep-until Description: Pause an Inngest Python function until a specific datetime using step.sleep_until(). The function resumes automatically at the target time. metaTitle = "step.sleep_until() | Sleep Until a Datetime (Python)" Sleep until a specific time. Accepts a `datetime.datetime` object. ## Arguments Step ID. Should be unique within the function. Time to sleep until. ## Examples ```py @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn(ctx: inngest.Context) -> None: await ctx.step.sleep_until( "zzz", datetime.datetime.now() + datetime.timedelta(seconds=2), ) ``` # Sleep Source: https://www.inngest.com/docs/reference/python/steps/sleep Description: Pause an Inngest Python function for a duration using step.sleep(). The function resumes automatically without blocking compute or consuming a thread. metaTitle = "step.sleep() | Pause a Python Function (SDK Reference)" Sleep for a period of time. Accepts either a `datetime.timedelta` object or a number of milliseconds. ## Arguments Step ID. Should be unique within the function. How long to sleep. Can be either a number of milliseconds or a `datetime.timedelta` object. ## Examples ```py @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn(ctx: inngest.Context) -> None: await ctx.step.sleep("zzz", datetime.timedelta(seconds=2)) ``` # Wait for event Source: https://www.inngest.com/docs/reference/python/steps/wait-for-event Description: Pause an Inngest Python function and resume when a matching event is received. Set a timeout and correlation expression to match the right event. metaTitle = "step.wait_for_event() | Wait for an Event (Python SDK)" Wait until the Inngest server receives a specific event. If an event is received before the timeout then the event is returned. If the timeout is reached then `None` is returned. ## Arguments Step ID. Should be unique within the function. Name of the event to wait for. Only match events that match this CEL expression. For example, `"event.data.height == async.data.height"` will only match incoming events whose `data.height` matches the `data.height` value for the trigger event. In milliseconds. ## Examples ```py @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn(ctx: inngest.Context) -> None: res = await ctx.step.wait_for_event( "wait", event="app/wait_for_event.fulfill", timeout=datetime.timedelta(seconds=2), ) ``` # Go SDK migration guide: v0.15 to v0.16 Source: https://www.inngest.com/docs/reference/go/migrations/v0.16 Description: Migration guide for upgrading the Inngest Go SDK from v0.15 to v0.16. metaTitle = "Go SDK Migration: v0.15 → v0.16" This guide will help you migrate your Inngest Go SDK from v0.15 to v0.16 by providing a summary of the breaking changes. ## `AllowInBandSync` The `ClientOpts.AllowInBandSync` option was removed. Authed sync requests now always perform the authenticated sync flow. ```go // Before client, err := inngestgo.NewClient(inngestgo.ClientOpts{ AppID: "my-app", AllowInBandSync: inngestgo.Ptr(true), }) // After client, err := inngestgo.NewClient(inngestgo.ClientOpts{ AppID: "my-app", }) ``` The `INNGEST_ALLOW_IN_BAND_SYNC` environment variable was also removed. ## Unauthenticated sync requests Unauthenticated sync requests in cloud mode are now rejected by default. Local development still allows unauthenticated sync requests because the Dev Server does not sign requests. Unauthenticated app syncs do not send app configuration back to the unauthenticated caller. Instead, the SDK makes an outgoing authenticated request to the configured Inngest API. Requiring authentication on the incoming sync request adds defense in depth. If you depend on unauthenticated sync requests in cloud mode, use `ServeOpts.EnableUnauthedSync`: ```go handler := client.ServeWithOpts(inngestgo.ServeOpts{ EnableUnauthedSync: inngestgo.Ptr(true), }) ``` You can also set `INNGEST_ENABLE_UNAUTHED_SYNC=true`. If you require unauthenticated sync requests with Inngest Cloud, we recommend migrating to the [API-based sync flow](/docs/apps/cloud?ref=docs-go-v0-15-to-v0-16-migration#programmatically) instead of relying on unauthenticated requests to your served SDK endpoint. If you're self-hosting an Inngest server then you'll always need to enable unauthenticated syncs, since self-hosted doesn't yet have authenticated syncs. ## Unauthorized responses Unauthorized responses now return a minimal response body: ```json { "message": "Unauthorized" } ``` These responses no longer include diagnostic fields such as `code`, `authentication_succeeded`, `function_count`, `mode`, `sdk_version`, or signing key hashes. Unauthorized and unsupported method responses now only expose `X-Inngest-SDK-Handled: true` from the `x-inngest-*` response headers. # Go SDK migration guide: v0.7 to v0.8 Source: https://www.inngest.com/docs/reference/go/migrations/v0.7-to-v0.8 Description: Step-by-step migration guide for upgrading the Inngest Go SDK from v0.7 to v0.8. Covers breaking changes, renamed types, and updated function configuration. metaTitle = "Go SDK Migration: v0.7 → v0.8" This guide will help you migrate your Inngest Go SDK from v0.7 to v0.8 by providing a summary of the breaking changes. ## High-level A minimal Inngest app looks like this: ```go !snippet:path=snippets/go/v0_8/migration_to/high_level.go ``` ## `Client` The `DefaultClient` was removed. You should now use the `NewClient` function to create a new client: ```go !snippet:path=snippets/go/v0_8/migration_to/client.go ``` `AppID` is now a required field. `NewClient` will return an error if it is not provided. The removal of `DefaultClient` also means that `inngestgo.Send` and `inngestgo.SendMany` are no longer available. You should now use the `Client.Send` and `Client.SendMany` methods. ## `Handler` The `Handler` was removed, along with `DefaultHandler` and the `NewHandler` function. The handler is predominately replaced by the `Client`, with `HandlerOpts` being replaced by `ClientOpts`. ## `CreateFunction` `CreateFunction` now accepts a `Client` argument and returns an error. Calling `CreateFunction` automatically registers the function, obviating the `Handler.Register` method. ```go !snippet:path=snippets/go/v0_8/migration_to/create_function.go ``` `ID` is now a required field. `CreateFunction` will return an error if it is not provided. If you were previously only setting the `Name` field, you can use `inngestgo.Slugify` to generate the same ID we used internally. If `inngestgo.CreateFunction` is called in a different package than `inngestgo.NewClient`, then you must use a side-effect import to include the function: ```go import ( "github.com/inngest/inngestgo" // Side-effect import to include functions declared in a different package. _ "github.com/myorg/myapp/fns" ) func main() { client, err := inngestgo.NewClient(inngestgo.ClientOpts{AppID: "my-app"}) // ... } ``` # Go SDK migration guide: v0.8 to v0.11 Source: https://www.inngest.com/docs/reference/go/migrations/v0.8-to-v0.11 Description: Step-by-step migration guide for upgrading the Inngest Go SDK from v0.8 to v0.11. Covers API changes, new features, and deprecated patterns. metaTitle = "Go SDK Migration: v0.8 → v0.11" This guide will help you migrate your Inngest Go SDK from v0.8 to v0.11 by providing a summary of the breaking changes. ## `Input` The `Input` type now accepts the event data type as a generic parameter. Previously, it accepted the `GenericEvent` type. ```go !snippet:path=snippets/go/v0_11/migration_to/input.go ``` ## `GenericEvent` The `GenericEvent` type no longer accepts the event user type as a generic parameter. ```go !snippet:path=snippets/go/v0_11/migration_to/generic_event.go ``` # REST API Source: https://www.inngest.com/docs/reference/rest-api/index Description: REST API reference for the Inngest platform: create events, fetch run status, cancel runs, manage functions, and integrate Inngest into your backend workflows. metaTitle = "Inngest REST API Reference" You can view our REST API docs at our API reference portal: [https://api-docs.inngest.com](https://api-docs.inngest.com). # `inngest/function.cancelled` {{ className: "not-prose" }} Source: https://www.inngest.com/docs/reference/system-events/inngest-function-cancelled Description: inngest/function.cancelled fires when a function run is cancelled. Use it to run cleanup logic or notify downstream systems. metaTitle = "inngest/function.cancelled | System Event Reference" The `inngest/function.cancelled` event is sent whenever any single function is cancelled in your [Inngest environment](/docs/platform/environments). The event will be sent if the event is cancelled via [`cancelOn` event](/docs/features/inngest-functions/cancellation/cancel-on-events), [function timeouts](/docs/features/inngest-functions/cancellation/cancel-on-timeouts), [REST API](/docs/guides/cancel-running-functions) or [bulk cancellation](/docs/platform/manage/bulk-cancellation). This event can be used to handle cleanup or similar for a single function or handle some sort of tracking function cancellations in some external system like Datadog. You can write a function that uses the `"inngest/function.cancelled"` event with the optional `if` parameter to filter to specifically handle a single function by `function_id`. ## The event payload The `inngest/` event prefix is reserved for system events in each environment. The event payload data. Data about the error payload as returned from the cancelled function. The cancellation error, always `"function cancelled"` The name of the error, defaulting to `"Error"`. The cancelled function's original event payload. The cancelled function's [`id`](/docs/reference/typescript/v4/functions/create#configuration). The cancelled function's [run ID](/docs/reference/typescript/v4/functions/create#run-id). The timestamp integer in milliseconds at which the cancellation occurred. ```json {{ title: "Example payload" }} { "name": "inngest/function.cancelled", "data": { "error": { "error": "function cancelled", "message": "function cancelled", "name": "Error" }, "event": { "data": { "content": "Yost LLC explicabo eos", "transcript": "s3://product-ideas/carber-vac-release.txt", "userId": "bdce1b1b-6e3a-43e6-84c2-2deb559cdde6" }, "id": "01JDJK451Y9KFGE5TTM2FHDEDN", "name": "integrations/export.requested", "ts": 1732558407003, "user": {} }, "events": [ { "data": { "content": "Yost LLC explicabo eos", "transcript": "s3://product-ideas/carber-vac-release.txt", "userId": "bdce1b1b-6e3a-43e6-84c2-2deb559cdde6" }, "id": "01JDJK451Y9KFGE5TTM2FHDEDN", "name": "integrations/export.requested", "ts": 1732558407003 } ], "function_id": "demo-app-export", "run_id": "01JDJKGTGDVV4DTXHY6XYB7BKK" }, "id": "01JDJKH1S5P2YER8PKXPZJ1YZJ", "ts": 1732570023717 } ``` ## Related resources * [Example: Cleanup after function cancellation](/docs/examples/cleanup-after-function-cancellation) # `inngest/function.failed` {{ className: "not-prose" }} Source: https://www.inngest.com/docs/reference/system-events/inngest-function-failed Description: inngest/function.failed fires when a function exhausts all retries. Use it to forward failures to Datadog, PagerDuty, or Slack. metaTitle = "inngest/function.failed | System Event Reference" The `inngest/function.failed` event is sent whenever any single function fails in your [Inngest environment](/docs/platform/environments). This event can be used to track all function failures in a single place, enabling you to send metrics, alerts, or events to [external systems like Datadog or Sentry](/docs/examples/track-failures-in-datadog) for all of your Inngest functions. Our SDKs offer shorthand ["on failure"](#related-resources) handler options that can be used to handle this event for a specific function. ## The event payload The `inngest/` event prefix is reserved for system events in each environment. The event payload data. Data about the error payload as returned from the failed function. The error message when an error is caught. The name of the error, defaulting to "Error" if unspecified. The stack trace of the error, if supported by the language SDK. The failed function's original event payload. The failed function's [`id`](/docs/reference/typescript/v4/functions/create#configuration). The failed function's [run ID](/docs/reference/typescript/v4/functions/create#run-id). The timestamp integer in milliseconds at which the failure occurred. ```json {{ title: "Example payload" }} { "name": "inngest/function.failed", "data": { "error": { "__serialized": true, "error": "invalid status code: 500", "message": "taylor@ok.com is already a list member. Use PUT to insert or update list members.", "name": "Error", "stack": "Error: taylor@ok.com is already a list member. Use PUT to insert or update list members.\n at /var/task/.next/server/pages/api/inngest.js:2430:23\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async InngestFunction.runFn (/var/task/node_modules/.pnpm/inngest@2.6.0_typescript@5.1.6/node_modules/inngest/components/InngestFunction.js:378:32)\n at async InngestCommHandler.runStep (/var/task/node_modules/.pnpm/inngest@2.6.0_typescript@5.1.6/node_modules/inngest/components/InngestCommHandler.js:459:25)\n at async InngestCommHandler.handleAction (/var/task/node_modules/.pnpm/inngest@2.6.0_typescript@5.1.6/node_modules/inngest/components/InngestCommHandler.js:359:33)\n at async ServerTiming.wrap (/var/task/node_modules/.pnpm/inngest@2.6.0_typescript@5.1.6/node_modules/inngest/helpers/ServerTiming.js:69:21)\n at async ServerTiming.wrap (/var/task/node_modules/.pnpm/inngest@2.6.0_typescript@5.1.6/node_modules/inngest/helpers/ServerTiming.js:69:21)" }, "event": { "data": { "billingPlan": "pro" }, "id": "01H0TPSHZTVFF6SFVTR6E25MTC", "name": "user.signup", "ts": 1684523501562, "user": { "external_id": "6463da8211cdbbcb191dd7da" } }, "function_id": "my-gcp-cloud-functions-app-hello-inngest", "run_id": "01H0TPSJ576QY54R6JJ8MEX6JH" }, "id": "01H0TPW7KB4KCR739TG2J3FTHT", "ts": 1684523589227 } ``` ## Related resources * [TypeScript SDK: onFailure handler](/docs/reference/typescript/v4/functions/handling-failures) * [Python SDK: on_failure handler](/docs/reference/python/functions/create#on_failure) * [Example: Track all function failures in Datadog](/docs/examples/track-failures-in-datadog) # Creating workflow actions Source: https://www.inngest.com/docs/reference/workflow-kit/actions Description: Define typed schemas, inputs, and handlers users can combine into their own workflows. metaTitle = "Creating Workflow Actions | Workflow Kit Reference" The [`@inngest/workflow-kit`](https://npmjs.com/package/@inngest/workflow-kit) package provides a [workflow engine](/docs/reference/workflow-kit/engine), enabling you to create workflow actions on the back end. These actions are later provided to the front end so end-users can build their own workflow instance using the [``](/docs/reference/workflow-kit/components-api). Workflow actions are defined as two objects using the [`EngineAction`](#passing-actions-to-the-workflow-engine-engine-action) (for the back-end) and [`PublicEngineAction`](#passing-actions-to-the-react-components-public-engine-action) (for the front-end) types. ```ts {{ title: "src/inngest/actions-definition.ts" }} actionsDefinition: PublicEngineAction[] = [ { kind: "grammar_review", name: "Perform a grammar review", description: "Use OpenAI for grammar fixes", }, ]; ``` ```tsx {{ title: "src/inngest/actions.ts" }} actions: EngineAction[] = [ { // Add a Table of Contents ...actionsDefinition[0], handler: async ({ event, step, workflowAction }) => { // implementation... } }, ]; ``` In the example above, the `actionsDefinition` array would be passed via props to the [``](/docs/reference/workflow-kit/components-api) while the `actions` are passed to the [`Engine`](/docs/reference/workflow-kit/engine). **Why do I need two types of actions?** The actions need to be separated into 2 distinct objects to avoid leaking the action handler implementations and dependencies into the front end: ## Passing actions to the React components: `PublicEngineAction[]` Kind is an enum representing the action's ID. This is not named as "id" so that we can keep consistency with the WorkflowAction type. Name is the human-readable name of the action. Description is a short description of the action. Icon is the name of the icon to use for the action. This may be an URL, or an SVG directly. {/* TODO TODO */} ## Passing actions to the Workflow Engine: `EngineAction[]` **Note**: Inherits `PublicEngineAction` properties. The handler is your code that runs whenever the action occurs. Every function handler receives a single object argument which can be deconstructed. The key arguments are `event` and `step`. ```ts {{ title: "src/inngest/actions.ts" }} actions: EngineAction[] = [ { // Add a Table of Contents ...actionsDefinition[0], handler: async ({ event, step, workflow, workflowAction, state }) => { // ... } }, ]; ``` The details of the `handler()` **unique argument's properties** can be found below: ### `handler()` function argument properties See the Inngest Function handler [`event` argument property definition](/docs/reference/typescript/v4/functions/create#event). See the Inngest Function handler [`step` argument property definition](/docs/reference/typescript/v4/functions/create#step). See the [Workflow instance format](/docs/reference/workflow-kit/workflow-instance). WorkflowAction is the action being executed, with fully interpolated inputs. Key properties are: - `id: string`: The ID of the action within the workflow instance. - `kind: string`: The action kind, as provided in the [`PublicEngineAction`](#passing-actions-to-the-react-components-public-engine-action). - `name?: string`: The name, as provided in the [`PublicEngineAction`](#passing-actions-to-the-react-components-public-engine-action). - `description?: string`: The description, as provided in the [`PublicEngineAction`](#passing-actions-to-the-react-components-public-engine-action). - `inputs?: string`: The record key is the key of the EngineAction input name, and the value is the variable's value. State represents the current state of the workflow, with previous action's outputs recorded as key-value pairs. # Components API (React) Source: https://www.inngest.com/docs/reference/workflow-kit/components-api Description: Build a visual workflow editor UI backed by durable Inngest execution. metaTitle = "React Components API | Workflow Kit Reference" The [`@inngest/workflow-kit`](https://npmjs.com/package/@inngest/workflow-kit) package provides a set of React components, enabling you to build a workflow editor UI in no time! ![workflow-kit-announcement-video-loop.gif](/assets/docs/reference/workflow-kit/workflow-demo.gif) ## Usage ```tsx {{ title: "src/components/my-workflow-editor.ts" }} // import `PublicEngineAction[]` // NOTE - Importing CSS from JavaScript requires a bundler plugin like PostCSS or CSS Modules import "@inngest/workflow-kit/ui/ui.css"; import "@xyflow/react/dist/style.css"; MyWorkflowEditor = ({ workflow }: { workflow: Workflow }) => { useState(workflow); return ( ); }; ``` ## Reference ### `` `` is a [Controlled Component](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components), watching the `workflow={}` to update. Make sure to updated `workflow={}` based on the updates received via `onChange={}`. A [Workflow instance object](/docs/reference/workflow-kit/workflow-instance). An object with a `name: string` property [representing an event name](/docs/reference/typescript/v4/functions/create#trigger). See [the `PublicEngineActionEngineAction[]` reference](/docs/reference/workflow-kit/actions#passing-actions-to-the-react-components-public-engine-action). A callback function, called after each `workflow` changes. The `` component should always get the following tree as children: ```tsx ``` # Using the workflow engine Source: https://www.inngest.com/docs/reference/workflow-kit/engine Description: Execute user-defined workflow instances with durable step execution, conditional logic, and action composition. metaTitle = "Workflow Engine | Workflow Kit Reference" The workflow `Engine` is used to run a given [workflow instance](/docs/reference/workflow-kit/workflow-instance) within an Inngest Function: ```tsx {{ title: "src/inngest/workflow.ts" }} new Engine({ actions: actionsWithHandlers, loader: (event) => { return loadWorkflowInstanceFromEvent(event); }, }); export default inngest.createFunction( { id: "blog-post-workflow", triggers: { event: "blog-post.updated" } }, async ({ event, step }) => { // When `run` is called, // the loader function is called with access to the event await workflowEngine.run({ event, step }); } ); ``` ## Configure See [the `EngineAction[]` reference](/docs/reference/workflow-kit/actions#passing-actions-to-the-workflow-engine-engine-action). An async function receiving the [`event`](/docs/reference/typescript/v4/functions/create#event) as unique argument and returning a valid [`Workflow` instance](/docs/reference/workflow-kit/workflow-instance) object. For selectively adding built-in actions, set this to true and expose the actions you want via the [``](/docs/reference/workflow-kit/components-api) `availableActions` prop. # Workflow Kit Source: https://www.inngest.com/docs/reference/workflow-kit/index Description: Inngest Workflow Kit: build configurable, user-defined workflow engines with durable execution, a React UI, and a flexible action API. metaTitle = "Workflow Kit | Build User-Defined Workflows" Workflow Kit enables you to build [user-defined workflows](/docs/guides/user-defined-workflows) with Inngest by providing a set of workflow actions to the **[Workflow Engine](/docs/reference/workflow-kit/engine)** while using the **[pre-built React components](/docs/reference/workflow-kit/components-api)** to build your Workflow Editor UI. ## Installing ```shell {{ title: "npm" }} npm install @inngest/workflow-kit inngest ``` ```shell {{ title: "pnpm" }} pnpm add @inngest/workflow-kit inngest ``` ```shell {{ title: "yarn" }} yarn add @inngest/workflow-kit inngest ``` **Prerequisites** The Workflow Kit integrates with our [TypeScript SDK](/docs/reference/typescript/intro). To use it, you'll need an application with [Inngest set up](/docs), ready to [serve Inngest functions](/docs/learn/serving-inngest-functions). ## Source code
Our Workflow Kit is open source and available on Github as [**inngest/workflow-kit**](https://github.com/inngest/workflow-kit/).
## Guides and examples Get started with Worflow Kit by exploring our guide or cloning our Next.js template: } iconPlacement="top" > Follow this step-by-step tutorial to learn how to use Workflow Kit to add automations to a CMS Next.js application. } iconPlacement="top" > This Next.js template features AI workflows helping with grammar fixes, generating Table of Contents or Tweets. # Workflow instance Source: https://www.inngest.com/docs/reference/workflow-kit/workflow-instance Description: Inngest Workflow Kit workflow instance format: the JSON structure defining user-configured workflow graphs and action chains. metaTitle = "Workflow Instance Format | Workflow Kit Reference" A workflow instance represents a user configuration of a sequence of [workflow actions](/docs/reference/workflow-kit/actions), later provided to the [workflow engine](/docs/reference/workflow-kit/engine) for execution. Example of a workflow instance object: ```json { "name": "Generate social posts", "edges": [ { "to": "1", "from": "$source" }, { "to": "2", "from": "1" } ], "actions": [ { "id": "1", "kind": "generate_tweet_posts", "name": "Generate Twitter posts" }, { "id": "2", "kind": "generate_linkedin_posts", "name": "Generate LinkedIn posts" } ] } ``` **How to use the workflow instance object** Workflow instance objects are meant to be retrieved from the [``](/docs/reference/workflow-kit/components-api) Editor, stored in database and loaded into the [Workflow Engine](/docs/reference/workflow-kit/engine) using a loader. Use this reference if you need to update the workflow instance between these steps. ## `Workflow` A Workflow instance in an object with the following properties: Name of the worklow configuration, provided by the end-user. description of the worklow configuration, provided by the end-user. See the [`WorkflowAction`](#workflow-action) reference below. See the [`WorkflowEdge`](#workflow-edge) reference below. ## `WorkflowAction` `WorkflowAction` represent a step of the workflow instance linked to an defined [`EngineAction`](/docs/reference/workflow-kit/actions). The ID of the action within the workflow instance. This is used as a reference and must be unique within the Instance itself. The action kind, used to look up the `EngineAction` definition. Name is the human-readable name of the action. Description is a short description of the action. Inputs is a list of configured inputs for the EngineAction. The record key is the key of the EngineAction input name, and the value is the variable's value. This will be type checked to match the EngineAction type before save and before execution. Ref inputs for interpolation are `"!ref($.)"`, eg. `"!ref($.event.data.email)"` ## `WorkflowEdge` A `WorkflowEdge` represents the link between two `WorkflowAction`. The `WorkflowAction.id` of the source action. `"$source"` is a reserved value used as the starting point of the worklow instance. The `WorkflowAction.id` of the next action. {/* `WorkflowAction.id` of the next action. */}