# Syncing an Inngest App Source: https://www.inngest.com/docs/apps/cloud Description: Learn how to sync Inngest Apps after a 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; - With a curl command. ### 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) ### Curl command Use the curl command to sync from your machine or automate the process within a CI/CD pipeline. Send a PUT request to your application's serve endpoint using the following command: ```shell curl -X PUT https://.com/api/inngest ``` 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/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 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.Context, step: inngest.StepSync, ) -> 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, step: inngest.Step, ) -> 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() { h := inngestgo.NewHandler("core", inngestgo.HandlerOpts{}) f := inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "account-created", Name: "Account creation flow", }, // Run on every api/account.created event. inngestgo.EventTrigger("api/account.created", nil), AccountCreated, ) h.Register(f) http.ListenAndServe(":8080", h) } ``` 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) # Cloudflare Pages Source: https://www.inngest.com/docs/deploy/cloudflare 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/functions) 2. [Serve your functions](/docs/learn/serving-inngest-functions#framework-cloudflare) 3. [Set environment variables](https://developers.cloudflare.com/pages/get-started/#environment-variables) for your deployment - `NODE_VERSION: 16` - `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) ## 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). # Netlify Source: https://www.inngest.com/docs/deploy/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 [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/dev-server). ## 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 with Inngest 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 or from your API. ### Automatically sync from your CI/CD Automatically sync your app with Inngest using the [Render Deploy Action](https://github.com/marketplace/actions/render-deploy-action), combined with a ["curl command"](/docs/apps/cloud#curl-command): ```yaml # .github/workflows/deploy.yaml name: My Deploy on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - name: Deploy to production uses: johnbeynon/render-deploy-action@v0.0.8 with: service-id: ${{ secrets.MY_RENDER_SERVICE_ID }} api-key: ${{ secrets.MY_RENDER_API_KEY }} wait-for-success: true sync_inngest: runs-on: ubuntu-latest needs: build steps: - name: Register application to Inngest - run: | curl -X PUT ${{ secrets.APP_URL }}/api/inngest ``` _The above GitHub Action requires the `MY_RENDER_API_KEY`, `MY_RENDER_SERVICE_ID` and `APP_URL` to be configured on your repository._ ### Automatically sync from your app You app can self-register as part of its startup flow **if it matches the following requirements**: - Your application should run as a long-lived server instance (not serverless) - Your application should be deployed as a single node (not with auto scaled replicas) The following Express.js code snippet showcases how to achieve self-register: ```tsx {{ title: "index.ts (Express.js)" }} // your express `app` definition stands here... app.listen(PORT, async () => { console.log(`✅ Server started on localhost:${PORT} ➡️ Inngest running at http://localhost:${PORT}/api/inngest`); // Attempt to self-register the app after deploy if (process.env.RENDER_EXTERNAL_URL) { console.log( `Attempting self-register. Functions: `, functions.map((f) => f.name).join(', ') ); new URL('/api/inngest', process.env.RENDER_EXTERNAL_URL); await fetch(inngestURL, { method: 'PUT', }); await sleep(2000); try { await result.json(); console.log( `Register attempted:`, inngestURL.toString(), result.status, json ); } catch (err) { console.log( `Register failed:`, inngestURL.toString(), result.status, result.body ); } } }); function sleep(t: number): Promise { return new Promise((res) => { return setTimeout(res, t); }); } ``` _[The full code is available on GitHub](https://github.com/inngest/inngest-demo-app/blob/e95247d3e3277ecd57bd9a8bb1478c36b3ee09b2/index.ts)_ ## 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 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 export default serve({ client: client, functions: [ firstFunction, anotherFunction ] }); ``` ```ts { GET, POST, PUT } = serve({ client: client, functions: [ firstFunction, anotherFunction ] }); ``` ## Deploying to Vercel Installing [Inngest's official Vercel integration](https://vercel.com/integrations/inngest) 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/security/deployment-protection#protection-bypass-for-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) ## 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. ## 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). # Inngest Dev Server Source: https://www.inngest.com/docs/dev-server The Inngest dev server is an [open source](https://github.com/inngest/inngest) environment that: 1. Runs a fast, in-memory version of Inngest on your machine 2. Provides a browser interface for sending events and viewing events and function runs ![Dev Server Demo](/assets/docs/local-development/dev-server-demo-2025-01-15.gif) You can start the dev server with a single command. The dev server will attempt to find an Inngest `serve` API endpoint by scanning ports and endpoints that are commonly used for this purpose (See "[Auto-discovery](#auto-discovery)"). Alternatively, you can specify the URL of the `serve` endpoint: ```shell {{ title: "npx (npm)" }} npx inngest-cli@latest dev # You can specify the URL of your development `serve` API endpoint npx inngest-cli@latest dev -u http://localhost:3000/api/inngest ``` ```shell {{ title: "Docker" }} docker run -p 8288:8288 inngest/inngest \ inngest dev -u http://host.docker.internal:3000/api/inngest ``` You can now open the dev server's browser interface on [`http://localhost:8288`](http://localhost:8288). For more information about developing with Docker, see the [Docker guide](/docs/guides/development-with-docker). ### Connecting apps to the Dev Server There are two ways to connect apps to the Dev Server: 1. **Automatically**: The Dev Server will attempt to "auto-discover" apps running on common ports and endpoints (See "[Auto-discovery](#auto-discovery)"). 2. **Manually**: You scan explicitly add the URL of the app to the Dev Server using one of the following options: - Using the CLI `-u` param (ex. `npx inngest-cli@latest dev -u http://localhost:3000/api/inngest`) - Adding the URL in the Dev Server Apps page. You can edit the URL or delete a manually added app at any point in time - Using the `inngest.json` (or similar) configuration file (See "[Configuration file](#configuration-file)") ![Dev Server demo manually syncing an app](/assets/docs/local-development/dev-server-apps-demo-2025-01-15.gif) The dev server does "auto-discovery" which scans popular ports and endpoints like `/api/inngest` and `/.netlify/functions/inngest`. **If you would like to disable auto-discovery, pass the `--no-discovery` flag to the `dev` command**. Learn more about [this below](#auto-discovery) ### How functions are loaded by the Dev Server The dev server polls your app locally for any new or changed functions. Then as events are sent, the dev server calls your functions directly, just as Inngest would do in production over the public internet. [IMAGE] ## Testing functions ### Invoke via UI From the Functions tab, you can quickly test any function by click the "Invoke" button and providing the data for your payload in the modal that pops up there. This is the easiest way to directly call a specific function: [IMAGE] ### Sending events to the Dev Server There are different ways that you can send events to the dev server when testing locally: 1. Using the Inngest SDK 2. Using the "Test Event" button in the Dev Server's interface 3. Via HTTP request (e.g. curl) #### Using the Inngest SDK When using the Inngest SDK locally, it tries to detect if the dev server is running on your machine. If it's running, the event will be sent there. ```ts {{ title: "Node.js" }} new Inngest({ id: "my-app" }); await inngest.send({ name: "user.avatar.uploaded", data: { url: "https://a-bucket.s3.us-west-2.amazonaws.com/..." }, }); ``` ```python {{ title: "Python" }} from inngest import Inngest inngest_client = inngest.Inngest(app_id="my_app") await inngest_client.send( name="user.avatar.uploaded", data={"url": "https://a-bucket.s3.us-west-2.amazonaws.com/..."}, ) ``` ```go {{ title: "Go" }} package main import "github.com/inngest/inngest-go" func main() { inngestgo.Send(context.Background(), inngestgo.Event{ Name: "user.avatar.uploaded", Data: map[string]any{"url": "https://a-bucket.s3.us-west-2.amazonaws.com/..."}, }) } ``` **Note** - During local development, you can use a dummy value for your [`INNGEST_EVENT_KEY`](/docs/sdk/environment-variables#inngest-event-key?ref=local-development) environment variable. The dev server does not validate keys locally. #### Using the "Test Event" button The dev server's interface also has a "Test Event" button on the top right that enables you to enter any JSON event payload and send it manually. This is useful for testing out different variants of event payloads with your functions. [IMAGE] #### Via HTTP request All events are sent to Inngest using a simple HTTP API with a JSON body. Here is an example of a curl request to the local dev server's `/e/` endpoint running on the default port of `8228` using a dummy event key of `123`: ```shell curl -X POST -v "http://localhost:8288/e/123" \ -d '{ "name": "user.avatar.uploaded", "data": { "url": "https://a-bucket.s3.us-west-2.amazonaws.com/..." } }' ``` 💡 Since you can send events via HTTP, this means you can send events with any programming language or from your favorite testing tools like Postman. ## Configuration file When using lots of configuration options or specifying multiple `-u` flags for a project, you can choose to configure the CLI via `inngest.json` configuration file. The `dev` command will start in your current directory and walk up directories until it finds a file. `yaml`, `yml`, `toml`, or `properties` file formats and extensions are also supported. You can list all options with `dev --help`. Here is an example file specifying two app urls and the `no-discovery` option: ```json {{ title: "inngest.json" }} { "sdk-url": [ "http://localhost:3000/api/inngest", "http://localhost:3030/api/inngest" ], "no-discovery": true } ``` ```yaml {{ title: "inngest.yaml" }} sdk-url: - "http://localhost:3000/api/inngest" - "http://localhost:3030/api/inngest" no-discovery: true ``` ## Inngest SDK debug endpoint The [SDK's `serve` API endpoint](/docs/learn/serving-inngest-functions) will return some diagnostic information for your server configuration when sending a `GET` request. You can do this via `curl` command or by opening the URL in the browser. Here is an example of a curl request to an Inngest app running at `http://localhost:3000/api/inngest`: ```sh $ curl -s http://localhost:3000/api/inngest | jq { "message": "Inngest endpoint configured correctly.", "hasEventKey": false, "hasSigningKey": false, "functionsFound": 1 } ``` ## Auto-discovery The dev server will automatically detect and connect to apps running on common ports and endpoints. You can disable auto-discovery by passing the `--no-discovery` flag to the `dev` command: ```sh npx inngest-cli@latest dev --no-discovery -u http://localhost:3000/api/inngest ``` ```plaintext {{ title: "Common endpoints" }} /api/inngest /x/inngest /.netlify/functions/inngest /.redwood/functions/inngest ``` ```plaintext {{ title: "Common ports" }} 80, 443, // Rails, Express & Next/Nuxt/Nest routes 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, // Django 5000, // Vite/SvelteKit 5173, // Other common ports 8000, 8080, 8081, 8888, // Redwood 8910, 8911, 8912, 8913, 8914, 8915, // Cloudflare Workers 8787, ``` # 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 “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. Click on "Manage" ([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/creating-an-event-key/new-event-key-2023.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 Source: https://www.inngest.com/docs/events/index Description: Learn how to send events with the Inngest SDK, set the Event Key, and send events from other languages via HTTP.'; # Sending events To start, make sure you have [installed the Inngest SDK](/docs/sdk/overview). 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 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). You can send a single event, or [multiple events at once](#sending-multiple-events-at-once). ```go {{ title: "Go" }} package main import "github.com/inngest/inngest-go" func main() { inngestgo.Send(context.Background(), inngestgo.Event{ Name: "storefront/cart.checkout.completed", Data: map[string]any{ "cartId": "ed12c8bde", "itemIds": []string{"9f08sdh84", "sdf098487", "0fnun498n"}, "account": map[string]any{ "id": 123, "email": "test@example.com", }, }, }) } ``` Sending this event, named `storefront/cart.checkout.completed`, to Inngest will do two things: 1. Automatically run any [functions](/docs/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" }, { 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/docs/inngest-api/pswkqb7u3obet-get-an-event). 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, step: inngest.Step, ) -> list[str]: return await 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/docs/inngest-api/pswkqb7u3obet-get-an-event). 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 _, err := inngestgo.SendMany(ctx, []inngestgo.Event{ { Name: "storefront/cart.checkout.completed", Data: data, }, { Name: "storefront/coupon.used", Data: data, }, { Name: "storefront/loyalty.program.joined", Data: data, }, }) ``` ## Using Event IDs Each event sent to Inngest is assigned a unique Event ID. These `ids` are returned from `inngestgo.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/docs/inngest-api/pswkqb7u3obet-get-an-event). You can choose to log or save these Event IDs if you want to look them up later. ```go ids, err := inngestgo.SendMany(ctx, []inngestgo.Event{ { Name: "storefront/cart.checkout.completed", Data: data, }, { Name: "storefront/coupon.used", Data: data, }, { Name: "storefront/loyalty.program.joined", Data: data, }, }) # # ids = [ # "01HQ8PTAESBZPBDS8JTRZZYY3S", # "01HQ8PTFYYKDH1CP3C6PSTBZN5" # ] # ``` ## 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 events from Python or Go applications, use our [Python SDK](/docs/reference/python/client/send) or [Go SDK](https://pkg.go.dev/github.com/inngest/inngestgo#Client). 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); ``` 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" }} package main import "github.com/inngest/inngest-go" func main() { inngestgo.Send(context.Background(), inngestgo.Event{ Name: "storefront/cart.checkout.completed", ID: "cart-checkout-completed-ed12c8bde", Data: map[string]any{ "cartId": "ed12c8bde", "itemIds": []string{"9f08sdh84", "sdf098487", "0fnun498n"}, "account": map[string]any{ "id": 123, "email": "test@example.com", }, }, }) } ``` 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/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) # AI Agents and RAG Source: https://www.inngest.com/docs/examples/ai-agents-and-rag Description: Learn how to use Inngest for AI automated workflows, AI agents, and RAG. Inngest offers tools to support the development of AI-powered applications. Whether you're building AI agents, automating tasks, or orchestrating and managing AI workflows, Inngest provides features that accommodate various needs and requirements, such as concurrency, debouncing, or throttling (see ["Related Concepts"](#related-concepts)). ## Quick Snippet Below is an example of a RAG workflow (from this [example app](https://github.com/inngest/inngest-demo-app/)). This asynchronous Inngest function summarizes content via GPT-4 by following these steps: - Query a vector database for relevant content. - Retrieve a transcript from an S3 file. - Combine the transcript and queried content to generate a summary using GPT-4. - Save the summary to a database and sends a notification to the client. The function uses [Inngest steps](/docs/learn/inngest-steps) to guarantee automatic retries on failure. ```typescript {{ title: "./inngest/functions.ts" }} summarizeContent = inngest.createFunction( { name: 'Summarize content via GPT-4', id: 'summarize-content' }, { event: 'ai/summarize.content' }, async ({ event, step, attempt }) => { await step.run('query-vectordb', async () => { return { matches: [ { id: 'vec3', score: 0, values: [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], text: casual.sentences(3), }, { id: 'vec4', score: 0.0799999237, values: [0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4], text: casual.sentences(3), }, { id: 'vec2', score: 0.0800000429, values: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2], text: casual.sentences(3), }, ], namespace: 'ns1', usage: { readUnits: 6 }, }; }); await step.run('read-s3-file', async () => { return casual.sentences(10); }); // We can globally share throttle limited functions like this using invoke await step.invoke('generate-summary-via-gpt-4', { function: chatCompletion, data: { messages: [ { role: 'system', content: 'You are a helpful assistant that summaries content for product launches.', }, { role: 'user', content: `Question: Summarize my content: \n${transcript}. \nInformation: ${results.matches .map((m) => m.text) .join('. ')}`, }, ], }, }); // You might use the response like this: completion.choices[0].message.content; await step.run('save-to-db', async () => { return casual.uuid; }); await step.run('websocket-push-to-client', async () => { return casual.uuid; }); return { success: true, summaryId: casual.uuid }; } ); ``` ## App examples Here are apps that use Inngest to power AI workflows: ## Resources Check the resources below to learn more about working with AI using Inngest: ## Related concepts - [Concurrency](/docs/guides/concurrency): control the number of steps executing code at any one time. - [Debouncing](/docs/guides/debounce): delay function execution until a series of events are no longer received. - [Prioritization](/docs/guides/priority): dynamically execute some runs ahead or behind others based on any data. - [Rate limiting](/docs/guides/rate-limiting): limit on how many function runs can start within a time period. - [Steps](/docs/reference/functions/step-run): individual tasks within a function that can be executed independently with a guaranteed retrial. - [Throttling](/docs/guides/throttling): specify how many function runs can start within a time period. # Cleanup after function cancellation Source: https://www.inngest.com/docs/examples/cleanup-after-function-cancellation Description: Create a function that executes after a function run has been cancelled via event, REST API, or bulk cancellation. When function runs are cancelled, you may want to perform some sort of post-cancellation code. This example will use the [`inngest/function.cancelled`](/docs/reference/system-events/inngest-function-cancelled) system event. Whether your function run is cancelled via [`cancelOn` event](/docs/features/inngest-functions/cancellation/cancel-on-events), [REST API](/docs/guides/cancel-running-functions) or [bulk cancellation](/docs/platform/manage/bulk-cancellation), this method will work the same. ## Quick snippet Here is an Inngest function and a corresponding function that will be run whenever the original function is cancelled. This uses the function trigger's `if` parameter to filter the `inngest/function.cancelled` event to only be triggered for the original function. ```ts new Inngest({ id: "newsletter-app" }); // This is our "import" function that will get cancelled importAllContacts = inngest.createFunction( { id: "import-all-contacts", cancelOn: [{ event: "contacts/import.cancelled", if: "async.data.importId == event.data.importId" }] }, { event: "contacts/import.requested" }, async ({ event, step }) => { // This is a long running function } ) // This function will be run only when the matching function_id has a run that is cancelled cleanupCancelledImport = inngest.createFunction( { name: "Cleanup cancelled import", id: "cleanup-cancelled-import" }, { event: "inngest/function.cancelled", // The function ID is a hyphenated slug of the App ID w/ the functions" id if: "event.data.function_id == 'newsletter-app-import-all-contacts'" }, async ({ event, step, logger }) => { // This code will execute after your function is cancelled // The event that triggered our original function run is passed nested in our event payload event.data.event; logger.info(`Import was cancelled: ${originalTriggeringEvent.data.importId}`) } ); ``` An example cancellation event payload: ```json { "name": "inngest/function.cancelled", "data": { "error": { "error": "function cancelled", "message": "function cancelled", "name": "Error" }, "event": { "data": { "importId": "bdce1b1b-6e3a-43e6-84c2-2deb559cdde6" }, "id": "01JDJK451Y9KFGE5TTM2FHDEDN", "name": "contacts/import.requested", "ts": 1732558407003, "user": {} }, "events": [ { "data": { "importId": "bdce1b1b-6e3a-43e6-84c2-2deb559cdde6" }, "id": "01JDJK451Y9KFGE5TTM2FHDEDN", "name": "contacts/import.requested", "ts": 1732558407003, "user": {} } ], "function_id": "newsletter-app-import-all-contacts", "run_id": "01JDJKGTGDVV4DTXHY6XYB7BKK" }, "id": "01JDJKH1S5P2YER8PKXPZJ1YZJ", "ts": 1732570023717 } ``` ## More context Check the resources below to learn more about building email sequences with Inngest. # Email Sequence Source: https://www.inngest.com/docs/examples/email-sequence Description: See how to implement an email sequence with Inngest A drip campaign is usually based on your user's behavior. Let's say you want to create the following campaign: - Send every user a welcome email when they join. - If a user received an email: wait a day and then follow-up with pro user tips meant for highly engaged users. - Otherwise: wait for up to three days and then send them the default trial offer, but only if the user hasn't already upgraded their plan in the meantime. This page provides an overview on how to use Inngest to build reliable marketing campaigns, as well as all the related materials to this feature. ## Quick Snippet Below is an example of how such a campaign would look like: ```javascript inngest.createFunction( { id: "signup-drip-campaign" }, { event: "app/signup.completed" }, async ({ event, step }) => { event.data; user "Welcome to ACME"; await step.run("welcome-email", async () => { return await sendEmail( email, welcome,

Welcome to ACME, {user.firstName}

); }); // Wait up to 3 days for the user open the email and click any link in it await step.waitForEvent("wait-for-engagement", { event: "resend/email.clicked", if: `async.data.email_id == ${emailId}`, timeout: "3 days", }); // if the user clicked the email, send them power user tips if (clickEvent) { await step.sleep("delay-power-tips-email", "1 day"); await step.run("send-power-user-tips", async () => { await sendEmail( email, "Supercharge your ACME experience",

Hello {firstName}, here are tips to get the most out of ACME

); }); // wait one more day before sending the trial offer await step.sleep("delay-trial-email", "1 day"); } // check that the user is not already on the pro plan db.users.byEmail(email); if (dbUser.plan !== "pro") { // send them a free trial offer await step.run("trial-offer-email", async () => { await sendEmail( email, "Free ACME Pro trial",

Hello {firstName}, try our Pro features for 30 days for free

); }); } } ); ``` ## Code examples Here are apps which use Inngest to power email campaigns. ## More context Check the resources below to learn more about building email sequences with Inngest. ## How it works With Inngest, you define functions or workflows using its SDK right in your own codebase and serve them through an HTTP endpoint in your application. Inngest uses this endpoint to download the function definitions and to execute them. When a specific event is triggered, Inngest takes care of reliably executing the function (or functions). In case of failure, Inngest will retry until it succeeds or you will see the failure on the Inngest dashboard, which you can debug and then retrigger so no data is lost. ## Related concepts - [Steps](/docs/learn/inngest-steps) - [Fan-out jobs](/docs/guides/fan-out-jobs) - [Delayed functions](/docs/guides/delayed-functions#delaying-jobs) - [Scheduled functions](/docs/guides/scheduled-functions) # Fetch run status and output Source: https://www.inngest.com/docs/examples/fetch-run-status-and-output Description: See how to fetch the run status and output of a function in Inngest. Inngest provides a way to fetch the status and output of a function run using [the REST API](https://api-docs.inngest.com/docs/inngest-api/1j9i5603g5768-introduction). This is useful when: * You want to check the status or output of a given run. * You want to display the status of a function run in your application, for example, in a user dashboard. This page provides a quick example of how to fetch the status and output of a function run using the Inngest API. ## Quick Snippet Here is a basic function that processes a CSV file and returns the number of items processed: ```typescript inngest.createFunction( { id: "process-csv-upload" }, { event: "imports/csv.uploaded" }, async ({ event, step }) => { // CSV processing logic omitted for the sake of the example return { status: "success", processedItems: results.length, failedItems: failures.length, } } ); ``` ### Triggering the function To trigger this function, you will send an event `"imports/csv.uploaded"` using `inngest.send()` with whatever payload data you need. The `inngest.send()` function returns an array of Event IDs that you will use to fetch the status and output of the function run. ```typescript await inngest.send({ name: "imports/csv.uploaded", data: { file: "http://s3.amazonaws.com/acme-uploads/user_0xp3wqz7vumcvajt/JVLO6YWS42IXEIGO.csv", userId: "user_0xp3wqz7vumcvajt", }, }); // ids = ["01HWAVEB858VPPX47Z65GR6P6R"] ``` ### Fetching triggered function status and output Using the REST API, we can use the Event ID to fetch all runs triggered by that event using the [event's runs endpoint](https://api-docs.inngest.com/docs/inngest-api/yoyeen3mu7wj0-list-event-function-runs): ```bash https://api.inngest.com/v1/events/01HWAVEB858VPPX47Z65GR6P6R/runs ``` To query this, we can use a simple `fetch` request using our signing key to authenticate with the API. Here, we'll wrap this in a re-usable function: ```typescript async function getRuns(eventId) { await fetch(`https://api.inngest.com/v1/events/${eventId}/runs`, { headers: { Authorization: `Bearer ${process.env.INNGEST_SIGNING_KEY}`, }, }); await response.json(); return json.data; } ``` We can now use the Event ID to fetch the status and output of the function run. The `getRuns` function will return an array of runs as events can trigger multiple runs via [fan-out](/docs/guides/fan-out-jobs). We'll consider that this event only triggers a single function: ```typescript await getRuns("01HWAVEB858VPPX47Z65GR6P6R"); console.log(runs[0]); /* { run_id: '01HWAVJ8ASQ5C3FXV32JS9DV9Q', run_started_at: '2024-04-25T14:46:45.337Z', function_id: '6219fa64-9f58-41b6-95ec-a45c7172fa1e', function_version: 12, environment_id: '6219fa64-9f58-41b6-95ec-a45c7172fa1e', event_id: '01HWAVEB858VPPX47Z65GR6P6R', status: 'Completed', ended_at: '2024-04-25T14:46:46.896Z', output: { status: "success", processedItems: 98, failedItems: 2, } } */ ``` If we want to trigger the function then immediately await it's output in the same code, we can wrap our `getRuns` to poll until the status is `Completed`: ```typescript async function getRunOutput(eventId) { let runs = await getRuns(eventId); while (runs[0].status !== "Completed") { await new Promise((resolve) => setTimeout(resolve, 1000)); runs = await getRuns(eventId); if (runs[0].status === "Failed" || runs[0].status === "Cancelled") { throw new Error(`Function run ${runs[0].status}`); } } return runs[0]; } ``` ### Putting it all together Brining this all together, we can now trigger the function and await the output: ```typescript await inngest.send({ name: "imports/csv.uploaded", data: { file: "http://s3.amazonaws.com/acme-uploads/user_0xp3wqz7vumcvajt/JVLO6YWS42IXEIGO.csv", userId: "user_0xp3wqz7vumcvajt", }, }); await getRunOutput(ids[0]); console.log(run.output); /* { status: "success", processedItems: 98, failedItems: 2, } */ ``` ## More context Check the resources below to learn more about working with the Inngest REST API. ## Related concepts - [Fan-out jobs](/docs/guides/fan-out-jobs) # Examples Source: https://www.inngest.com/docs/examples/index hidePageSidebar = true; Explore the features built with Inngest: } title={'AI Agents and RAG'} > Use Inngest to build AI agents and RAG. } title={'Email Sequence'} > Build a dynamic drip campaign based on a user's behavior. } title={'Scheduling a one-off function'} > Schedule a function to run at a specific time. } title={'Fetch run status and output'} > Get the result of a run using an Event ID. } title={'Track all function failures in Datadog'} > Send all function failures to Datadog (or similar) for monitoring. ## Middleware Access environment variables and other Cloudflare bindings within Inngest functions when using Workers or Hono. # Cloudflare Workers environment variables and context Source: https://www.inngest.com/docs/examples/middleware/cloudflare-workers-environment-variables Cloudflare Workers does not set environment variables a global object like Node.js does with `process.env`. Workers [binds environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/) to the worker's special `fetch` event handler thought a specific `env` argument. This means accessing environment variables within Inngest function handlers isn't possible without explicitly passing them through from the worker event handler to the Inngest function handler. We can accomplish this by use the [middleware](/docs/features/middleware) feature for Workers or when using [Hono](/docs/learn/serving-inngest-functions#framework-hono). ## Creating middleware You can create middleware which extracts the `env` argument from the Workers `fetch` event handler arguments for either Workers or Hono: 1. Use `onFunctionRun`'s `reqArgs` array to get the `env` object and, optionally, cast a type. 2. Return the `env` object within the special `ctx` object of `transformInput` lifecycle method. ```ts {{ title: "Workers" }} new InngestMiddleware({ name: 'Cloudflare Workers bindings', init({ client, fn }) { return { onFunctionRun({ ctx, fn, steps, reqArgs }) { return { transformInput({ ctx, fn, steps }) { // reqArgs is the array of arguments passed to the Worker's fetch event handler // ex. fetch(request, env, ctx) // We cast the argument to the global Env var that Wrangler generates: reqArgs[1] as Env; return { ctx: { // Return the env object to the function handler's input args env, }, }; }, }; }, }; }, }); // Include the middleware when creating the Inngest client inngest = new Inngest({ id: 'my-workers-app', middleware: [bindings], }); ``` ```ts {{ title: "Hono" }} type Bindings = { MY_VAR: string; DB_URL: string; MY_BUCKET: R2Bucket; }; new InngestMiddleware({ name: 'Hono bindings', init({ client, fn }) { return { onFunctionRun({ ctx, fn, steps, reqArgs }) { return { transformInput({ ctx, fn, steps }) { // reqArgs is the array of arguments passed to a Hono handler // We cast the argument to the correct Hono Context type with our // environment variable bindings reqArgs as [Context<{ Bindings: Bindings }>]; return { ctx: { // Return the context's env object to the function handler's input args env: honoCtx.env, }, }; }, }; }, }; }, }); // Include the middleware when creating the Inngest client inngest = new Inngest({ id: 'my-hono-app', middleware: [bindings], }); ``` Within your functions, you can now access the environment variables via the `env` object argument that you returned in `transformInput` above. Here's an example function: ```ts inngest.createFunction( { id: 'my-fn' }, { event: 'demo/event.sent' }, // The "env" argument returned in transformInput is passed through: async ({ event, step, env }) => { // The env object will be typed as well: console.log(env.MY_VAR); } ); ``` # Scheduling a one-off function Source: https://www.inngest.com/docs/examples/scheduling-one-off-function Description: Schedule a function to run at a specific time in the future. Inngest provides a way to delay a function run to a specific time in the future. This is useful when: * You want to schedule work in the future based on user input. * You want to slightly delay execution of a non-urgent function for a few seconds or minutes. This page provides a quick example of how to delay a function run to a specific time in the future using the [event payload's](/docs/events#event-payload-format) `ts` field. ## Quick Snippet Here is a basic function that sends a reminder to a user at a given email. ```typescript inngest.createFunction( { id: "send-reminder" }, { event: "notifications/reminder.scheduled" }, async ({ event, step }) => { event.data; await emailApi.send({ to: user.email, subject: "Reminder for your upcoming event", body: message, }); return { id } } ); ``` ### Triggering the function with a timestamp To trigger this function, you will send an event `"notifications/reminder.scheduled"` using `inngest.send()` with the necessary data. The `ts` field in the [event payload](/docs/events#event-payload-format) should be set to the Unix timestamp of the time you want the function to run. For example, to schedule a reminder for 5 minutes in the future: ```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, }); ``` ⚠️ Providing a timestamp in the event only applies for starting function runs. Functions waiting for a matching event will immediately resume, regardless of the timestamp. ### Alternatives Depending on your use case, you may want to consider using [scheduled functions (cron jobs)](/docs/guides/scheduled-functions) for scheduling periodic work or use [`step.sleepUntil()`](/docs/reference/functions/step-sleep-until) to add mid-function delays for a layer time. ## More context Check the resources below to learn more about scheduling functions with Inngest. ## Related concepts - [Scheduled functions (cron jobs)](/docs/guides/scheduled-functions) - [`step.sleepUntil()`](/docs/reference/functions/step-sleep-until) # Track all function failures in Datadog Source: https://www.inngest.com/docs/examples/track-failures-in-datadog Description: Create a function that handles all function failures in an Inngest environment and forwards them to Datadog. Your functions may fail from time to time. Inngest provides a way to handle all failed functions in a single place. This can enable you to send metrics, alerts, or events to external systems like Datadog or Sentry for all of your Inngest functions. This page provides an example of tracking all function failures using [Datadog's Events API](https://docs.datadoghq.com/api/latest/events/) to send all failures the Datadog event stream. You could replace Datadog with whatever system you use for monitoring and alerting. ## Quick Snippet Here is a basic function that uses the internal [`"inngest/function.failed"`](/docs/reference/system-events/inngest-function-failed) event. This event is triggered whenever any single function fails in your [Inngest environment](/docs/platform/environments). ```ts client.createConfiguration(); new v1.EventsApi(configuration); export default inngest.createFunction( { name: "Send failures to Datadog", id: "send-failed-function-events-to-datadog" }, { event: "inngest/function.failed" }, async ({ event, step }) => { // This is a normal Inngest function, so we can use steps as we normally do: await step.run("send-event-to-datadog", async () => { event.data.error; // Create the Datadog event body using information about the failed function: { body: { title: "Inngest Function Failed", alert_type: "error", text: `The ${event.data.function_id} function failed with the error: ${error.message}`, tags: [ // Add a tag with the Inngest function id: `inngest_function_id:${event.data.function_id}`, ], }, }; // Send to Datadog: await apiInstance.createEvent(params); // Return the data to Inngest for viewing in function logs: return { message: "Event sent successfully", data }; }); } ); ``` An example failure event payload: ```json { "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 } ``` ## More context Check the resources below to learn more about building email sequences with Inngest. # Frequently Asked Questions (FAQs) Source: https://www.inngest.com/docs/faq Description: Frequently asked questions about Inngest - [How do I run crons only in production?](#how-do-i-run-crons-only-in-production) - [How do I stop functions from running?](#how-do-i-stop-functions-from-running) - [What is the "Finalization" step in my trace?](#what-is-the-finalization-step-in-my-trace) - [Why am I getting “Event key not found" errors in branch environments?](#why-am-i-getting-event-key-not-found-errors-in-branch-environments) - [How do I specify multiple serve paths for a same Vercel application on the Dashboard?](#why-am-i-getting-event-key-not-found-errors-in-branch-environments) - [What's the recommended way to redact data from step outputs?](#what-s-the-recommended-way-to-redact-data-from-step-outputs) - [Why am I getting a `FUNCTION_INVOCATION_TIMEOUT` error?](#why-am-i-getting-a-function-invocation-timeout-error) - [My app's serve endpoint requires authentication. What should I do?](#my-app-s-serve-endpoint-requires-authentication-what-should-i-do) - [Why am I getting a `killed` error when running the Dev Server?](#why-am-i-getting-a-killed-error-when-running-the-dev-server) - [Why am I getting a `NON_DETERMINISTIC_FUNCTION` error?](#why-am-i-getting-a-non-deterministic-function-error) - [Why am I getting an `Illegal invocation` error?](#why-am-i-getting-an-illegal-invocation-error) - [Why is the dev server polling endpoints that don't exist?](#why-is-the-dev-server-polling-endpoints-that-don-t-exist) ## How do I run crons only in production? There are multiple ways to achieve it: 1. Conditionally rendering depending on the environment. ```javascript process.env.NODE_ENV === "production" ? { cron: "* * *" } : { event: "dev/manualXYZ" } ``` 💡 If you render an event instead of a cron in the other environments, you can still trigger your functions manually if needed. 2. [Disable branch environments](/docs/platform/environments#disabling-branch-environments-in-vercel). ## How do I stop functions from running? The best way to ensure a deprecated function doesn't run is to deploy without including it in your [serve handler](/docs/reference/serve). You can temporarily achieved the same result by archiving the function on our dashboard, but note that a new deployment will unarchive the function. ## What is the "Finalization" step in my trace? The "finalization" step in a run's trace represents the execution of the code between your function's last step and the end of the function handler. ```ts {{ x: "10"}} inngest.createFunction( { id: "handle-import" } { event: "integration.connected" } async ({ event, step }) => { await step.run("import-data", async () => { // ... }); // -- Finalization starts ⬇️ -- res.rows.filter((row) => row.created === true) return { message: `Imported ${newRows.length} rows` } // -- Finalization ends ⬆️ -- }, ) ``` ## Why am I getting “Event key not found" errors in branch environments? Branch environments are [automatically archived](/docs/platform/environments#archiving-branch-environments) 3 days after their latest deploy. It's possible to disable the auto archive functionality for each active environment on our [dashboard](https://app.inngest.com/env). ## How do I specify multiple serve paths for a same Vercel application on the dashboard? You can pass multiple paths by adding their path information to each Vercel project in the [Vercel Integration’s settings](https://app.inngest.com/settings/integrations/vercel). ## What's the recommended way to redact data from step outputs? We recommend doing [E2E encryption](/docs/reference/middleware/examples#e2-e-encryption) instead, as it's more secure and plaintext data never leaves your servers. ## Why am I getting a `FUNCTION_INVOCATION_TIMEOUT` error? This is a Vercel error that means your function timed out within Vercel's infrastructure before it was able to respond to Inngest. More information can be found in [Vercel's docs](https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration). If you're unable to sufficiently extend the timeout within Vercel, our [streaming feature](/docs/streaming) can help. ## My app's serve endpoint requires authentication. What should I do? Your app's [serve endpoint](/docs/learn/serving-inngest-functions) needs to be accessible by our servers, so we can trigger your functions. For this reason, we recommend disabling authentication for the serve endpoint. Our servers communicate securely with your app's serve endpoint using your [signing key](/docs/learn/serving-inngest-functions#signing-key). ### Vercel By default, Vercel enables [Deployment Protection](https://vercel.com/docs/security/deployment-protection) for both preview and generated production URLs. This means that your app's serve endpoint will be unreachable by our servers unless you [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](/docs/deploy/vercel#bypassing-deployment-protection). ## Why am I getting a `killed` error when running the Dev Server? The Inngest CLI binary may become corrupted, particularly during updates while being downloaded. Symptoms can also include the CLI giving no output or a `Segmentation fault`. Clear your npx cache by running `rm -rf ~/.npm/_npx`, or the cache of whichever package manager you're using to run the Dev Server (for example `pnpm prune`, `yarn cache clean`). If the error still persists, please reach out to us on [our Discord](https://www.inngest.com/discord). ## Why am I getting a `NON_DETERMINISTIC_FUNCTION` error? This is an error present in v2.x.x of the TypeScript SDK that can be thrown when a deployment changes a function in the middle of a run. If you're seeing this error, we encourage you to upgrade to v3.x.x of the TypeScript SDK, which will recover and continue gracefully from this circumstance. For more information, see the [Upgrading from v2 to v3](/docs/sdk/migration) migration guide. ## Why am I getting an `Illegal invocation` error? When making requests to an Inngest Server, the TypeScript SDK uses [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). The actual implementation of this varies across different runtimes, versions, and environments. The SDK tries to account for these differences internally, but sometimes providing a custom `fetch` function is necessary or wanted. This error is usually indicative of providing a custom `fetch` function to either a `new Inngest()` or `serve()` call, but not carrying over its [binding](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind). This is a common JavaScript gotcha, where bound methods lose their binding when passed into an object. To resolve, make sure that you rebind the `fetch` function as it is passed. This is commonly bound to `globalThis`, though your specific runtime/version/environment may vary. ```ts new Inngest({ fetch: fetch.bind(globalThis), }); ``` ## Why is the dev server polling endpoints that don't exist? The dev server will automatically detect and connect to apps running on common ports and endpoints. These endpoints include `/api/inngest`, `/x/inngest`, `/.netlify/functions/inngest`, `/.redwood/functions/inngest`. You can disable auto-discovery by passing the `--no-discovery` flag to the `dev` command: ```sh npx inngest-cli@latest dev --no-discovery ``` Learn more about this in the [dev server](/docs/dev-server#auto-discovery) docs. # Event payload format Source: https://www.inngest.com/docs/features/events-triggers/event-format # The event payload is a JSON object that must contain a `name` and `data` property. ### Required properties * The `name` is the type or category of event. Event `name`s are used to [trigger functions](/docs/functions). For example, `app/user.created` or `billing/invoice.paid`. See [tips for event naming](#tips-for-event-naming) below. * `data` contains any data you want to associate with the event. This data will be serialized to JSON. For example, if you're sending an event for a paid invoice, you might include the invoice's `id`, the `amount`, and the `customerId` in the `data` property. The `data` property can contain any nested JSON object, including objects and arrays. ### Optional properties * `user` contains any relevant user-identifying data or attributes associated with the event. This data is encrypted at rest. For example, you might include the user's `id`, `email`, and `name` in the `user` property. The `user` property can contain any JSON object, including nested objects and arrays. * `id` is a unique identifier for the event used to prevent duplicate events. Learn more about [deduplication](#deduplication). * `ts` is the timestamp of the event in milliseconds since the Unix epoch. If not provided, the timestamp will be set to the time the event was received by Inngest. * `v` is the event payload version. This is useful to track changes in the event payload shape over time. For example, `"2024-01-14.1"` ```json {{ title: "Basic JSON event example" }} { "name": "billing/invoice.paid", "data": { "customerId": "cus_NffrFeUfNV2Hib", "invoiceId": "in_1J5g2n2eZvKYlo2C0Z1Z2Z3Z", "amount": 1000, "metadata": { "accountId": "acct_1J5g2n2eZvKYlo2C0Z1Z2Z3Z", "accountName": "Acme.ai" } }, "user": { "email": "taylor@example.com" } } ``` ```ts {{ title: "TypeScript Type representation" }} // If you prefer to think in TypeScript types, here's the type representation of the event payload: type EventPayload = { name: string; data: Record; user?: Record; id?: string; ts?: number; v?: string; } ``` ```py {{ title: "Pydantic Type representation" }} 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, ) class InvoicePaidEventData(pydantic.BaseModel): customerId: str invoiceId: str amount: int metadata: dict class InvoicePaidEvent(BaseEvent): data: InvoicePaidEventData name: typing.ClassVar[str] = "billing/invoice.paid" ``` ### Tips for event naming Event names are used to trigger functions. We recommend using a consistent naming convention for your events. This will make it easier to find and trigger functions in the future. Here are some tips for naming events: * **Object-Action**: Use an Object-Action pattern as represented by noun and a verb. This is great for grouping related events on a given object, `account.created`, `account.updated`, `account.deleted`. * **Past-tense**: Use a past-tense verb for the action. For example, `uploaded`, `paid`, `completed`, `sent`. * **Separators**: Use dot-notation and/or underscores to separate words. For example, `user.created` or `blog_post.published`. * **Prefixes**: Use prefixes to group related events. For example, `api/user.created`, `billing/invoice.paid`, `stripe/customer.created`. This is especially useful if you have multiple applications that send events to Inngest. There is no right or wrong way to name events. The most important thing is to be consistent and use a naming convention that makes sense for your application. # Neon Source: https://www.inngest.com/docs/features/events-triggers/neon Inngest allows you to trigger functions from your Neon Postgres database updates. ## Benefits of triggering functions from database events By decoupling function triggers from your application logic, events are initiated by database updates rather than relying on instrumentation in your code to send them. This ensures you won’t miss an event when data is manipulated within your application. This decoupling creates a clean abstraction layer between database operations and code that runs asynchronously. Additionally, as database events are pushed into the Inngest system to enqueue new functions, this can eliminate the need for architecture patterns like the [transactional outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html). ### Leveraging Inngest features with database triggers Beyond the architectural benefit, some specific Inngest features go perfectly with database triggers: - [**Fan-out**](/docs/guides/fan-out-jobs) - Use a single database event to trigger multiple functions to run in parallel. For example, a pg/users.inserted might trigger a welcome email function and a function that starts a trial in Stripe. - [**Batching**](/docs/guides/batching) - Database events can be batched to process many updates more efficiently. For example, many small updates can be aggregated or efficiently perform bulk operations using third party APIs that support it, like Shopify. - [**Flow control**](/docs/guides/flow-control) - Combine database triggers with flow control functionality like throttling, debouncing, or rate limiting for better resource management and efficiency. For example, use throttling for working with third party API rate limits or use debounce for operations that may happen frequently, helping to avoid redundant work. ## How it works Once you connect Neon to Inngest, any changes to data in your database will automatically send new events to your Inngest account. ## Connecting Neon to Inngest Connecting Neon will require some configuration changes on your Postgres database and Neon project. There are three steps to install the Neon integration in Inngest: 1. **Authorization:** by adding your postgres credentials, Inngest can access your database to proceed with the installation 2. **Enable logical replication:** change the `wal_level` configuration to `logical` 3. **Connect the Neon database to Inngest** You will find Neon in the integrations page inside your Inngest dashboard. Click "Connect" to begin the setup process: ### 1. Authorizing Inngest Inngest doesn’t store your credentials. Make sure you don’t refresh the page when completing the steps, otherwise your credentials will be lost. If that’s the case, you will be prompt to authorize Inngest again. Insert your postgres credentials and hit the “Verify” button to start the validation process: ### 2. Enable logical replication You will need to make sure your Neon project has enabled logical replication. Enable logical replication either automatically using the Neon dashboard: Or follow the steps in the [Neon guide](https://neon.tech/docs/guides/logical-replication-postgres-to-neon#enable-logical-replication-in-the-source-neon-project) to locate and edit your postgresql.conf file. Once that’s complete, go back to Inngest to “Verify logical replication is enabled”: ### 3. Connecting There are two ways to connect to the Neon Database: - Automatically - Manually *(coming soon)* Ingest will setup and connect to your Neon Database automatically. It will create a Postgres role for replication, grant schema access to the role, create a replication slot and create a publication. ## Local development *(coming soon)* For information about our plans check our [public roadmap.](https://roadmap.inngest.com/roadmap) # Prisma Pulse: Trigger Functions from database changes Source: https://www.inngest.com/docs/features/events-triggers/prisma-pulse Prisma Pulse integrates with Inngest, transforming database changes into Inngest Events. Connecting Prisma Pulse to your database will trigger a new row in the `users` table, creating a `db/user.created` event that can trigger Inngest Functions (*for example, a user onboarding email sequence*). ## Setup Prisma Pulse Using Prisma Pulse requires some configuration changes on both your Prisma Data Platform account and database. Please refer to the [Prisma Pulse documentation to enable it](https://www.prisma.io/docs/pulse/getting-started#1-enable-pulse-in-the-platform-console). ## Setup and deploying the Inngest Pulse Router Once Prisma Pulse enabled on your account, you'll have to deploy a Inngest Pulse Router, responsible for translating Prisma Pulse events into Inngest events. The following command will generate for you the Inngest Pulse Router in your application: ```bash npx try-prisma -t pulse/inngest-router ``` ### Configuring the watched tables The list of watched tables should be configured in the `src/index.ts` file by updating the following list: ```ts {{ filename: 'src/index.ts' }} // Here configure each prisma model to stream changes from ['notification', 'user']; ``` ### Deploying the Inngest Pulse Router The Inngest Pulse Router is a Node.js program opening a websocket connection with the Prisma Pulse API. The following environment variables are required: - `DATABASE_URL` - [`PULSE_API_KEY`](https://www.prisma.io/docs/pulse/getting-started#14-generate-an-api-key) - [`INNGEST_EVENT_KEY`](/docs/events/creating-an-event-key) - [`INNGEST_SIGNING_KEY`](/docs/platform/signing-keys) Due to its long-lived nature, the Inngest Pulse Router needs to be deployed on a Cloud Provider such as Railway. **Using Prisma Pulse with Serverless** The Inngest Pulse Router can also be deployed on Serverless by leveraging the [Delivery Guarantees](https://www.prisma.io/blog/prisma-pulse-introducing-delivery-guarantees-for-database-change-events) of Pulse events. By passing a `name` to your `prisma.model.stream({ name: "inngest-router" })`, Prisma Pulse will keep track of received events and only send the new ones (since the last connection). We can leverage this mechanism by creating a Inngest Functions trigger by a CRON schedule and running on a Serverless Function. The Inngest Function will run every 15min and collect the first 100 updates before finishing. ## Trigger a Function from a database event Once your Inngest Pulse Router deployed, Inngest events matching your database changes will be triggered. All events follow the following format: `"db/."` with `` part of: - `create` - `update` - `delete` The events sent by the Inngest Pulse Router contains the changed data, as described [in this API Reference](https://www.prisma.io/docs/pulse/api-reference#pulsecreateeventuser). Here is an example of a onboarding workflow followed upon each new account creation: ```ts {{ filename: 'app/inngest/functions.ts' }} handleNewUser = inngest.createFunction( { id: "handle-new-user" }, { event: "db/user.create" }, async ({ event, step }) => { // This object includes the entire record that changed event.data; await step.run("send-welcome-email", async () => { // Send welcome email await sendEmail({ template: "welcome", to: pulseEvent.created.email, }); }); await step.sleep("wait-before-tips", "3d"); await step.run("send-new-user-tips-email", async () => { // Follow up with some helpful tips await sendEmail({ template: "new-user-tips", to: pulseEvent.created.email, }); }); }, ); ``` **Best practice** A Function run performing a database change might trigger a Prisma Pulse event that will run the Function again. We recommend using `"db/*"` events in combination with a `if:` filter to avoid any infinite loop of Function runs triggered. # Events & Triggers Source: https://www.inngest.com/docs/features/events-triggers import { RiTimeLine, RiCloudLine, RiGitForkFill, RiWebhookFill, RiNewspaperLine, } from "@remixicon/react"; Inngest functions are triggered asynchronously by **events** coming from various sources, including: } href={'/docs/events'}> Send an event from your application’s backend with the Inngest SDK. } href={'/docs/guides/scheduled-functions'}> Run an Inngest function periodically with a trigger using cron syntax. } href={'/docs/platform/webhooks'}> Use Inngest as a webhook consumer for any service to trigger functions. } href={'/docs/guides/invoking-functions-directly'}> Directly invoke other functions to compose more powerful functions. You can customize each of these triggers in multiple ways: - **[Filtering event triggers](/docs/guides/writing-expressions)** - Trigger a function for a subset of matching events sent. - **[Delaying execution](/docs/guides/delayed-functions)** - Trigger a function to run at a specific timestamp in the future. - **[Batching events](/docs/guides/batching)** - Process multiple events in a single function for more efficient systems. - **[Multiple triggers](/docs/guides/multiple-triggers)** - Use a single function to handle multiple event types. ## Why events? Using Events to trigger Inngest Functions instead of direct invocations offers a lot of flexibility: - Events can trigger multiple Inngest Functions. - Events can be used to synchronize Inngest Function runs with [cancellation](/docs/features/inngest-functions/cancellation) and [“wait for event” step](/docs/reference/functions/step-wait-for-event). - Events can be leveraged to trigger Functions across multiple applications. - Similar Events can be grouped together for faster processing. Events act as a convenient mapping between your application actions (ex, `user.signup`) and your application's code (ex, `sendWelcomeEmail()` and `importContacts()`): ### Learn more about Events } href={'https://www.inngest.com/blog/accidentally-quadratic-evaluating-trillions-of-event-matches-in-real-time'}> Accidentally Quadratic: Evaluating trillions of event matches in real-time } href={'https://www.inngest.com/blog/nextjs-trpc-inngest'}> Building an Event Driven Video Processing Workflow with Next.js, tRPC, and Inngest # Cancel on Events Source: https://www.inngest.com/docs/features/inngest-functions/cancellation/cancel-on-events Description: Learn how to cancel long running functions with events.' # Cancel on Events As you have learned that you can trigger functions to run using events, you can also cancel active functions by sending an event. For our example, we'll take a reminder app where a user can schedule to be reminded of something in the future at whatever time they want. The user can also delete the reminder if they change their mind and don't want to receive the reminder anymore. Delaying code to run for days or weeks is easy with `step.sleepUntil`, but we need a way to be able to stop the function if the user deletes the reminder while our function is "sleeping." When defining a function, you can also specify the `cancelOn` option which allows you to list one or more events that, when sent to Inngest, will cause the sleep to be terminated and function will be marked as "Canceled." Here is our schedule reminders function that leverages `cancelOn`: ```ts {{ title: "inngest/syncContacts.ts" }} inngest.createFunction( { id: "schedule-reminder", cancelOn: [{ event: "tasks/reminder.deleted", // The event name that cancels this function // Ensure the cancellation event (async) and the triggering event (event)'s reminderId are the same: if: "async.data.reminderId == event.data.reminderId", }], } { event: "tasks/reminder.created" }, async ({ event, step }) => { await step.sleepUntil('sleep-until-remind-at-time', event.data.remindAt); await step.run('send-reminder-push', async ({}) => { await pushNotificationService.push(event.data.userId, event.data.reminderBody) }) } // ... ); ``` Let's break down how this works: 1. Whenever the function is triggered, a cancellation listener is created which waits for an `"tasks/reminder.deleted"` event to be received. 2. The `if` statement tells Inngest that both the triggering event (`"tasks/reminder.created"`) and the cancellation event (`"tasks/reminder.deleted"`) have the same exact value for `data.reminderId` in each event payload. This makes sure that an event does not cancel a different reminder. For more information on writing events, read our guide [on writing expressions](/docs/guides/writing-expressions). Here is an example of these two events which will be matched on the `data.reminderId` field: ```json { "name": "tasks/reminder.created", "data": { "userId": "user_123", "reminderId": "reminder_0987654321", "reminderBody": "Pick up Jane from the airport" } } ``` ```json { "name": "tasks/reminder.deleted", "data": { "userId": "user_123", "reminderId": "reminder_0987654321", } } ``` ## * You can also optionally specify a `timeout` to only enable cancellation for a period of time. * You can configure multiple events to cancel a function, up to five. * You can write a more complex matching statement using the `if` field. Learn more in the full [reference](/docs/reference/typescript/functions/cancel-on). Delaying code to run for days or weeks is easy with `step.sleep_until`, but we need a way to be able to stop the function if the user deletes the reminder while our function is "sleeping." When defining a function, you can also specify the `cancel` option which allows you to list one or more events that, when sent to Inngest, will cause the sleep to be terminated and function will be marked as "Canceled." Here is our schedule reminders function that leverages `cancel`: ```py {{ title: "inngest/schedule_reminder.py" }} @inngest_client.create_function( fn_id="schedule-reminder", trigger=inngest.TriggerEvent(event="tasks/reminder.created"), cancel=[inngest.Cancel( event="tasks/reminder.deleted", # The event name that cancels this function # Ensure the cancellation event (async) and the triggering event (event)'s reminderId are the same: if_exp="async.data.reminderId == event.data.reminderId" )], ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: # Step 1 await step.sleep_until( "sleep-until-remind-at-time", ctx.event.data["remind_at"], ) # Step 2 await step.run("send-reminder-push", send_reminder_push) async def send_reminder_push() -> None: pass ``` Let's break down how this works: 1. Whenever the function is triggered, a cancellation listener is created which waits for an `"tasks/reminder.deleted"` event to be received. 2. The `if` statement tells Inngest that both the triggering event (`"tasks/reminder.created"`) and the cancellation event (`"tasks/reminder.deleted"`) have the same exact value for `data.reminderId` in each event payload. This makes sure that an event does not cancel a different reminder. For more information on writing events, read our guide [on writing expressions](/docs/guides/writing-expressions). Here is an example of these two events which will be matched on the `data.reminderId` field: ```json { "name": "tasks/reminder.created", "data": { "userId": "user_123", "reminderId": "reminder_0987654321", "reminderBody": "Pick up Jane from the airport" } } ``` ```json { "name": "tasks/reminder.deleted", "data": { "userId": "user_123", "reminderId": "reminder_0987654321", } } ``` Delaying code to run for days or weeks is easy with `step.Sleep()`, but we need a way to be able to stop the function if the user deletes the reminder while our function is "sleeping." When defining a function, you can also specify the `Cancel` option which allows you to list one or more events that, when sent to Inngest, will cause the sleep to be terminated and function will be marked as "Canceled." Here is our schedule reminders function that leverages `Cancel`: ```go {{ title: "main.go" }} package main import ( "context" "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) func main() { f := inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "schedule-reminder", Name: "Schedule reminder", Cancel: []inngestgo.Cancel{ { Event: "tasks/reminder.deleted", IfExp: "event.data.id == async.data.id", }, }, }, // Run on every tasks/reminder.created event. inngestgo.EventTrigger("tasks/reminder.created", nil), ScheduleReminder, ) } func ScheduleReminder(ctx context.Context, input inngestgo.Input[ScheduleReminderEvent]) (any, error) { // ... } ``` Let's break down how this works: 1. Whenever the function is triggered, a cancellation listener is created which waits for an `"tasks/reminder.deleted"` event to be received. 2. The `if` statement tells Inngest that both the triggering event (`"tasks/reminder.created"`) and the cancellation event (`"tasks/reminder.deleted"`) have the same exact value for `data.reminderId` in each event payload. This makes sure that an event does not cancel a different reminder. For more information on writing events, read our guide [on writing expressions](/docs/guides/writing-expressions). Here is an example of these two events which will be matched on the `data.reminderId` field: ```json { "name": "tasks/reminder.created", "data": { "userId": "user_123", "reminderId": "reminder_0987654321", "reminderBody": "Pick up Jane from the airport" } } ``` ```json { "name": "tasks/reminder.deleted", "data": { "userId": "user_123", "reminderId": "reminder_0987654321", } } ``` # Cancel on timeouts Source: https://www.inngest.com/docs/features/inngest-functions/cancellation/cancel-on-timeouts Description: Learn how to cancel long running functions with events.' # Cancel on timeouts It's possible to force runs to cancel if they take too long to start, or if the runs execute for too long. The `timeouts` configuration property allows you to automatically cancel functions based off of two timeout properties: - `timeouts.start`, which controls how long a function can stay "queued" before they start - `timeouts.finish`, which controls how long a function can execute once started In the following examples, we'll explore how to configure the timeout property and how this works. ## Runs may stay in the queue waiting to start due to concurrency backlogs, throttling configurations, or other delays. You can automatically cancel these runs if they are queued for too long prior to starting. The `timeouts.start` configuration property controls this timeout. This example forces runs to cancel if it takes over 10 seconds to successfully start the first step of a run: ```ts {{ title: "inngest/function.ts" }} inngest.createFunction( { id: "schedule-reminder", timeouts: { // If the run takes longer than 10s to start, cancel the run. start: "10s", }, } { event: "tasks/reminder.created" }, async ({ event, step }) => { await step.run('send-reminder-push', async () => { await pushNotificationService.push(event.data.reminder) }) } // ... ); ``` ```go {{ title: "inngest/function.go" }} return inngestgo.CreateFunction( inngestgo.FunctionOpts{ Name: "A function", Timeouts: &inngestgo.Timeouts{ // If the run takes longer than 10s to start, cancel the run. Start: inngestgo.Ptr(10*time.Second), }, }, inngestgo.EventTrigger("tasks/reminder.created", nil), func(ctx context.Context, input inngestgo.Input[ReminderEvent]) (any, error) { return step.Run(ctx, "send-reminder", func (ctx context.Context) (bool, error) { // ... return false, nil }) }, ) ``` ### `timeouts.finish` - Adding timeouts to executing runs You may want to limit the overall duration of a run after the run starts executing. You can cancel functions automatically if they're executing for too long. The `timeouts.finish` configuration property controls this timeout. This example forces runs to cancel if it takes over 30 seconds to finish, once started: ```ts {{ title: "inngest/function.ts" }} inngest.createFunction( { id: "schedule-reminder", timeouts: { // If the run takes longer than 10s to start, cancel the run. start: "10s", // And if the run takes longer than 30s to finish after starting, cancel the run. finish: "30s", }, } { event: "tasks/reminder.created" }, async ({ event, step }) => { await step.run('send-reminder-push', async () => { await pushNotificationService.push(event.data.reminder) }) } // ... ); ``` ```go {{ title: "inngest/function.go" }} return inngestgo.CreateFunction( inngestgo.FunctionOpts{ Name: "A function", Timeouts: &inngestgo.Timeouts{ // If the run takes longer than 10s to start, cancel the run. Start: inngestgo.Ptr(10*time.Second), // And if the run takes longer than 30s to finish after starting, cancel the run. Finish: inngestgo.Ptr(30*time.Second), }, }, inngestgo.EventTrigger("tasks/reminder.createad", nil), func(ctx context.Context, input inngestgo.Input[ReminderEvent]) (any, error) { return step.Run(ctx, "send-reminder", func (ctx context.Context) (bool, error) { // ... return false, nil }) }, ) ``` ### Tips * The `timeouts.start` duration limits how long a run waits in the queue for the first step to start * Once the first attempt of a step begins, the `timeouts.start` property no longer applies. Instead, the `timeouts.finish` duration begins. * Once started, the `timeouts.finish` duration limits how long a run can execute * Both properties can be stacked to control the overall length of a function run * Runs that are cancelled due to a timeout trigger an [`inngest/function.cancelled`](/docs/reference/system-events/inngest-function-cancelled) event # Cancellation Source: https://www.inngest.com/docs/features/inngest-functions/cancellation import { RiTerminalBoxLine, RiCloseCircleLine, } from "@remixicon/react"; Cancellation is a useful mechanism for preventing unnecessary actions based on previous actions (ex, skipping a report generation upon an account deletion) or stopping an unwanted function run composed of multiple steps (ex, deployment mistake, duplicates). Inngest enables you to cancel running Functions via the API, Dashboard, or based on events: } href={'/docs/features/inngest-functions/cancellation/cancel-on-events'}> Cancel scheduled or sleeping Functions based on incoming events. } href={'/docs/platform/manage/bulk-cancellation'}> The quickest way to cancel the Function runs within a given time range. } href={'/docs/guides/cancel-running-functions#bulk-cancel-via-the-rest-api'}> Useful to cancel a large number of Function runs within a specific range. } href={'/docs/platform/replay'}> Canceled Functions runs can be replayed from the Platform Dashboard ## Anatomy of a cancellation Inngest cancellation mechanisms prevent a scheduled Function run from running or stop an ongoing Function run between some following steps (sleep or action steps). Please note that: - Cancelling a function that has a currently executing step will not stop the step's execution. Any actively executing steps will run to completion. - Canceling a set of Function runs does not prevent new Function runs from being enqueued (ex, in case of loop issues). Consider using [Functions Pausing](/docs/guides/pause-functions) instead. Consider the below Inngest Function: ```ts {{ title: "inngest/scheduleReminder.ts" }} inngest.createFunction( { id: "schedule-reminder", cancelOn: [{ event: "tasks/deleted", if: "event.data.id == async.data.id" }], } { event: "tasks/reminder.created" }, async ({ event, step }) => { // Step 1 await step.sleepUntil('sleep-until-remind-at-time', event.data.remindAt); // Step 2 await step.run('send-reminder-push', async ({}) => { await pushNotificationService.push(event.data.userId, event.data.reminderBody) }) } // ... ); ``` ```py {{ title: "inngest/schedule_reminder.py" }} @inngest_client.create_function( fn_id="schedule-reminder", trigger=inngest.TriggerEvent(event="tasks/reminder.created"), cancel=[inngest.Cancel( event="tasks/deleted", if_exp="event.data.id == async.data.id" )], ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: # Step 1 await step.sleep_until( "sleep-until-remind-at-time", ctx.event.data["remind_at"], ) # Step 2 await step.run("send-reminder-push", send_reminder_push) async def send_reminder_push() -> None: pass ``` ```go {{ title: "main.go" }} package main import ( "context" "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) func main() { f := inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "schedule-reminder", Name: "Schedule reminder", Cancel: []inngestgo.Cancel{ { Event: "tasks/deleted", IfExp: "event.data.id == async.data.id", }, }, }, // Run on every tasks/reminder.created event. inngestgo.EventTrigger("tasks/reminder.created", nil), ScheduleReminder, ) } func ScheduleReminder(ctx context.Context, input inngestgo.Input[ScheduleReminderEvent]) (any, error) { // ... } ``` Let's now look at two different cancellations triggered from the Dashboard Bulk Cancellation UI: **We cancel the Function run before or after Step 1** 1. The Function Run gets picked up by Inngest 2. The Step 1 is processed, triggering a sleep until the following week 3. Three days after, a cancellation is received 4. The Function run is canceled (Step 2 is skipped) **We cancel the Function run when Step 2 is running** 1. The Function Run gets picked up by Inngest 2. The Step 1 is processed, triggering a sleep until a next week 3. A week after, a cancellation is received but the Step 2 is already started 4. The Step 2 runs until completion 5. The Function run is marked as "canceled" All canceled Function runs can be replay by using the Platform's [Functions Replay UI](/docs/platform/replay). ## Handling cancelled functions Function runs that are cancelled may require additional work like database cleanup or purging of deletion of temporary resources. This can be done leveraging the [`inngest/function.cancelled`](/docs/reference/system-events/inngest-function-cancelled) system event. See [this complete example](/docs/examples/cleanup-after-function-cancellation) for how to use this event within your system to cleanup after a function run is cancelled. # Failure handlers Source: https://www.inngest.com/docs/features/inngest-functions/error-retries/failure-handlers If your function exhausts all of its retries, it will be marked as "Failed." You can handle this circumstance by either providing an [`onFailure/on_failure`](/docs/reference/functions/handling-failures) handler when defining your function, or by listening for the [`inngest/function.failed`](/docs/reference/system-events/inngest-function-failed) system event. The first approach is function-specific, while the second covers all function failures in a given Inngest environment. # Examples The example below checks if a user's subscription is valid a total of six times. If you can't check the subscription after all retries, you'll unsubscribe the user: ```ts {{ title: "TypeScript" }} /* Option 1: give the inngest function an `onFailure` handler. */ inngest.createFunction( { id: "update-subscription", retries: 5, onFailure: async ({ event, error }) => { // if the subscription check fails after all retries, unsubscribe the user await unsubscribeUser(event.data.userId); }, }, { event: "user/subscription.check" }, async ({ event }) => { /* ... */ }, ); /* Option 2: Listens for the [`inngest/function.failed`](/docs/reference/functions/handling-failures#the-inngest-function-failed-event) system event to catch all failures in the inngest environment*/ inngest.createFunction( { id: "handle-any-fn-failure" }, { event: "inngest/function.failed" }, async ({ event }) => { /* ... */ }, ); ``` ```python {{ title: "Python" }} # Option 1: give the inngest function an [`on_failure`] handler. async def update_subscription_failed(ctx: inngest.Context, step: inngest.Step): # if the subscription check fails after all retries, unsubscribe the user await unsubscribe_user(ctx.data.userId) @inngest_client.create_function( fn_id="update-subscription", retries=5, on_failure=update_subscription_failed, trigger=TriggerEvent(event="user/subscription.check")) async def update_subscription(ctx: Context, step: Step): pass # ... # Option 2: Listens for the [inngest/function.failed](/docs/reference/functions/handling-failures#the-inngest-function-failed-event) # system event to catch all failures in the inngest environment @inngest_client.create_function( fn_id="global_failure_handler", trigger=[ TriggerEvent(event="inngest/function.failed"), #TriggerEvent(event="inngest/function.cancelled") ], ) async def global_failure_handler(ctx: Context, step: Step): pass # handle all failures, e.g. to send to sentry ``` ```go // the Go SDK doesn't have native way to define failure handlers, // but you can define one by create a new function that uses // the "inngest/function.failed" event and an expression: myFailureHandler := inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "account-created-on-failure", Name: "Account creation flow: On Failure", }, inngestgo.EventTrigger( "inngest/function.failed", // The full function_id is a concatenated slug of your app id and the // failing function's "ID" inngestgo.StrPtr("event.data.function_id == 'my-app-account-created'") ), func( ctx context.Context, input inngestgo.Input[inngestgo.GenericEvent[functionFailedEventData, any]], ) (any, error) { // Handle your failure here } ) type functionFailedEventData struct { Error struct { Message string `json:"message"` Name string `json:"name"` } `json:"error"` FunctionID string `json:"function_id"` RunID string `json:"run_id"` } // How to determine the full function_id to use in the expression? // 1. Get the "app id" set via "NewHandler" h := inngestgo.NewHandler("my-app", inngestgo.HandlerOpts{}) // 2. Get the FunctionOpts's ID parameter: f := inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "account-created", Name: "Account creation flow", }, inngestgo.EventTrigger("api/account.created", nil), AccountCreated, ) // 3. Join them with a hyphen: // event.data.function_id == 'my-app-account-created' ``` To handle cancelled function runs, checkout out [this example](/docs/examples/cleanup-after-function-cancellation) that uses the [`inngest/function.cancelled`](/docs/reference/system-events/inngest-function-cancelled) system event. # Inngest Errors Source: https://www.inngest.com/docs/features/inngest-functions/error-retries/inngest-errors hidePageSidebar = true; Inngest automatically handles errors and retries for you. You can use standard errors or use included Inngest errors to control how Inngest handles errors. ## Standard errors All `Error` objects are handled by Inngest and [retried automatically](/docs/features/inngest-functions/error-retries/retries). This includes all standard errors like `TypeError` and custom errors that extend the `Error` class. You can throw errors in the function handler or within a step. ```typescript export default inngest.createFunction( { id: "import-item-data" }, { event: "store/import.requested" }, async ({ event }) => { // throwing a standard error if (!event.itemId) { throw new Error("Item ID is required"); } // throwing an error within a step await step.run('fetch-item', async () => { await fetch(`https://api.ecommerce.com/items/${event.itemId}`); if (response.status === 500) { throw new Error("Failed to fetch item from ecommerce API"); } // ... }); } ); ``` All thrown Errors are handled by Inngest and [retried automatically](/docs/features/inngest-functions/error-retries/retries). This includes all standard errors like `ValueError` and custom errors that extend the `Exception` class. You can throw errors in the function handler or within a step. ```python @client.create_function( fn_id="import-item-data", retries=0, trigger=inngest.TriggerEvent(event="store/import.requested"), ) async def fn_async( ctx: inngest.Context, step: inngest.Step, ) -> None: def foo() -> None: raise ValueError("foo") # a retry will be attempted await step.run("foo", foo) ``` All Errors returned by your Inngest Functions are handled by Inngest and [retried automatically](/docs/features/inngest-functions/error-retries/retries). ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) // Register the function inngestgo.CreateFunction( &inngest.FunctionOptions{ ID: "send-user-email", }, inngest.FunctionTrigger{ Event: "user/created", }, SendUserEmail, ) func SendUserEmail(ctx *inngest.FunctionContext) (any, error) { // Run a step which emails the user. This automatically retries on error. // This returns the fully typed result of the lambda. result, err := step.Run(ctx, "on-user-created", func(ctx context.Context) (bool, error) { // Run any code inside a step. result, err := emails.Send(emails.Opts{}) return result, err }) if err != nil { // This step retried 5 times by default and permanently failed. return nil, err } return nil, nil } ``` ## Prevent any additional retries Use `NonRetriableError` to prevent Inngest from retrying the function _or_ step. This is useful when the type of error is not expected to be resolved by a retry, for example, when the error is caused by an invalid input or when the error is expected to occur again if retried. ```typescript export default inngest.createFunction( { id: "mark-store-imported" }, { event: "store/import.completed" }, async ({ event }) => { try { await database.updateStore( { id: event.data.storeId }, { imported: true } ); return result.ok === true; } catch (err) { // Passing the original error via `cause` enables you to view the error in function logs throw new NonRetriableError("Store not found", { cause: err }); } } ); ``` ### Parameters ```ts new NonRetriableError(message: string, options?: { cause?: Error }): NonRetriableError ``` The error message. The original error that caused the non-retriable error. Use `NonRetriableError` to prevent Inngest from retrying the function _or_ step. This is useful when the type of error is not expected to be resolved by a retry, for example, when the error is caused by an invalid input or when the error is expected to occur again if retried. ```python @client.create_function( fn_id="import-item-data", retries=0, trigger=inngest.TriggerEvent(event="store/import.requested"), ) async def fn_async( ctx: inngest.Context, step: inngest.Step, ) -> None: def step_1() -> None: raise inngest.NonRetriableError("non-retriable-step-error") step.run("step_1", step_1) ``` Use `inngestgo.NoRetryError` to prevent Inngest from retrying the function. This is useful when the type of error is not expected to be resolved by a retry, for example, when the error is caused by an invalid input or when the error is expected to occur again if retried. ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) // Register the function inngestgo.CreateFunction( &inngest.FunctionOptions{ ID: "send-user-email", }, inngest.FunctionTrigger{ Event: "user/created", }, SendUserEmail, ) func SendUserEmail(ctx *inngest.FunctionContext) (any, error) { // Run a step which emails the user. This automatically retries on error. // This returns the fully typed result of the lambda. result, err := step.Run(ctx, "on-user-created", func(ctx context.Context) (bool, error) { // Run any code inside a step. result, err := emails.Send(emails.Opts{}) return result, err }) if err != nil { // This step retried 5 times by default and permanently failed. // we return a NoRetryError to prevent Inngest from retrying the function return nil, inngestgo.NoRetryError(err) } return nil, nil } ``` ## Retry after a specific period of time Use `RetryAfterError` to control when Inngest should retry the function or step. This is useful when you want to delay the next retry attempt for a specific period of time, for example, to more gracefully handle a race condition or backing off after hitting an API rate limit. If `RetryAfterError` is not used, Inngest will use [the default retry backoff policy](https://github.com/inngest/inngest/blob/main/pkg/backoff/backoff.go#L10-L22). ```typescript inngest.createFunction( { id: "send-welcome-sms" }, { event: "app/user.created" }, async ({ event, step }) => { await twilio.messages.create({ to: event.data.user.phoneNumber, body: "Welcome to our service!", }); if (!success && retryAfter) { throw new RetryAfterError("Hit Twilio rate limit", retryAfter); } } ); ``` ### Parameters ```ts new RetryAfterError( message: string, retryAfter: number | string | date, options?: { cause?: Error } ): RetryAfterError ``` The error message. The specified time to delay the next retry attempt. The following formats are accepted: * `number` - The number of **milliseconds** to delay the next retry attempt. * `string` - A time string, parsed by the [ms](https://npm.im/ms) package, such as `"30m"`, `"3 hours"`, or `"2.5d"`. * `date` - A `Date` object. The original error that caused the non-retriable error. Use `RetryAfterError` to control when Inngest should retry the function or step. This is useful when you want to delay the next retry attempt for a specific period of time, for example, to more gracefully handle a race condition or backing off after hitting an API rate limit. If `RetryAfterError` is not used, Inngest will use [the default retry backoff policy](https://github.com/inngest/inngest/blob/main/pkg/backoff/backoff.go#L10-L22). ```python @client.create_function( fn_id="import-item-data", retries=0, trigger=inngest.TriggerEvent(event="store/import.requested"), ) async def fn_async( ctx: inngest.Context, step: inngest.Step, ) -> None: def step_1() -> None: raise inngest.RetryAfterError("rate-limit-hit", 1000) # delay in milliseconds step.run("step_1", step_1) ``` ### Parameters ```python RetryAfterError( message: typing.Optional[str], retry_after: typing.Union[int, datetime.timedelta, datetime.datetime], ) -> None ``` The error message. The specified time to delay the next retry attempt. The following formats are accepted: * `int` - The number of **milliseconds** to delay the next retry attempt. * `datetime.timedelta` - A time delta object, such as `datetime.timedelta(seconds=30)`. * `datetime.datetime` - A `datetime` object. Use `RetryAtError` to control when Inngest should retry the function or step. This is useful when you want to delay the next retry attempt for a specific period of time, for example, to more gracefully handle a race condition or backing off after hitting an API rate limit. If `RetryAtError` is not used, Inngest will use [the default retry backoff policy](https://github.com/inngest/inngest/blob/main/pkg/backoff/backoff.go#L10-L22). ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) // Register the function inngestgo.CreateFunction( &inngest.FunctionOptions{ ID: "send-user-email", }, inngest.FunctionTrigger{ Event: "user/created", }, SendUserEmail, ) func SendUserEmail(ctx *inngest.FunctionContext) (any, error) { // Run a step which emails the user. This automatically retries on error. // This returns the fully typed result of the lambda. result, err := step.Run(ctx, "on-user-created", func(ctx context.Context) (bool, error) { // Run any code inside a step. result, err := emails.Send(emails.Opts{}) return result, err }) if err != nil { // This step retried 5 times by default and permanently failed. // We delay the next retry attempt by 5 hours return nil, inngestgo.RetryAtError(err, time.Now().Add(5*time.Hour)) } return nil, nil } ``` ## Step errors After a step exhausts all of its retries, it will throw a `StepError` which can be caught and handled in the function handler if desired. ```ts {{ title: "try/catch" }} inngest.createFunction( { id: "send-weather-forecast" }, { event: "weather/forecast.requested" }, async ({ event, step }) => { let data; try { data = await step.run('get-public-weather-data', async () => { return await fetch('https://api.weather.com/data'); }); } catch (err) { // err will be an instance of StepError // Handle the error by recovering with a different step data = await step.run('use-backup-weather-api', async () => { return await fetch('https://api.stormwaters.com/data'); }); } // ... } ); ``` ```ts {{ title: "Chaining with .catch()" }} inngest.createFunction( { id: "send-weather-forecast" }, { event: "weather/forecast.requested" }, async ({ event, step }) => { await step .run('get-public-weather-data', async () => { return await fetch('https://api.example.com/data'); }) .catch((err) => { // err will be an instance of StepError // Recover with a chained step return step.run("use-backup-weather-api", () => { return await fetch('https://api.stormwaters.com/data'); }); }); } ); ``` ```ts {{ title: "Ignoring and logging the error" }} inngest.createFunction( { id: "send-weather-forecast" }, { event: "weather/forecast.requested" }, async ({ event, step }) => { await step .run('get-public-weather-data', async () => { return await fetch('https://api.example.com/data'); }) // This will swallow the error and log it if it's non critical .catch((err) => logger.error(err)); } ); ``` Support for handling step errors is available in the Inngest TypeScript SDK starting from version **3.12.0**. Prior to this version, wrapping a step in try/catch will not work correctly. ## Step errors After a step exhausts all of its retries, it will throw a `StepError` which can be caught and handled in the function handler if desired. ```python @client.create_function( fn_id="import-item-data", retries=0, trigger=inngest.TriggerEvent(event="store/import.requested"), ) async def fn_async( ctx: inngest.Context, step: inngest.Step, ) -> None: def foo() -> None: raise ValueError("foo") try: step.run("foo", foo) except inngest.StepError: raise MyError("I am new") ``` ## Attempt counter The current attempt number is passed in as input to the function handler. `attempt` is a zero-index number that increments for each retry. The first attempt will be `0`, the second `1`, and so on. The number is reset after a successfully executed step. ```ts inngest.createFunction( { id: "generate-summary" }, { event: "blog/post.created" }, async ({ attempt }) => { // `attempt` is the zero-index attempt number await step.run('call-llm', async () => { if (attempt < 2) { // Call OpenAI's API two times } else { // After two attempts to OpenAI, try a different LLM, for example, Mistral } }); } ); ``` ## Stack traces When calling functions that return Promises, await the Promise to ensure that the stack trace is preserved. This applies to functions executing in different cycles of the event loop, for example, when calling a database or an external API. This is especially useful when debugging errors in production. ```ts {{ title: "Returning Promise" }} inngest.createFunction( { id: "update-recent-usage" }, { event: "app/update-recent-usage" }, async ({ event, step }) => { // ... await step.run("update in db", () => doSomeWork(event.data)); // ... } ); ``` ```ts {{ title: "Awaiting Promise" }} inngest.createFunction( { id: "update-recent-usage" }, { event: "app/update-recent-usage" }, async ({ event, step }) => { // ... await step.run("update in db", async () => { return await doSomeWork(event.data); }); // ... } ); ``` Please note that immediately returning the Promise will not include a pointer to the calling function in the stack trace. Awaiting the Promise will ensure that the stack trace includes the calling function. # Retries Source: https://www.inngest.com/docs/features/inngest-functions/error-retries/retries By default, in _addition_ to the **initial attempt**, Inngest will retry a function or a step up to 4 times until it succeeds. This means that for a function with a default configuration, it will be attempted 5 times in total. For the function below, if the database write fails then it'll be retried up to 4 times until it succeeds: ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "click-recorder" }, { event: "app/button.clicked" }, async ({ event, attempt }) => { await db.clicks.insertOne(event.data); // this code now retries! }, ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "click-recorder"}, inngestgo.EventTrigger("app/button.clicked", nil), func(ctx context.Context, input inngestgo.Input[ButtonClickedEvent]) (any, error) { result, err := db.Clicks.InsertOne(input.Event["data"]) }, ) ``` ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="click-recorder", trigger=inngest.TriggerEvent(event="app/button.clicked"), ) def record_click(ctx: inngest.Context) -> None: db.clicks.insert_one(ctx.event.data) ``` You can configure the number of `retries` by specifying it in your function configuration. Setting the value to `0` will disable retries. ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "click-recorder", retries: 10, // choose how many retries you'd like }, { event: "app/button.clicked" }, async ({ event, step, attempt }) => { /* ... */ }, ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "click-recorder", Retries: 10, // choose how many retries you'd like }, inngestgo.EventTrigger("app/button.clicked", nil), func(ctx context.Context, input inngestgo.Input[ButtonClickedEvent]) (any, error) { // ... }, ) ``` ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="click-recorder", retries=10, # choose how many retries you'd like trigger=inngest.TriggerEvent(event="app/button.clicked"), ) def click_recorder(ctx: inngest.Context) -> None: # ... ``` You can customize the behavior of your function based on the number of retries using the `attempt` argument. `attempt` is passed in the function handler's context and is zero-indexed, meaning the first attempt is `0`, the second is `1`, and so on. The `attempt` is incremented every time the function throws an error and is retried, and is reset when steps complete. This allows you to handle attempt numbers differently in each step. Retries will be performed with backoff according to [the default schedule](https://github.com/inngest/inngest/blob/main/pkg/backoff/backoff.go#L10-L22). ## Steps and Retries A function can be broken down into multiple steps, where each step is individually executed and retried. Here, both the "_get-data_" and "_save-data_" steps have their own set of retries. If the "_save-data_" step has a failure, it's retried, alone, in a separate request. ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "sync-systems" }, { event: "auto/sync.request" }, async ({ step }) => { // Can be retried up to 4 times await step.run("get-data", async () => { return getDataFromExternalSource(); }); // Can also be retried up to 4 times await step.run("save-data", async () => { return db.syncs.insertOne(data); }); }, ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "sync-systems"}, inngestgo.EventTrigger("auto/sync.request", nil), func(ctx context.Context, input inngestgo.Input[SyncRequestEvent]) (any, error) { // can be retried up to 4 times data, err := step.Run(ctx, "get-data", func(ctx context.Context) (any, error) { return getDataFromExternalSource() }) if err != nil { return nil, err } // can also be retried up to 4 times _, err = step.Run(ctx, "save-data", func(ctx context.Context) (any, error) { return db.Syncs.InsertOne(data.(DataType)) }) if err != nil { return nil, err } return nil, nil }, ) ``` ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="sync-systems", trigger=inngest.TriggerEvent(event="auto/sync.request"), ) def sync_systems(ctx: inngest.Context, step: inngest.StepSync) -> None: # Can be retried up to 4 times data = step.run("Get data", get_data_from_external_source) # Can also be retried up to 4 times step.run("Save data", db.syncs.insert_one, data) ``` You can configure the number of [`retries`](/docs/reference/functions/create#inngest-create-function-configuration-trigger-handler-inngest-function) for each function. This excludes the initial attempt. A retry count of `4` means that each step will be attempted up to 5 times. ## Preventing retries with Non-retriable errors You can throw a [non-retriable error](/docs/reference/typescript/functions/errors#non-retriable-error) from a step or a function, which will bypass any remaining retries and fail the step or function it was thrown from. This is useful for when you know an error is permanent and want to stop all execution. In this example, the user doesn't exist, so there's no need to continue to email them. ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "user-weekly-digest" }, { event: "user/weekly.digest.requested" }, async ({ event, step }) => { await step .run("get-user-email", () => { return db.users.findOne(event.data.userId); }) .catch((err) => { if (err.name === "UserNotFoundError") { throw new NonRetriableError("User no longer exists; stopping"); } throw err; }); await step.run("send-digest", () => { return sendDigest(user.email); }); }, ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "user-weekly-digest"}, inngestgo.EventTrigger("user/weekly.digest.requested", nil), func(ctx context.Context, input inngestgo.Input[WeeklyDigestRequestedEvent]) (any, error) { user, err := step.Run(ctx, "get-user-email", func(ctx context.Context) (any, error) { return db.Users.FindOne(input.Event.Data.UserID) }) if err != nil { if stepErr, ok := err.(step.StepError); ok && stepErr.Name == "UserNotFoundError" { return nil, inngestgo.NoRetryError(fmt.Errorf("User no longer exists; stopping")) } return nil, err } _, err = step.Run(ctx, "send-digest", func(ctx context.Context) (any, error) { return sendDigest(user.(UserType).Email) }) if err != nil { return nil, err } return nil, nil }, ) ``` ```py {{ title: "Python" }} from inngest.errors import NonRetriableError @inngest_client.create_function( fn_id="user-weekly-digest", trigger=inngest.TriggerEvent(event="user/weekly.digest.requested"), ) def user_weekly_digest(ctx: inngest.Context, step: inngest.StepSync) -> None: try: user = step.run("get-user-email", db.users.find_one, ctx.event.data["userId"]) except Exception as err: if err.name == "UserNotFoundError": raise NonRetriableError("User no longer exists; stopping") raise step.run("send-digest", send_digest, user["email"]) ``` ## Customizing retry times Retries are executed with exponential back-off with some jitter, but it's also possible to specify exactly when you'd like a step or function to be retried. In this example, an external API provided `Retry-After` header with information on when requests can be made again, so you can tell Inngest to retry your function then. ```ts inngest.createFunction( { id: "send-welcome-notification" }, { event: "app/user.created" }, async ({ event, step }) => { await step.run('send-message', async () => { await twilio.messages.create({ to: event.data.user.phoneNumber, body: "Welcome to our service!", }); if (!success && retryAfter) { throw new RetryAfterError("Hit Twilio rate limit", retryAfter); } return { message }; }); }, ); ``` ```go inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "send-welcome-notification"}, inngestgo.EventTrigger("user.created", nil), func(ctx context.Context, input inngestgo.Input[SignedUpEvent]) (any, error) { success, retryAfter, err := twilio.Messages.Create(twilio.MessageOpts{ To: input.Event.Data.User.PhoneNumber, Body: "Welcome to our service!", }) if err != nil { return nil, err } if !success && retryAfter != nil { return nil, inngestgo.RetryAtError(fmt.Errorf("Hit Twilio rate limit"), *retryAfter) } return nil, nil } ) ``` ```py {{ title: "Python" }} import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="send-welcome-notification", trigger=inngest.TriggerEvent(event="user.created"), ) def send_welcome_notification(ctx: inngest.Context, step: inngest.StepSync) -> None: success, retryAfter, err = twilio.Messages.Create(twilio.MessageOpts{ To: ctx.event.data["user"]["phoneNumber"], Body: "Welcome to our service!", }) if not success and retryAfter is not None: raise inngest.RetryAfterError("Hit Twilio rate limit", retryAfter) ``` # Rollbacks Source: https://www.inngest.com/docs/features/inngest-functions/error-retries/rollbacks Unlike an error being thrown in the main function's body, a failing step (one that has exhausted all retries) will throw a `StepError`. This allows you to handle failures for each step individually, where you can recover from the error gracefully. If a step failure isn't handled, the error will bubble up to the function itself, which will then be marked as failed. Below is an attempt to use DALL-E to generate an image from a prompt, and to fall back to Midjourney if it fails. Remember that these calls are split over separate requests, making the code much more durable against timeouts, transient errors, and these dependencies on external APIs. ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "generate-result" }, { event: "prompt.created" }, async ({ event, step }) => { // try one AI model, if it fails, try another let imageURL: string | null = null; let via: "dall-e" | "midjourney"; try { imageURL = await step.run("generate-image-dall-e", () => { // open api call to generate image... }); via = "dall-e"; } catch (err) { imageURL = await step.run("generate-image-midjourney", () => { // midjourney call to generate image... }); via = "midjourney"; } await step.run("notify-user", () => { return pusher.trigger(event.data.channelID, "image-result", { imageURL, via, }); }); }, ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "generate-result"}, inngestgo.EventTrigger("prompt.created", nil), func(ctx context.Context, input inngestgo.Input[PromptCreatedEvent]) (any, error) { var ( imageURL string err error ) via := "dall-e" imageURL, err = step.Run(ctx, "generate-image-dall-e", func(ctx context.Context) (string, error) { // Open API call to generate image with Dall-E... }) if err != nil { // Update how we ran the code. This could also have been a return from the step. via = "midjourney" imageURL, err = step.Run(ctx, "generate-image-midjourney", func(ctx context.Context) (string, error) { // MidJourney call to generate image... }) } if err != nil { return nil, err } _, err = step.Run(ctx, "notify-user", func(ctx context.Context) (any, error) { return pusher.Trigger(input.Event.Data.ChannelID, "image-result", map[string]string{ "imageURL": imageURL.(string), "via": via, }) }) if err != nil { return nil, err } return nil, nil }, ) ``` {/* ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="generate-result", trigger=inngest.TriggerEvent(event="prompt.created"), ) def generate_result(ctx: inngest.Context, step: inngest.StepSync) -> None: image_url = None via = None try: image_url = step.run("generate-image-dall-e", lambda: open_api_call_to_generate_image()) via = "dall-e" except Exception as err: image_url = step.run("generate-image-midjourney", lambda: midjourney_call_to_generate_image()) via = "midjourney" def _notify_user() -> None: pusher.trigger(ctx.event.data["channelID"], "image-result", {"imageURL": image_url, "via": via}) step.run("notify-user", _notify_user) ``` */} ### Simple rollbacks With this pattern, it's possible to assign a small rollback for each step, making sure that every action is safe regardless of how many steps are being run. ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "add-data" }, { event: "app/row.data.added" }, async ({ event, step }) => { // ignore the error - this step is fine if it fails await step .run("non-critical-step", () => { return updateMetric(); }) .catch(); // Add a rollback to a step await step .run("create-row", async () => { await createRow(event.data.rowId); await addDetail(event.data.entry); }) .catch((err) => step.run("rollback-row-creation", async () => { await removeRow(event.data.rowId); }), ); }, ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "add-data"}, inngestgo.EventTrigger("app/row.data.added", nil), func(ctx context.Context, input inngestgo.Input[RowDataAddedEvent]) (any, error) { _, _ = step.Run(ctx, "non-critical-step", func(ctx context.Context) (any, error) { return updateMetric() }) _, err := step.Run(ctx, "create-row", func(ctx context.Context) (any, error) { _, err := createRow(input.Event.Data.RowID) if err != nil { return nil, err } return addDetail(input.Event.Data.Entry) }) if err != nil { _, err = step.Run(ctx, "rollback-row-creation", func(ctx context.Context) (any, error) { return removeRow(input.Event.Data.RowID) }) if err != nil { return nil, err } } return nil, nil }, ) ``` {/* ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="add-data", trigger=inngest.TriggerEvent(event="app/row.data.added"), ) def add_data(ctx: inngest.Context, step: inngest.StepSync) -> None: # ignore the error - this step is fine if it fails try: step.run("Non-critical step", lambda: update_metric()) except Exception: pass # Add a rollback to a step try: step.run("Create row", lambda: create_row_and_add_detail(ctx.event.data["rowId"], ctx.event.data["entry"])) except Exception as err: step.run("Rollback row creation", lambda: remove_row(ctx.event.data["rowId"])) def create_row_and_add_detail(row_id, entry): create_row(row_id) add_detail(entry) ``` */} # Sleeps Source: https://www.inngest.com/docs/features/inngest-functions/steps-workflows/sleeps Two step methods, `step.sleep` and `step.sleepUntil`, are available to pause the execution of your function for a specific amount of time. Your function can sleep for seconds, minutes, or days, up to a maximum of one year (seven days for account on our [free tier](/pricing?ref=docs-sleeps)). Using sleep methods can avoid the need to run multiple cron jobs or use additional queues. For example, Sleeps enable you to create a user onboarding workflow that sequences multiple actions in time: first send a welcome email, then send a tutorial each day for a week. ## How Sleeps work `step.sleep` and `step.sleepUntil` tell Inngest to resume execution of your function at a future time. Your code doesn't need to be running during the sleep interval, allowing sleeps to be used in any environment, even serverless platforms. A Function paused by a sleeping Step doesn't affect your account capacity; i.e. it does not count against your plan's concurrency limit. A sleeping Function doesn't count against any [concurrency policy](/docs/guides/concurrency) you've set on the function, either. ## Pausing an execution for a given time Use `step.sleep()` to pause the execution of your function for a specific amount of time. ```ts export default inngest.createFunction( { id: "send-delayed-email" }, { event: "app/user.signup" }, async ({ event, step }) => { await step.sleep("wait-a-couple-of-days", "2d"); // Do something else } ); ``` Check out the [`step.sleep()` TypeScript reference.](/docs/reference/functions/step-sleep) Use `step.sleep()` to pause the execution of your function for a specific amount of time. ```py @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: await step.sleep("zzz", datetime.timedelta(seconds=2)) ``` Check out the [`step.sleep()` Python reference.](/docs/reference/python/steps/sleep) Use `step.Sleep()` to pause the execution of your function for a specific amount of time. ```go func AccountCreated(ctx context.Context, input inngestgo.Input[AccountCreatedEvent]) (any, error) { // Sleep for a second, minute, hour, week across server restarts. step.Sleep(ctx, "initial-delay", time.Second) // ... return nil, nil } ``` Check out the [`step.Sleep()` Go reference.](https://pkg.go.dev/github.com/inngest/inngestgo@v0.9.0/step#Sleep) ## Pausing an execution until a given date Use `step.sleepUntil()` to pause the execution of your function until a specific date time. ```ts export default inngest.createFunction( { id: "send-scheduled-reminder" }, { event: "app/reminder.scheduled" }, async ({ event, step }) => { new Date(event.data.remind_at); await step.sleepUntil("wait-for-scheduled-reminder", date); // Do something else } ); ``` Check out the [`step.sleepUntil()` TypeScript reference.](/docs/reference/functions/step-sleep-until) Use `step.sleep_until()` to pause the execution of your function until a specific date time. ```py @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: await step.sleep_until( "zzz", datetime.datetime.now() + datetime.timedelta(seconds=2), ) ``` Check out the [`step.sleep_until()` Python reference.](/docs/reference/python/steps/sleep-until) _Sleep until a given date is not yet available in the Go SDK._ **Sleeps and trace/log history** You may notice that Inngest Cloud's Function Runs view doesn't show function runs that use sleeps longer than your [Inngest plan's](/pricing?ref=docs-sleeps) trace & log history limit, even though the functions are still sleeping and will continue to run as expected. **This is a known limitation** in our current dashboard and we're working to improve it. In the meantime: - Rest assured that your sleeping functions *are* still sleeping and will resume as scheduled, even if they're not visible in the Function Runs list. - Given a function run's ID, you can inspect its status using Inngest Cloud's Quick Search feature (Ctrl-K or ⌘K) or the [REST API](https://api-docs.inngest.com/docs/inngest-api/). # AI Inference Source: https://www.inngest.com/docs/features/inngest-functions/steps-workflows/step-ai-orchestration You can build complex AI workflows and call model providers as steps using two-step methods, `step.ai.infer()` and `step.ai.wrap()`, or our AgentKit SDK. They work with any model provider, and all offer full AI observability: - [AgentKit](https://agentkit.inngest.com) allows you to easily create single model calls or agentic workflows. Read the AgentKit docs here - `step.ai.wrap()` wraps other AI SDKs (OpenAI, Anthropic, and Vercel AI SDK) as a step, augmenting the observability of your Inngest Functions with information such as prompts and tokens used. - `step.ai.infer()` offloads the inference request to Inngest's infrastructure, pausing your function execution until the request finishes. This can be a significant cost saver if you deploy to serverless functions ### Benefits Using [AgentKit](https://agentkit.inngest.com) and `step.ai` allows you to: - Automatically monitor AI usage in production to ensure quality output - Easily iterate and test prompts in the dev server - Track requests and responses from foundational inference providers - Track how inference calls work together in multi-step or agentic workflows - Automatically create datasets based off of production requests
**AgentKit TypeScript SDK** **In TypeScript, we strongly recommend using AgentKit, our AI SDK which adds multiple AI capabilities to Inngest.** AgentKit allows you to call single-shot inference APIs with a simple self-documenting class and also allows you to create semi or fully autonomous agent workflows using a network of agents. - [AgentKit GitHub repo](https://github.com/inngest/agent-kit) - [AgentKit docs](https://agentkit.inngest.com) ## AgentKit: AI and agent orchestration AgentKit is a simple, standardized way to implement model calling — either as individual calls, a complex workflow, or agentic flows. Here's an example of a single model call: ```ts {{ title: "TypeScript" }} export default inngest.createFunction( { id: "summarize-contents" }, { event: "app/ticket.created" }, async ({ event, step }) => { // Create a new agent with a system prompt (you can add optional tools, too) createAgent({ name: "writer", system: "You are an expert writer. You write readable, concise, simple content.", model: openai({ model: "gpt-4o", step }), }); // Run the agent with an input. This automatically uses steps // to call your AI model. await writer.run("Write a tweet on how AI works"); } ); ``` [Read the full AgentKit docs here](https://agentkit.inngest.com) and [see the code on GitHub](https://github.com/inngest/agent-kit). ## Step tools: `step.ai` ### `step.ai.infer()` Using `step.ai.infer()` allows you to call any inference provider's endpoints by offloading it to Inngest's infrastructure. All requests and responses are automatically tracked within your workflow traces. **Request offloading** On serverless environments, your function is not executing while the request is in progress — which means you don't pay for function execution while waiting for the provider's response. Once the request finishes, your function restarts with the inference result's data. Inngest never logs or stores your API keys or authentication headers. Authentication originates from your own functions. Here's an example which calls OpenAI: ```ts {{ title: "TypeScript" }} export default inngest.createFunction( { id: "summarize-contents" }, { event: "app/ticket.created" }, async ({ event, step }) => { // This calls your model's chat endpoint, adding AI observability, // metrics, datasets, and monitoring to your calls. await step.ai.infer("call-openai", { model: step.ai.models.openai({ model: "gpt-4o" }), // body is the model request, which is strongly typed depending on the model body: { messages: [{ role: "assistant", content: "Write instructions for improving short term memory", }], }, }); // The response is also strongly typed depending on the model. return response.choices; } ); ``` } iconPlacement="top" > Use `step.ai.infer()` to process a PDF with Claude Sonnet. ### `step.ai.wrap()` (TypeScript only) Using `step.ai.wrap()` allows you to wrap other TypeScript AI SDKs, treating each inference call as a step. This allows you to easily convert AI calls to steps with full observability without changing much application-level code: ```ts {{ title: "Vercel AI SDK" }} export default inngest.createFunction( { id: "summarize-contents" }, { event: "app/ticket.created" }, async ({ event, step }) => { // This calls `generateText` with the given arguments, adding AI observability, // metrics, datasets, and monitoring to your calls. await step.ai.wrap("using-vercel-ai", generateText, { model: openai("gpt-4-turbo"), prompt: "What is love?" }); } ); ``` ```ts {{ title: "Anthropic SDK" }} new Anthropic(); export default inngest.createFunction( { id: "summarize-contents" }, { event: "app/ticket.created" }, async ({ event, step }) => { // This calls `generateText` with the given arguments, adding AI observability, // metrics, datasets, and monitoring to your calls. await step.ai.wrap("using-anthropic", anthropic.messages.create, { model: "claude-3-5-sonnet-20241022", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], }); } ); ``` In this case, instead of calling the SDK directly, you specify the SDK function you want to call and the function's arguments separately within `step.ai.wrap()`. ### Supported providers The list of current providers supported for `step.ai.infer()` is: - `openai`, including any OpenAI compatible API such as Perplexity - `gemini` ### Limitations - Streaming responses from providers is coming soon, alongside real-time support with Inngest functions. - When using `step.ai.wrap` with sdk clients that require client instance context to be preserved between invocations, currently it's necessary to bind the client call outside the `step.ai.wrap` call like so: ```ts {{ title: "Wrap Anthropic SDK" }} new Anthropic(); anthropicWrapGenerateText = inngest.createFunction( { id: "anthropic-wrap-generateText" }, { event: "anthropic/wrap.generate.text" }, async ({ event, step }) => { // // Will fail because anthropic client requires instance context // to be preserved across invocations. await step.ai.wrap( "using-anthropic", anthropic.messages.create, { model: "claude-3-5-sonnet-20241022", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], }, ); // // Will work beccause we bind to preserve instance context anthropic.messages.create.bind(anthropic.messages); await step.ai.wrap( "using-anthropic", createCompletion, { model: "claude-3-5-sonnet-20241022", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], }, ); }, ); ``` ```ts {{ title: "Wrap OpenAI SDK" }} new OpenAI({ apiKey: OPENAI_API_KEY }); openAIWrapCompletionCreate = inngest.createFunction( { id: "opeai-wrap-completion-create" }, { event: "openai/wrap.completion.create" }, async ({ event, step }) => { // // Will fail because anthropic client requires instance context // to be preserved across invocations. await step.ai.wrap( "openai.wrap.completions", openai.chat.completions.create, { model: "gpt-4o-mini", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Write a haiku about recursion in programming.", }, ], }, ); // // Will work beccause we bind to preserve instance context openai.chat.completions.create.bind( openai.chat.completions, ); await step.ai.wrap( "openai-wrap-completions", createCompletion, { model: "gpt-4o-mini", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Write a haiku about recursion in programming.", }, ], }, ); }, ); ``` - When using `step.ai.wrap`, you can edit prompts and rerun steps in the dev server. But, arguments must be JSON serializable. ```ts {{ title: "Vercel AI SDK" }} vercelWrapGenerateText = inngest.createFunction( { id: "vercel-wrap-generate-text" }, { event: "vercel/wrap.generate.text" }, async ({ event, step }) => { // // Will work but you will not be able to edit the prompt and rerun the step in the dev server. await step.ai.wrap( "vercel-openai-generateText", vercelGenerateText, { model: vercelOpenAI("gpt-4o-mini"), prompt: "Write a haiku about recursion in programming.", }, ); // // Will work and you will be able to edit the prompt and rerun the step in the dev server because // the arguments to step.ai.wrap are JSON serializable. { model: "gpt-4o-mini", prompt: "Write a haiku about recursion in programming.", }; ({ model, prompt }: { model: string; prompt: string }) => vercelGenerateText({ model: vercelOpenAI(model), prompt, }); await step.ai.wrap("using-vercel-ai", gen, args); }, ); ``` - `step.ai.wrap's` Typescript definition will for the most part infer allowable inputs based on the signature of the wrapped function. However, in some cases where the wrapped function contains complex overloads, such as Vercel's `generateObject`, it may be necessary to type cast. *Note*: Future version of the Typescript SDK will correctly infer these complex types, but for no,w we require type casting to ensure backward compatibility. ```ts {{ title: "Vercel AI SDK" }} vercelWrapSchema = inngest.createFunction( { id: "vercel-wrap-generate-object" }, { event: "vercel/wrap.generate.object" }, async ({ event, step }) => { // // Calling generateObject directly is fine await vercelGenerateObject({ model: vercelOpenAI("gpt-4o-mini"), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string() }), ), steps: z.array(z.string()), }), }), prompt: "Generate a lasagna recipe.", }); // // step.ai.wrap requires type casting await step.ai.wrap( "vercel-openai-generateObject", vercelGenerateObject, { model: vercelOpenAI("gpt-4o-mini"), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string() }), ), steps: z.array(z.string()), }), }), prompt: "Generate a lasagna recipe.", } as any, ); }, ); ``` # Wait for an Event Source: https://www.inngest.com/docs/features/inngest-functions/steps-workflows/wait-for-event {/* Sidebar doesn't work well when GuideSection have different headings... */} hidePageSidebar = true; One step method is available to pause a Function's run until a given event is sent. This is a useful pattern to react to specific user actions (for example, implement "Human in the loop" in AI Agent workflows). Use `step.waitForEvent()` to wait for a particular event to be received before continuing. It returns a `Promise` that is resolved with the received event or `null` if the event is not received within the timeout. ```ts export default inngest.createFunction( { id: "send-onboarding-nudge-email" }, { event: "app/account.created" }, async ({ event, step }) => { await step.waitForEvent( "wait-for-onboarding-completion", { event: "app/onboarding.completed", timeout: "3d", match: "data.userId" } ); if (!onboardingCompleted) { // if no event is received within 3 days, onboardingCompleted will be null } else { // if the event is received, onboardingCompleted will be the event payload object } } ); ``` Check out the [`step.waitForEvent()` TypeScript reference.](/docs/reference/functions/step-wait-for-event) To add a simple time based delay to your code, use [`step.sleep()`](/docs/reference/functions/step-sleep) instead. ## Examples ### Dynamic functions that wait for additional user actions Below is an example of an Inngest function that creates an Intercom or Customer.io-like drip email campaign, customized based on ```ts export default inngest.createFunction( { id: "onboarding-email-drip-campaign" }, { event: "app/account.created" }, async ({ event, step }) => { // Send the user the welcome email immediately await step.run("send-welcome-email", async () => { await sendEmail(event.user.email, "welcome"); }); // Wait up to 3 days for the user to complete the final onboarding step // If the event is received within these 3 days, onboardingCompleted will be the // event payload itself, if not it will be null await step.waitForEvent("wait-for-onboarding", { event: "app/onboarding.completed", timeout: "3d", // The "data.userId" must match in both the "app/account.created" and // the "app/onboarding.completed" events match: "data.userId", }); // If the user has not completed onboarding within 3 days, send them a nudge email if (!onboardingCompleted) { await step.run("send-onboarding-nudge-email", async () => { await sendEmail(event.user.email, "onboarding_nudge"); }); } else { // If they have completed onboarding, send them a tips email await step.run("send-tips-email", async () => { await sendEmail(event.user.email, "new_user_tips"); }); } } ); ``` ### Advanced event matching with `if` For more complex functions, you may want to match the event payload against some other value. This could be a hard coded value like a billing plan name, a greater than filter for a number value or a value returned from a previous step. In this example, we have built an AI blog post generator which returns three ideas to the user to select. Then when the user selects an idea from that batch of ideas, we generate an entire blog post and save it. ```ts export default inngest.createFunction( { id: "generate-blog-post-with-ai" }, { event: "ai/post.generator.requested" }, async ({ event, step }) => { // Generate a number of suggestions for topics with OpenAI await step.run("generate-topic-ideas", async () => { await openai.createCompletion({ model: "text-davinci-003", prompt: helpers.topicIdeaPromptWrapper(event.data.prompt), n: 3, }); return { completionId: completion.data.id, topics: completion.data.choices, }; }); // Send the topics to the user via Websockets so they can select one // Also send the completion id so we can match that later await step.run("send-user-topics", () => { pusher.sendToUser(event.data.userId, "topics_generated", { sessionId: event.data.sessionId, completionId: generatedTopics.completionId, topics: generatedTopics.topics, }); }); // Wait up to 5 minutes for the user to select a topic // Ensuring the topic is from this batch of suggestions generated await step.waitForEvent("wait-for-topic-selection", { event: "ai/post.topic.selected", timeout: "5m", // "async" is the "ai/post.topic.selected" event here: if: `async.data.completionId == "${generatedTopics.completionId}"`, }); // If the user selected a topic within 5 minutes, "topicSelected" will // be the event payload, otherwise it is null if (topicSelected) { // Now that we've confirmed the user selected their topic idea from // this batch of suggestions, let's generate a blog post await step.run("generate-blog-post-draft", async () => { await openai.createCompletion({ model: "text-davinci-003", prompt: helpers.blogPostPromptWrapper(topicSelected.data.prompt), }); // Do something with the blog post draft like save it or something else... await blog.saveDraft(completion.data.choices[0]); }); } } ); ``` Use `step.wait_for_event()` to wait for a particular event to be received before continuing. ```py @inngest_client.create_function( fn_id="my_function", trigger=inngest.TriggerEvent(event="app/my_function"), ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: res = await step.wait_for_event( "wait", event="app/wait_for_event.fulfill", timeout=datetime.timedelta(seconds=2), ) ``` Check out the [`step.wait_for_event()` Python reference.](/docs/reference/python/steps/wait-for-event) Use `step.waitForEvent()` to wait for a particular event to be received before continuing. It either returns the received event data or a `step.ErrEventNotReceived` error. ```go func AccountCreated(ctx context.Context, input inngestgo.Input[AccountCreatedEvent]) (any, error) { // Sleep for a second, minute, hour, week across server restarts. opened, err = step.waitForEvent(ctx, "wait-for-open", opts.WaitForEventOpts{ Event: "email/mail.opened", If: inngestgo.StrPtr(fmt.Sprintf("async.data.id == %s", strconv.Quote("my-id"))), Timeout: 24 * time.Hour, }) 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 } ``` Check out the [`step.WaitForEvent()` Go reference.](https://pkg.go.dev/github.com/inngest/inngestgo@v0.9.0/step#WaitForEvent) **Preventing race conditions** The "wait for event" method begins listening for new events from when the code is executed. This means that events sent before the function is executed will not be handled by the wait. To avoid race condition, always double-check the flow of events going through your functions.
_Note: The "wait for event" mechanism will soon provide a "lookback" feature, including events from a given past timeframe._
# Steps & Workflows Source: https://www.inngest.com/docs/features/inngest-functions/steps-workflows import { RiGuideFill, RiCalendarLine, RiGitForkFill, } from "@remixicon/react"; Steps are fundamental building blocks of Inngest, turning your Inngest Functions into reliable workflows that can runs for months and recover from failures. } href={'/docs/guides/multi-step-functions'}> Discover by example how steps enable more reliable and flexible functions with step-level error handling, conditional steps and waits. Once you are familiar with Steps, start adding new capabilities to your Inngest Functions: } href={'/docs/features/inngest-functions/steps-workflows/sleeps'}> Enable your Inngest Functions to pause by waiting from minutes to months. } href={'/docs/features/inngest-functions/steps-workflows/wait-for-event'}> Write functions that react to incoming events. } href={'/docs/guides/working-with-loops'}> Iterate over large datasets by looping with steps. } href={'/docs/guides/step-parallelism'}> Discover how to apply the map-reduce pattern with Steps. ## How steps work You might wonder: how do Steps work? Why doesn't an Inngest Function get timed out when running on a Serverless environment? You can think of steps as an API for expressing checkpoints in your workflow, such as waits or work that might benefit from retries or parallelism: ```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "sync-systems" }, { event: "auto/sync.request" }, async ({ step }) => { // By wrapping code in step.run, the code will be retried if it throws an error and when successfuly. // It's result is saved to prevent unnecessary re-execution await step.run("get-data", async () => { return getDataFromExternalSource(); }); // Can also be retried up to 4 times await step.run("save-data", async () => { return db.syncs.insertOne(data); }); }, ); ``` ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="sync-systems", trigger=inngest.TriggerEvent(event="auto/sync.request"), ) def sync_systems(ctx: inngest.Context, step: inngest.StepSync) -> None: # By wrapping code in step.run, the code will be retried if it throws an error and when successfuly. # It's result is saved to prevent unnecessary re-execution data = step.run("Get data", get_data_from_external_source) # Can also be retried up to 4 times step.run("Save data", db.syncs.insert_one, data) ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "sync-systems"}, inngestgo.EventTrigger("auto/sync.request", nil), func(ctx context.Context, input inngestgo.Input[SyncRequestEvent]) (any, error) { // By wrapping code in step.run, the code will be retried if it throws an error and when successfuly. // It's result is saved to prevent unnecessary re-execution data, err := step.Run(ctx, "get-data", func(ctx context.Context) (any, error) { return getDataFromExternalSource() }) if err != nil { return nil, err } // can also be retried up to 4 times _, err = step.Run(ctx, "save-data", func(ctx context.Context) (any, error) { return db.Syncs.InsertOne(data.(DataType)) }) if err != nil { return nil, err } return nil, nil }, ) ``` Each step execution relies on a communication with Inngest's [Durable Execution Engine](/docs/learn/how-functions-are-executed) which is responsible to: - Invoking Functions with the correct steps state (current step + previous steps data) - Gather each step result and schedule the next step to perform This architecture powers the durability of Inngest Functions with retriable steps and waits from hours to months. Also, when used in a serverless environment, steps benefit from an extended max duration, enabling workflows that both span over months and run for more than 5 minutes! Explore the following guide for a step-by-step overview of a complete workflow run: } href={'/docs/learn/how-functions-are-executed'}> A deep dive into Inngest's Durable Execution Engine with a step-by-step workflow run example. ## SDK References } > Steps API reference } > Steps API reference } > Steps API reference # Inngest Functions Source: https://www.inngest.com/docs/features/inngest-functions import { RiGitPullRequestFill, RiGuideFill, RiTimeLine, RiCalendarLine, RiMistFill, } from "@remixicon/react"; Inngest functions enable developers to run reliable background logic, from background jobs to complex workflows. An Inngest Function is composed of 3 main parts that provide robust tools for retrying, scheduling, and coordinating complex sequences of operations: } 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/features/inngest-functions/steps-workflows'}> Transform your Inngest Function into a workflow with retriable checkpoints. ```ts {{ title: "TypeScript" }} inngest.createFunction({ id: "sync-systems", // Easily add Throttling with Flow Control throttle: { limit: 3, period: "1min"}, }, // A Function is triggered by events { event: "auto/sync.request" }, 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); }); } ); ``` ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="sync-systems", # A Function is triggered by events trigger=inngest.TriggerEvent(event="auto/sync.request"), # Easily add Throttling with Flow Control throttle=inngest.Throttle( count=2, period=datetime.timedelta(minutes=1) ), ) def sync_systems(ctx: inngest.Context, step: inngest.StepSync) -> None: # step is retried if it throws an error data = step.run("Get data", get_data_from_external_source) # Steps can reuse data from previous ones step.run("Save data", db.syncs.insert_one, data) ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "sync-systems", }, // Functions are triggered by events inngestgo.EventTrigger("auto/sync.request", nil), func(ctx context.Context, input inngestgo.Input[SyncRequestEvent]) (any, error) { // step is retried if it throws an error data, err := step.Run(ctx, "get-data", func(ctx context.Context) (any, error) { return getDataFromExternalSource() }) if err != nil { return nil, err } // steps can reuse data from previous ones _, err = step.Run(ctx, "save-data", func(ctx context.Context) (any, error) { return db.Syncs.InsertOne(data.(DataType)) }) if err != nil { return nil, err } return nil, nil }, ) ``` {/* 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/multi-step-functions'}> Run long-running tasks out of the critical path of a request. } href={'/docs/learn/how-functions-are-executed'}> Schedule Functions that run in the future. } href={'/docs/guides/scheduled-functions'}> Build Inngest Functions as CRONs. } href={'/docs/features/inngest-functions/steps-workflows'}> Start creating worflows 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/guides/multi-step-functions'}> 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 # Creating middleware Source: https://www.inngest.com/docs/features/middleware/create import { RiEyeOffLine, RiMistFill, RiPlugLine, RiTerminalBoxLine, RiKeyLine, } from "@remixicon/react"; Creating middleware means defining the lifecycles and subsequent hooks in those lifecycles to run code in. Lifecycles are actions such as a function run or sending events, and individual hooks within those are where we run code, usually with a _before_ and _after_ step. A Middleware is created using the `InngestMiddleware` class. **`new InngestMiddleware(options): InngestMiddleware`** ```ts // Create a new middleware new InngestMiddleware({ name: "My Middleware", init: () => { return {}; }, }); // Register it on the client new Inngest({ id: "my-app", middleware: [myMiddleware], }); ``` A Middleware is created using the `inngest.Middleware` class. **`class MyMiddleware(inngest.Middleware):`** ```py import inngest class MyMiddleware(inngest.Middleware): def __init__( self, client: inngest.Inngest, raw_request: object, ) -> None: # ... 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], ) ``` ## Initialization As you can see above, we start with the `init` function, which is called when the client is initialized. ```ts new InngestMiddleware({ name: "Example Middleware", init() { // This runs when the client is initialized // Use this to set up anything your middleware needs return {}; }, }); ``` As you can see above, we start with the `__init__` method, which is called when the client is initialized. ```py import inngest class MyMiddleware(inngest.Middleware): def __init__( self, client: inngest.Inngest, raw_request: object, ) -> None: # This runs when the client is initialized # Use this to set up anything your middleware needs # ... ``` Function registration, lifecycles, and hooks can all be with synchronous or `async` functions. This makes it easy for our initialization handler to do some async work, like setting up a database connection. ```ts new InngestMiddleware({ name: "Example Middleware", async init() { await connectToDatabase(); return {}; }, }); ``` ```py import inngest class MyMiddleware(inngest.Middleware): def __init__( self, client: inngest.Inngest, raw_request: object, ) -> None: # ...connect to database ``` All lifecycle and hook functions can be synchronous or `async` functions - the SDK will always wait until a middleware's function has resolved before continuing to the next one. As it's possible for an application to use multiple Inngest clients, it's recommended to always initialize dependencies within the initializer function/method, instead of in the global scope. ## Specifying lifecycles and hooks Notice we're returning an empty object `{}`. From here, we can instead return the lifecycles we want to use for this client. See the [Middleware - Lifecycle - Hook reference](/docs/reference/middleware/lifecycle#hook-reference) for a full list of available hooks. ```ts new InngestMiddleware({ name: "Example Middleware", async init() { // 1. Use init to set up dependencies // 2. Use return values to group hooks by lifecycle: - "onFunctionRun" "onSendEvent" return { onFunctionRun({ ctx, fn, steps }) { // 3. Use the lifecycle function to pass dependencies into hooks // 4. Return any hooks that you want to define for this action return { // 5. Define the hook that runs at a specific stage for this lifecycle. beforeExecution() { // 6. Define your hook }, }; }, }; }, }); ``` Here we use the `beforeExecution()` hook within the `onFunctionRun()` lifecycle. The use of [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) here means that our `onFunctionRun()` lifecycle can access anything from the middleware's initialization, like our `db` connection. `onFunctionRun()` here is also called for every function execution, meaning you can run code specific to this execution without maintaining any global state. We can even conditionally register hooks based on incoming arguments. For example, here we only register a hook for a specific event trigger: ```ts new InngestMiddleware({ name: "Example Middleware", async init() { return { onFunctionRun({ ctx, fn, steps }) { // Register a hook only if this event is the trigger if (ctx.event.name === "app/user.created") { return { beforeExecution() { console.log("Function executing with user created event"); }, }; } // Register no hooks if the trigger was not `app/user.created` return {}; }, }; }, }); ``` Learn more about hooks with: - [Lifecycle](/docs/reference/middleware/lifecycle) - middleware ordering and see all available hooks - [TypeScript](/docs/reference/middleware/typescript) - how to affect input and output types and values You might have notice that our custom middleware defines custom method such as `before_send_events` and `after_send_events`. Those methods, called hooks, enable your middleware to hook itself to specific steps of the Function and Steps execution lifecycle. ```py import inngest class MyMiddleware(inngest.Middleware): def __init__( self, client: inngest.Inngest, raw_request: object, ) -> None: # ... async def before_send_events( self, events: list[inngest.Event]) -> None: # called before an event is sent from within a Function or Step print(f"Sending {len(events)} events") async def after_send_events(self, result: inngest.SendEventsResult) -> None: # called after an event is sent from within a Function or Step print("Done sending events") ``` You can find the [full list of available hooks in the Python SDK reference](/docs/reference/python/middleware/lifecycle). ## Adding configuration It's common for middleware to require additional customization or options from developers. For this, we recommend creating a function that takes in some options and returns the middleware. ```ts {{ title: "inngest/middleware/myMiddleware.ts" }} createMyMiddleware = (logEventOutput: string) => { return new InngestMiddleware({ name: "My Middleware", init() { return { onFunctionRun({ ctx, fn, steps }) { if (ctx.event.name === logEventOutput) { return { transformOutput({ result, step }) { console.log( `${logEventOutput} output: ${JSON.stringify(result)}` ); }, }; } return {}; }, }; }, }); }; ``` ```ts inngest = new Inngest({ id: "my-client", middleware: [createMyMiddleware("app/user.created")], }); ``` Make sure to let TypeScript infer the output of the function instead of strictly typing it; this helps Inngest understand changes to input and output of arguments. See [Middleware - TypeScript](/docs/reference/middleware/typescript) for more information. Adding configuration to a custom middleware can be achieved by adding a `factory()` class method, leveraging the [Factory pattern](https://en.wikipedia.org/wiki/Factory_method_pattern). For example, let's add a `secret_key` configuration option to our `MyMiddleware` middleware: ```py import inngest class MyMiddleware(inngest.Middleware): def __init__( self, client: inngest.Inngest, raw_request: object, ) -> None: # ... @classmethod def factory( cls, secret_key: typing.Union[bytes, str], ) -> typing.Callable[[inngest.Inngest, object], MyMiddleware]: def _factory( client: inngest.Inngest, raw_request: object, ) -> MyMiddleware: return cls( client, raw_request, secret_key, ) return _factory async def before_send_events( self, events: list[inngest.Event]) -> None: # called before an event is sent from within a Function or Step print(f"Sending {len(events)} events") async def after_send_events(self, result: inngest.SendEventsResult) -> None: # called after an event is sent from within a Function or Step print("Done sending events") ``` Our middleware can now be registered as follow: ```py inngest_client = inngest.Inngest( app_id="my_app", middleware=[MyMiddleware.factory(_secret_key)], ) ``` ## Next steps Check out our pre-built middleware and examples: } href={'/docs/features/middleware/dependency-injection'}> Provide shared client instances (ex, OpenAI) to your Inngest Functions. } href={'/docs/features/middleware/encryption-middleware'}> End-to-end encryption for events, step output, and function output. } href={'/docs/features/middleware/sentry-middleware'}> Quickly setup Sentry for your Inngest Functions. } href={'/docs/examples/track-failures-in-datadog'}> Add tracing with Datadog under a few minutes. } href={'/docs/examples/middleware/cloudflare-workers-environment-variables'}> Access environment variables within Inngest functions. # Using Middleware for Dependency Injection Source: https://www.inngest.com/docs/features/middleware/dependency-injection Inngest Functions running in the same application often need to share common clients instances such as database clients or third-party libraries. The following is an example of adding a OpenAI client to all Inngest functions, allowing them immediate access without needing to create the client themselves. We can use the `dependencyInjectionMiddleware` to add arguments to a function's input. Check out the [TypeScript example](?guide=typescript) for a customized middleware. ```ts new OpenAI(); new Inngest({ id: 'my-app', middleware: [ dependencyInjectionMiddleware({ openai }), ], }); ``` Our Inngest Functions can now access the OpenAI client through the context: ```ts inngest.createFunction( { name: "user-create" }, { event: "app/user.create" }, async ({ openai }) => { await openai.chat.completions.create({ messages: [{ role: "user", content: "Say this is a test" }], model: "gpt-3.5-turbo", }); // ... }, ); ``` 💡 Types are inferred from middleware outputs, so your Inngest functions will see an appropriately-typed `openai` property in their input. Explore other examples in the [TypeScript SDK Middleware examples page](/docs/reference/middleware/examples). ### Advanced mutation When the middleware runs, the types and data within the passed `ctx` are merged on top of the default provided by the library. This means that you can use a few tricks to overwrite data and types safely and more accurately. For example, here we use a `const` assertion to infer the literal value of our `foo` example above. ```ts // In middleware dependencyInjectionMiddleware({ foo: "bar", } as const) // In a function async ({ event, foo }) => { // ^? (parameter) foo: "bar" } ``` ## Ordering middleware and types Middleware runs in the order specified when registering it (see [Middleware - Lifecycle - Registering and order](/docs/reference/middleware/lifecycle#registering-and-order)), which affects typing too. When inferring a mutated input or output, the SDK will apply changes from each middleware in sequence, just as it will at runtime. This means that for two middlewares that add a `foo` value to input arguments, the last one to run will be what it seen both in types and at runtime. Our custom `openaiMiddleware` relies on the [`transformInput` hook](/docs/reference/middleware/lifecycle#on-function-run-lifecycle) to mutate the Function's context: ```ts new InngestMiddleware({ name: "OpenAI Middleware", init() { new OpenAI(); return { onFunctionRun(ctx) { return { transformInput(ctx) { return { // Anything passed via `ctx` will be merged with the function's arguments ctx: { openai, }, }; }, }; }, }; }, }); ``` Our Inngest Functions can now access the OpenAI client through the context: ```ts inngest.createFunction( { name: "user-create" }, { event: "app/user.create" }, async ({ openai }) => { await openai.chat.completions.create({ messages: [{ role: "user", content: "Say this is a test" }], model: "gpt-3.5-turbo", }); // ... }, ); ``` 💡 Types are inferred from middleware outputs, so your Inngest functions will see an appropriately-typed `openai` property in their input. Explore other examples in the [TypeScript SDK Middleware examples page](/docs/reference/middleware/examples). ### Advanced mutation When middleware runs and `transformInput()` returns a new `ctx`, the types and data within that returned `ctx` are merged on top of the default provided by the library. This means that you can use a few tricks to overwrite data and types safely and more accurately. For example, here we use a `const` assertion to infer the literal value of our `foo` example above. ```ts // In middleware transformInput() { return { ctx: { foo: "bar", } as const, }; } // In a function async ({ event, foo }) => { // ^? (parameter) foo: "bar" } ``` Because the returned `ctx` object and the default are merged together, sometimes good inferred types are overwritten by more generic types from middleware. A common example of this might be when handling event data in middleware. To get around this, you can provide the data but omit the type by using an `as` type assertion. For example, here we use a type assertion to add `foo` and alter the event data without affecting the type. ```ts async transformInput({ ctx }) { await decrypt(ctx.event); { foo: "bar", event, }; return { // Don't affect the `event` type ctx: newCtx as Omit, }; }, ``` ## Ordering middleware and types Middleware runs in the order specified when registering it (see [Middleware - Lifecycle - Registering and order](/docs/reference/middleware/lifecycle#registering-and-order)), which affects typing too. When inferring a mutated input or output, the SDK will apply changes from each middleware in sequence, just as it will at runtime. This means that for two middlewares that add a `foo` value to input arguments, the last one to run will be what it seen both in types and at runtime. Our `OpenAIMiddleware` uses the [`transform_input` hook](/docs/reference/python/middleware/lifecycle#transform-input) to inject context: ```py import inngest from openai import OpenAI class OpenAIMiddleware(inngest.Middleware): def __init__( self, client: inngest.Inngest, raw_request: object, ) -> None: self.openai = OpenAI( # This is the default and can be omitted api_key=os.environ.get("OPENAI_API_KEY"), ) def transform_input( self, ctx: execution_lib.Context, function: function.Function, steps: step_lib.StepMemos, ) -> None: ctx.openai = self.openai # type: ignore inngest_client = inngest.Inngest( app_id="my_app", middleware=[OpenAIMiddleware], ) ``` Our Inngest Functions can now access the `openai` client through the context: ```py @inngest_client.create_function( fn_id="user-create", trigger=inngest.TriggerEvent(event="app/user.create"), ) async def fn(ctx: inngest.Context, step: inngest.Step): chat_completion = ctx.openai.chat.completions.create( messages=[ { "role": "user", "content": "Say this is a test", } ], model="gpt-3.5-turbo", ) ``` # Encryption Middleware Source: https://www.inngest.com/docs/features/middleware/encryption-middleware import { Callout, Card, CardGroup, CodeGroup, Col, GuideSection, GuideSelector, Info, Properties, Property, Row, VersionBadge, } from "src/shared/Docs/mdx"; 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 The `EncryptionMiddleware` is available as part of the `inngest_encryption` package: ```py import inngest from inngest_encryption import EncryptionMiddleware inngest_client = inngest.Inngest( app_id="my-app", middleware=[EncryptionMiddleware.factory("my-secret-key")], ) ``` The following data is encrypted by default: - The `encrypted` field in `event.data`. - `step.run` return values. - Function return values. 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 // Initialize the middleware encryptionMiddleware({ // your encryption key string should not be hard coded key: process.env.MY_ENCRYPTION_KEY, }); // Use the middleware with Inngest new Inngest({ id: "my-app", middleware: [mw], }); ``` By default, the following will be encrypted: - All step data - All function output - Event data placed inside `data.encrypted` ## Changing the encrypted `event.data` field By default, `event.data.encrypted` is encrypted. All other fields are sent in plaintext. To encrypt a different field, set the `event_encryption_field` parameter. Only select pieces of event data are encrypted. By default, only the data.encrypted field. This can be customized using the `eventEncryptionField: string` setting. ## Decrypt only mode To disable encryption but continue decrypting, set `decrypt_only=True`. This is useful when you want to migrate away from encryption but still need to process older events. 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. ## Fallback decryption keys To attempt decryption with multiple keys, set the `fallback_decryption_keys` parameter. This is useful when rotating keys, since older events may have been encrypted with a different key. To attempt decryption with multiple keys, set the `fallbackDecryptionKeys` parameter. This is useful when rotating keys, since older events may have been encrypted with a different key: ```ts // start out with the current key encryptionMiddleware({ key: process.env.MY_ENCRYPTION_KEY, }); // deploy all services with the new key as a decryption fallback encryptionMiddleware({ key: process.env.MY_ENCRYPTION_KEY, fallbackDecryptionKeys: ["new"], }); // deploy all services using the new key for encryption encryptionMiddleware({ key: process.env.MY_ENCRYPTION_KEY_V2, fallbackDecryptionKeys: ["current"], }); // once you are sure all data using the "current" 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 our TypeScript SDK. Encrypted events can be sent from Python and decrypted in TypeScript, and vice versa. # Sentry Middleware Source: https://www.inngest.com/docs/features/middleware/sentry-middleware Using the Sentry middleware is useful to: - Capture exceptions for reporting - Add tracing to each function run - Include useful context for each exception and trace like function ID and event names ## Installation The `SentryMiddleware` is shipped as part of the `inngest` package: ```py import inngest from inngest.experimental.sentry_middleware import SentryMiddleware import sentry_sdk # Initialize Sentry as usual wherever is appropriate sentry_sdk.init( traces_sample_rate=1.0, profiles_sample_rate=1.0, ) inngest_client = inngest.Inngest( app_id="my-app", middleware=[SentryMiddleware], ) ``` Install the [`@inngest/middleware-sentry` package](https://www.npmjs.com/package/@inngest/middleware-sentry) and configure it as follows: ```ts // Initialize Sentry as usual wherever is appropriate Sentry.init(...); new Inngest({ id: "my-app", middleware: [sentryMiddleware()], }); ``` Requires inngest@>=3.0.0 and @sentry/*@>=8.0.0`. # Middleware Source: https://www.inngest.com/docs/features/middleware import { RiEyeOffLine, RiMistFill, RiPlugLine, RiFileSearchLine, } from "@remixicon/react"; Middleware allows your code to run at various points in an Inngest client's lifecycle, such as during a function's execution or when sending an event. This can be used for a wide range of uses: } href={'/docs/features/middleware/create'}> Add custom logging, tracing or helpers to your Inngest Functions. } href={'/docs/features/middleware/dependency-injection'}> Provide shared client instances (ex, OpenAI) to your Inngest Functions. } href={'/docs/features/middleware/encryption-middleware'}> End-to-end encryption for events, step output, and function output. } href={'/docs/features/middleware/sentry-middleware'}> Quickly setup Sentry for your Inngest Functions. ## Middleware SDKs support Middleware are available in the [TypeScript SDK](/docs/reference/middleware/typescript) and [Python SDK](/docs/reference/python/middleware/lifecycle) . Support in the Go SDK in planned. ## Middleware lifecycle Middleware can be registered at the Inngest clients or functions level. Adding middleware contributes to an overall "stack" of middleware. If you register multiple middlewares, the SDK will group and run hooks for each middleware in the following order: 1. Middleware registered on the **client**, in descending order 2. Middleware registered on the **function**, in descending order For example: ```ts {{ title: "TypeScript" }} new Inngest({ id: "my-app", middleware: [ logMiddleware, // This is executed first errorMiddleware, // This is executed second ], }); inngest.createFunction( { id: "example", middleware: [ dbSetupMiddleware, // This is executed third datadogMiddleware, // This is executed fourth ], }, { event: "test" }, async () => { // ... } ); ``` ```py {{ title: "Python" }} inngest_client = inngest.Inngest( app_id="my_app", middleware=[ LogMiddleware, # This is executed first ErrorMiddleware # This is executed second ], ) # ... @inngest_client.create_function( fn_id="import-product-images", trigger=inngest.TriggerEvent(event="shop/product.imported"), middleware=[ DbSetupMiddleware, # This is executed third DatadogMiddleware # This is executed fourth ], ) async def fn(ctx: inngest.Context, step: inngest.Step): # ... ``` Learn more about the Middleware hooks and their execution order in ["Creating a Middleware"](/docs/features/middleware/create). # Realtime in Next.js Source: https://www.inngest.com/docs/features/realtime/nextjs Description: How to use Realtime in your Next.js app.' # Realtime in Next.js Realtime provides a direct compatibility with Next.js API Routes's streaming capabilities. A `stream` returned by the `subscribe()` helper can be used to create a HTTP stream response: ```tsx {{ filename: app/api/simple-search/route.ts" }} export async function POST(req: Request) { await req.json(); json; // Generate a unique ID for Inngest function run crypto.randomUUID(); // The Inngest function will rely on this ID to publish messages // on a dedicated channel for this run. await inngest.send({ name: "app/simple-search-agent.run", data: { uuid, input: prompt, }, }); // Subscribe to the Inngest function's channel. await subscribe({ channel: `simple-search.${uuid}`, topics: ["updates"], // subscribe to one or more topics in the user channel }); // Stream the response to the client with Vercel's streaming response. return new Response(stream.getEncodedStream(), { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }, }); } ``` On the client, you can consume the stream using a simple `fetch()`: ```tsx {{ filename: "app/components/Chat.tsx" }} "use client"; export function SimpleSearch() { useState([]); useState(""); async (e: React.FormEvent) => { e.preventDefault(); if (!input.trim()) return; try { await fetch("/api/simple-search", { method: "POST", body: JSON.stringify({ prompt: input }), }); response.body?.getReader(); if (!reader) { return; } while (true) { await reader.read(); if (done) { break; } new TextDecoder().decode(value); JSON.parse(text).data; if (data === "Search complete") { reader.cancel(); break; } else { setUpdates((prev) => [...prev, data]); } } } catch (error) { console.error("Error:", error); } finally { setInput(""); } }; return ( // ... ); } ``` } iconPlacement="top" > Explore our Next.js Realtime demo applications. # ### How do I consume the stream on the client? A stream return by a Vercel Function can be consumed by the client using the `fetch()` API. From the `fetch()` response, you can get a `Reader` object, which you can use to read the stream's content using: - a loop to read the stream's content chunk by chunk - a `TextDecoder` to decode the stream's content into a string - a `JSON.parse()` to parse the stream's content into a JSON object ```ts await fetch("/api/simple-search", { method: "POST", body: JSON.stringify({ prompt: input }), }); response.body?.getReader(); if (!reader) { return; } while (true) { await reader.read(); if (done) { break; } new TextDecoder().decode(value); JSON.parse(text).data; if (data === "Search complete") { setIsLoading(false); setIsInputVisible(true); reader.cancel(); break; } else { setUpdates((prev) => [...prev, data]); } } ``` Depending on your use case, you might want to handle the stream's termination differently (see below for an example). ### How do I handle the termination of the stream? By default, an Inngest Realtime stream will remain open until explicitly closed by the client. For this reason, you should handle the stream's termination by publishing a specific message from your Inngest function and handling it in the client's stream reader. ```ts {{ filename: "app/inngest/functions/streaming-workflow.ts" }} simpleSearchAgent = inngest.createFunction( { id: "simple-search-agent-workflow", }, { event: "app/simple-search-agent.run", }, async ({ step, event, publish }) => { event.data; // ... await publish({ channel: `simple-search.${uuid}`, topic: "updates", data: "Search complete", }); return { response, }; } ); ``` ```tsx {{ filename: "app/components/Chat.tsx" }} "use client"; export function SimpleSearch() { useState([]); useState(""); async (e: React.FormEvent) => { e.preventDefault(); if (!input.trim()) return; try { await fetch("/api/simple-search", { method: "POST", body: JSON.stringify({ prompt: input }), }); response.body?.getReader(); if (!reader) { return; } while (true) { await reader.read(); if (done) { break; } new TextDecoder().decode(value); JSON.parse(text).data; if (data === "Search complete") { reader.cancel(); break; } else { setUpdates((prev) => [...prev, data]); } } } catch (error) { console.error("Error:", error); } finally { setInput(""); } }; return ( // ... ); } ``` ### Is it compatible with Vercel's AI `useChat()`? An Inngest Function publishing messages matching the `useChat()` hook's signature will be compatible with it. See the [`Message`](https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-chat#messages) reference for the expected message format. # React hooks Source: https://www.inngest.com/docs/features/realtime/react-hooks Description: Learn how to use the realtime React hooks to subscribe to channels.' # React hooks Realtime provides a `useInngestSubscription()` react hook, used to subscribe using a token and collate streamed data for you. ```tsx import type { Realtime } from closed"`, `"error"`, `"refresh_token"`, `"connecting"`, `"active"`, or `"closing"`. # Realtime Source: https://www.inngest.com/docs/features/realtime Description: Learn how to use realtime to stream data from workflows to your users.' # Realtime Realtime is currently in developer preview. Some details including APIs are still subject to change during this period. Read more about the [developer preview here](#developer-preview). Realtime allows you to stream data from workflows to your users, without configuring infrastructure or maintaining state. This allows you to build interactive applications, show status updates, or stream AI responses to your users directly in your existing code. ## Usage There are two core parts of realtime: **publishing** and **subscribing**. You must publish data from your functions in order for it to be visible. Publishing data accepts three parameters: - `data`, the data to be published to the realtime stream - `topic`, the (optionally typed and validated) topic name, allowing you to differentiate between types of data - `channel`, the name for a group of topics, eg. `user:123` Subscriptions receive data by subscribing to topics within a channel. We manage the WebSocket connections, publishing, subscribing, and state for you. ### Getting started To use realtime, start by installing the `@inngest/realtime` package: ```shell {{ title: import { RiNextjsFill, RiNodejsFill, } from "@remixicon/react"; npm" }} npm install @inngest/realtime ``` ```shell {{ title: "yarn" }} yarn add @inngest/realtime ``` ```shell {{ title: "pnpm" }} pnpm add @inngest/realtime ``` ```shell {{ title: "Bun" }} bun add @inngest/realtime ``` ```shell {{ title: "Deno" }} deno add jsr:@inngest/realtime ``` ## Using our APIs, you publish specific data that users subscribe to. Here's a basic example: ```tsx {{ title: "Minimal" }} // NOTE: This is an untyped, minimal example. To view typed channels, use the code // tabs above. new Inngest({ id: "my-app", // Whenever you create your app, include the `realtimeMiddleware()` middleware: [realtimeMiddleware()], }); inngest.createFunction( { id: "some-task" }, { event: "ai/ai.requested" }, async ({ event, step, publish }) => { // Publish data to a user's channel, on the given topic. Channel names are custom // and act as a container for a group of topics. Each topic is a stream of data. await publish({ channel: `user:${event.data.userId}`, topic: "ai", data: { response: "an llm response here", success: true, }, }); } ); ``` ```tsx {{ title: "Typed channels" }} new Inngest({ id: "my-app", // Whenever you create your app, include the `realtimeMiddleware()` middleware: [realtimeMiddleware()], }); // create a channel for each user, given a user ID. a channel is a namespace // for one or more topics of streams. channel((userId: string) => `user:${userId}`) // Add a specific topic, eg. "ai" for all AI data within the user's channel .addTopic( topic("ai").schema( z.object({ response: z.string(), // Transforms are supported for realtime data success: z.number().transform(Boolean), }) ) ); // we can also create global channels that do not require input channel("logs").addTopic(topic("info").type()); inngest.createFunction( { id: "some-task" }, { event: "ai/ai.requested" }, async ({ event, step, publish }) => { // Publish data to the given channel, on the given topic. await publish( userChannel(event.data.userId).ai({ response: "an llm response here", success: true, }) ); await publish(logsChannel().info("All went well")) } ); ``` ### Subscribing Subscribing can be done using an Inngest client that either has a valid signing key or a subscription token. You can can subscribe from the [client](#subscribe-from-the-client) or the [backend](#subscribe-from-the-backend). ### Subscribe from the client Subscription tokens should be created on the server and passed to the client. You can create a new endpoint to generate a token, checking things like user permissions or channel subscriptions. Here's an example of a server endpoint that creates a token, scoped to a user's channel and specific topics. ```ts {{ title: "Next.js - App router" }} // this could be any auth provider // ex. /api/get-subscribe-token export async function POST() { await auth() await getSubscriptionToken({ channel: `user:${userId}`, topics: ["ai"], }) return NextResponse.json({ token }, { status: 200 }) } ``` ```ts {{ title: "Express" }} // this could be any auth provider app.post("/get-subscribe-token", async (req, res) => { getAuth(req) await getSubscriptionToken({ channel: `user:${userId}`, topics: ["ai"], }) res.json({ token }) }) ``` From the client, you can send a request to your server endpoint to fetch the token: ```ts {{ title: "Client" }} await fetch("/api/get-subscribe-token", { method: "POST", credentials: "include", }).then(res => res.json()); ``` Once you have a token, you can subscribe to a channel by calling the `subscribe` function with the token. You can also subscribe using the `useInngestSubscription` React hook. Read more about the [React hook here](/docs/features/realtime/react-hooks). ```ts {{ title: "Basic subscribe" }} await subscribe(token) for await (const message of stream) { console.log(message) } ``` ```ts {{ title: "React hook - useInngestSubscription" }} export default function MyComponent({ token }: { token: Realtime.Subscribe.Token }) { useInngestSubscription({ token }); return (
{data.map((message, i) => (
{message.data}
))}
); } ```
That's all you need to do to subscribe to a channel from the client!
### Subscribe from the backend Subscribing on the backend is simple: ```ts {{ title: "Minimal" }} await subscribe({ channel: "user:123", topics: ["ai"], // subscribe to one or more topics in the user channel }); // The returned `stream` from `subscribe()` is a `ReadableStream` that can be // used with `getReader()` or as an async iterator // // In both cases, message is typed based on the subscription // Example 1: AsyncIterator for await (const message of stream) { console.log(message); } // Example 2: ReadableStream stream.getReader(); await reader.read(); if (!done) { console.log(value); } ``` ```ts {{ title: "Typed channels" }} await subscribe({ channel: userChannel("123"), topics: ["ai"], // subscribe to one or more topics in the user channel }); // The returned `stream` from `subscribe()` is a `ReadableStream` that can be // used with `getReader()` or as an async iterator // // In both cases, message is typed based on the subscription // Example 1: AsyncIterator for await (const message of stream) { console.log(message); // message is now typed/validated } // Example 2: ReadableStream stream.getReader(); await reader.read(); if (!done) { console.log(value); // value is now typed/validated } ``` ### Type-only channels When passing channels to `subscribe()` or `getSubscriptionToken()`, you may not be able to import a channel directly, for example if the code is contained within a Node package and we're on the browser. For these instances we can use `typeOnlyChannel()` to use the types of the channel without requiring the runtime object: ```ts import { subscribe, getSubscriptionToken, typeOnlyChannel, } from "@inngest/realtime"; await fetchTokenFromBackend(); await subscribe({ channel: typeOnlyChannel("user:123"), topics: ["ai"], }); // or generating a token... await getSubscriptionToken({ channel: typeOnlyChannel("user:123"), topics: ["ai"], }); ``` For convenience, a `Realtime.Token` type helper is provided to help type backend outputs when generating tokens for your frontend: ```ts type UserAiToken = Realtime.Token; ``` ## Concepts ### Channels Channels are environment-level containers which group one or more topics of data. You can create as many channels as you need. Some tips: - You can subscribe to a channel before any data is published - You can create a channel for a specific run ID, eg. for a run's status: `run:${ctx.runId}` - You can create channels for each user, or for a given conversation ### Topics Topics allow you to specify individual streams within a channel. For example, within a given run you may publish status updates, AI responses, and tool outputs to a user. Benefits of separating data by topics include: - **Typing and data handling**: you can switch on the topic name to properly type and handle different streams of data within a channel. - **Security**: you must specify topics when creating subscription tokens, allowing you to protect or hide specific published data. ### Subscription Tokens Subscription tokens allow you to subscribe to the specified channel's topics. Tokens expire 1 minute after creation for security purposes. Once connected, you do not need to manage authentication or re-issue tokens to keep the connection active. ## SDK Support Realtime is supported in the following SDKs: | SDK | Publish | Subscribe | Version | | ---------- | ------- | --------- | ------- | | TypeScript | ✅ | ✅ | >=v3.32.0 | | Golang | ✅ | ✅ | >=v0.9.0 | | Python | In progress | In progress | - | ## Limitations - The number of currently active topics depends on your Inngest plan - Data sent is currently at-most-once and ephemeral - The max message size is currently 512KB ## Developer preview Realtime is available as a developer preview. During this period: * This feature is **widely available** for all Inngest accounts. * Some details including APIs and SDKs are subject to change based on user feedback. * There is no additional cost to using realtime. Realtime will be available to all Inngest billing plans at general availability, but final pricing is not yet determined. ## Security Realtime is secure by default. You can only subscribe to a channel's topics using time-sensitive tokens. The subscription token mechanism must be placed within your own protected API endpoints. You must always specify the channel and topics when publishing data. This lets you ensure that users can only access specific subsets of data within runs. ## Delivery guarantees Message delivery is currently at-most-once. We recommend that your users subscribe to a channel's topics as you invoke runs or send events to ensure delivery of data within a topic. ## Examples } iconPlacement="top" > A Next.js app with Inngest Realtime including React hooks, and creating tokens via server actions. } iconPlacement="top" > A single-file demo of using Inngest Realtime with Node.js. # Managing concurrency Source: https://www.inngest.com/docs/functions/concurrency Limit the number of concurrently running steps for your function with the [`concurrency`](/docs/reference/functions/create#configuration) configuration options. Setting an optional `key` parameter limits the concurrency for each unique value of the expression. [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 functions running 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 execution 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, }, ], } { 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/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) The current concurrency option controls the number of concurrent _steps_ that can be running at any one time. Because a single function run can contain multiple steps, it's possible that more functions than the concurrency limit are triggered, but only the set number of steps will ever be running. # Referencing functions Source: https://www.inngest.com/docs/functions/references Using [`step.invoke()`](/docs/reference/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 [Zod](https://zod.dev) schemas 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 [Zod](https://zod.dev) schema to use to provide typing to the `data` payload required by the referenced function. The [Zod](https://zod.dev) schema to use to provide typing to the return value of the referenced function when invoked. # Next.js Quick Start Source: https://www.inngest.com/docs/getting-started/nextjs-quick-start description = `Get started with Inngest in this ten-minute Next.js tutorial` In this tutorial you will add Inngest to a Next.js app to see how easy it can be to build complex workflows. Inngest makes it easy to build, manage, and execute reliable workflows. 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! ### Choose Next.js version Choose your preferred Next.js version for this tutorial: ## Before you start: choose a project In this tutorial you can use any existing Next.js project, or you can create a new one.
Instructions for creating a new Next.js project Run the following command in your terminal to create a new Next.js project: ```shell npx create-next-app@latest --ts --eslint --tailwind --no-src-dir --no-app --import-alias='@/*' inngest-guide ``` ```shell npx create-next-app@latest --ts --eslint --tailwind --src-dir --app --import-alias='@/*' inngest-guide ```
Once you've chosen a project, open it in a code editor. Next, start your Next.js app in development mode by running: ```shell npm run dev ``` Now you can add Inngest to your project! ## 1. Install Inngest With the Next.js app now running running open a new tab in your terminal. 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 events and function runs: ```shell {{ title: "npm" }} npx inngest-cli@latest dev ``` ```shell {{ title: "yarn" }} yarn dlx inngest-cli@latest dev ``` ```shell {{ title: "pnpm" }} pnpm dlx inngest-cli@latest dev ``` ```shell {{ title: "bun" }} bun add global inngest-cli@latest inngest-cli dev ```
You should see a similar output to the following: ```bash {{ language: 'js' }} $ npx inngest-cli@latest dev 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/client/create) in your Next.js project, which you will use to send events and create functions. Create a new file at `./pages/api/inngest.ts` with the following code: ```ts {{ filename: "pages/api/inngest.ts" }} // Create a client to send and receive events inngest = new Inngest({ id: "my-app" }); // Create an API that serves zero functions export default serve({ client: inngest, functions: [ /* your functions will be passed here later! */ ], }); ``` Make a new directory next to your `app` directory (for example, `src/inngest`) where you'll define your Inngest functions and the client. In the `/src/inngest` directory create an Inngest client: ```ts {{ filename: "src/inngest/client.ts" }} // Create a client to send and receive events inngest = new Inngest({ id: "my-app" }); ``` Next, you will set up a route handler for the `/api/inngest` route. To do so, create a file inside your `app` directory (for example, at `src/app/api/inngest/route.ts`) with the following code: ```ts {{ filename: "src/app/api/inngest/route.ts" }} // Create an API that serves zero functions { GET, POST, PUT } = serve({ client: inngest, functions: [ /* your functions will be passed here later! */ ], }); ``` ## 4. Write your first Inngest function In this step, you will write your first reliable serverless 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!". ### Define the function To define the function, use the [`createFunction`](/docs/reference/functions/create) method on the Inngest client.
Learn more: What is `createFunction` method? The `createFunction` method takes three objects as 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 can also specify [additional options](/docs/reference/functions/create#configuration) such as `concurrency`, `rateLimit`, `retries`, or `batchEvents`, and others. - **Trigger**: `event` is the name of the event that triggers your function. Alternatively, you can use `cron` to specify a schedule to trigger this function. Learn more about triggers [here](/docs/features/events-triggers). - **Handler**: The function that is called when the `event` is received. The `event` payload is passed as an argument. Arguments include `step` to define durable steps within your handler and [additional arguments](/docs/reference/functions/create#handler) include logging helpers and other data.
Add the following code to the `./pages/api/inngest.ts` file: ```ts // Step 2 code... helloWorld = inngest.createFunction( { id: "hello-world" }, { event: "test/hello.world" }, async ({ event, step }) => { await step.sleep("wait-a-moment", "1s"); return { message: `Hello ${event.data.email}!` }; }, ); ``` Inside your `src/inngest` directory create a new file called `functions.ts` where you will define Inngest functions. Add the following code: ```ts {{ filename: "src/inngest/functions.ts" }} helloWorld = inngest.createFunction( { id: "hello-world" }, { event: "test/hello.world" }, async ({ event, step }) => { await step.sleep("wait-a-moment", "1s"); return { message: `Hello ${event.data.email}!` }; }, ); ``` ### Add the function to `serve()` Next, add your Inngest function to the `serve()` handler. ```ts export default serve({ client: inngest, functions: [ helloWorld, // <-- This is where you'll always add your new functions ], }); ``` Next, import your Inngest function in the routes handler (`src/app/api/inngest/route.ts`) and add it to the `serve` handler so Inngest can invoke it via HTTP: ```ts {{ filename: "src/app/api/inngest/route.ts" }} { GET, POST, PUT } = serve({ client: inngest, functions: [ helloWorld, // <-- This is where you'll always add all your functions ], }); ``` 👉 Note that you can import [`serve()`](/docs/reference/serve) for other frameworks and the rest of the code, in fact, remains the same — only the import statement changes (instead of `inngest/next`, it would be `inngest/astro`, `inngest/remix`, and so on). Now, it's time to run your function! ## 5. Trigger your function from the Inngest Dev Server UI Inngest is powered by events.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 Next.js app 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 Next.js app: from the “hello” Next.js API function. To do so, create a new API handler in the `./pages/api/hello.ts``src/app/api/hello/route.ts` file: ```ts {{ filename: "pages/api/hello.ts" }} // Opt out of caching; every request should send a new event dynamic = "force-dynamic"; // Create a simple async Next.js API route handler export default async function handler( req: NextApiRequest, res: NextApiResponse, ) { // Send your event payload to Inngest await inngest.send({ name: "test/hello.world", data: { email: "testUser@example.com", }, }); res.status(200).json({ message: "Event sent!" }); } ``` ```ts {{ filename: "src/app/api/hello/route.ts" }} // Import our client // Opt out of caching; every request should send a new event dynamic = "force-dynamic"; // Create a simple async Next.js API route handler export async function GET() { // Send your event payload to Inngest await inngest.send({ name: "test/hello.world", data: { email: "testUser@example.com", }, }); return NextResponse.json({ message: "Event sent!" }); } ``` 👉 Note that we use [`"force-dynamic"`](https://nextjs.org/docs/app/building-your-application/caching) to ensure we always send a new event on every request. In most situations, you'll probably want to send an event during a `POST` request so that you don't need this config option. 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 Next.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/functions). - About [Inngest steps](/docs/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/deploy).
# Node.js Quick Start Source: https://www.inngest.com/docs/getting-started/nodejs-quick-start description = `Get started with Inngest in this ten-minute JavaScript tutorial` 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: {/* TODO - I'd like to make these easily cloneable from
Instructions for creating a new Next.js project Run the following command in your terminal to create a new Next.js project: ```shell npx create-next-app@latest --ts --eslint --tailwind --no-src-dir --no-app --import-alias='@/*' inngest-guide ``` ```shell npx create-next-app@latest --ts --eslint --tailwind --src-dir --app --import-alias='@/*' inngest-guide ```
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: ```shell {{ title: "tsx" }} npx tsx watch ./index.ts # replace with your own main entrypoint file ``` ```shell {{ title: "nodemon" }} nodemon ./index.js # replace with your own main entrypoint file ``` 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 events and function runs. This tutorial assumes that your server will be running on port `3000`; change this to match your port if you use another. ```shell {{ title: "npm" }} npx inngest-cli@latest dev -u http://localhost:3000/api/inngest ``` ```shell {{ title: "yarn" }} yarn dlx inngest-cli@latest dev -u http://localhost:3000/api/inngest ``` ```shell {{ title: "pnpm" }} pnpm dlx inngest-cli@latest dev -u http://localhost:3000/api/inngest ``` ```shell {{ title: "bun" }} bun add global inngest-cli@latest inngest-cli dev -u http://localhost:3000/api/inngest ```
You should see a similar output to the following: ```bash {{ language: 'js' }} $ npx inngest-cli@latest 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/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 Using your existing Express.js server, we'll set up Inngest using the provided `serve` handler which will "serve" Inngest functions. Here we'll assume this file is your entrypoint at `inngest.ts` and all import paths will be relative to that: ```ts {{ filename: "./index.ts" }} express(); // Important: ensure you add JSON middleware to process incoming JSON POST payloads. app.use(express.json()); // Set up the "/api/inngest" (recommended) routes with the serve handler app.use("/api/inngest", serve({ client: inngest, functions })); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); }); ``` Using your existing Fastify server, we'll set up Inngest using the provided Fastify plugin which will "serve" Inngest functions. Here we'll assume this file is your entrypoint at `inngest.ts` and all import paths will be relative to that: ```ts {{ filename: "./index.ts" }} Fastify({ logger: true, }); // This automatically adds the "/api/inngest" routes to your server fastify.register(fastifyPlugin, { client: inngest, functions, options: {}, }); // Start up the fastify server fastify.listen({ port: 3000 }, function (err, address) { if (err) { fastify.log.error(err); process.exit(1); } }); ``` 👉 Note that you can import a [`serve`](/docs/reference/serve) handler for other frameworks and the rest of the code remains the same. These adapters enable you to change your web framework without changing any Inngest function code (ex. instead of `inngest/express``inngest/fastify` it could be `inngest/next` or `inngest/hono`); ## 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/functions/create) method on the Inngest client.
Learn more: What is `createFunction` method? The `createFunction` method takes three objects as 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 can also specify [additional options](/docs/reference/functions/create#configuration) such as `concurrency`, `rateLimit`, `retries`, or `batchEvents`, and others. - **Trigger**: `event` is the name of the event that triggers your function. Alternatively, you can use `cron` to specify a schedule to trigger this function. Learn more about triggers [here](/docs/features/events-triggers). - **Handler**: The function that is called when the `event` is received. The `event` payload is passed as an argument. Arguments include `step` to define durable steps within your handler and [additional arguments](/docs/reference/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" }, { 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 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 handler on your server object: ```ts {{ filename: "./index.ts" }} app.use(express.json()); app.use("/api/inngest", serve({ client: inngest, functions })); // Create a new route app.get("/api/hello", async function (req, res, next) { await inngest.send({ name: "test/hello.world", data: { email: "testUser@example.com", }, }).catch(err => next(err)); res.json({ message: 'Event sent!' }); }); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); }); ``` ```ts {{ filename: "./index.ts" }} Fastify({ logger: true, }); fastify.register(fastifyPlugin, { client: inngest, functions, options: {} }); // Create a new route: fastify.get("/api/hello", async function (request, reply) { await inngest.send({ name: "test/hello.world", data: { email: "testUser@example.com", }, }); return { message: "Event sent!" }; }) fastify.listen({ port: 3000 }, function (err, address) { if (err) { fastify.log.error(err); process.exit(1); } }); ``` 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 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/functions). - About [Inngest steps](/docs/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/deploy).
# Python Quick Start Source: https://www.inngest.com/docs/getting-started/python-quick-start description = `Get started with Inngest in this ten-minute Python tutorial` {/* This is a duplicate of /docs/reference/python/overview/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.9 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, step: inngest.Step) -> 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: ```sh {{ title: "npx (npm)" }} npx inngest-cli@latest dev -u http://127.0.0.1:8000/api/inngest --no-discovery ``` ```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] # Background jobs Source: https://www.inngest.com/docs/guides/background-jobs description = `Define background jobs in just a few lines of code.` 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/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/functions/step-run) method. Every [Inngest step](/docs/steps) is automatically retried upon failure. 3. We pause the execution of the function until a specific date using [`step.sleepUntil()`](/docs/reference/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" }, { 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/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 import ( "time" "github.com/inngest/inngest-go" "github.com/inngest/inngest-go/step" ) inngestgo.CreateFunction( inngest.FunctionOpts{ ID: "send-signup-email", }, inngest.TriggerEvent("app/user.created"), func(ctx *inngest.Context) error { _, err := step.Run("send-the-user-a-signup-email", func(ctx *inngest.StepContext) (any, error) { return nil, sesclient.SendEmail(&ses.SendEmailInput{ To: ctx.Event.Data["user_email"].(string), Subject: "Welcome to Inngest!", Message: "...", }) }) if err != nil { return err, nil } step.Sleep("wait-for-the-future", 4 * time.Hour) _, err = step.Run("do-some-work-in-the-future", func(ctx *inngest.StepContext) error { // Code here runs in the future automatically. return nil, nil }) return err, nil }, ) ``` ### 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 _, err := inngestgo.Send(context.Background(), inngestgo.Event{ Name: "app/user.created", // This matches the event used in `createFunction` Data: map[string]interface{}{ "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](/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/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, step: inngest.Step): async def send_email(): await sesclient.send_email( to=ctx.event.data["user_email"], subject="Welcome to Inngest!", message="..." ) await step.run("send-the-user-a-signup-email", send_email) await 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 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: Handle high load by processing events in batches. Ideal for bulk operations.' # Batching 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 }, }, { 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), }; }); await step.run("record-data-to-db", async () => { return db.bulkWrite(attrs); }); return { success: true, recorded: result.length }; } ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( &inngestgo.FunctionOpts{ ID: "record-api-calls", BatchEvents: &inngest.EventBatchConfig{ MaxSize: 100, Timeout: "5s", Key: "event.data.user_id", // Optional: batch events by user ID }, }, inngestgo.EventTrigger("log/api.call"), func(ctx context.Context, events []*inngestgo.Event, step inngestgo.StepFunction) (any, error) { // NOTE: Use the events argument, which is an array of event payloads 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.Ts), } } var result []interface{} _, err := step.Run(ctx, "record-data-to-db", func(ctx context.Context) (interface{}, error) { return nil, db.BulkWrite(attrs) }) if err != nil { return err, nil } return nil, map[string]interface{}{ "success": true, "recorded": len(result), } }, ) ``` ```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" ), ) async def record_api_calls(ctx: inngest.Context, step: inngest.Step): # 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) } for evt in ctx.events ] async def record_data(): return await db.bulk_write(attrs) result = await 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. 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. ## 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. ## Combining with other flow control methods Batching does not work with all other flow control features. You _can_ combine batching with simple [concurrency](/docs/guides/concurrency) limits, but will not work correctly with the `key` configuration option. 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/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: Learn how to cancel long running functions with our API.' # Bulk Cancellation {/* 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/functions/step-sleep), [`step.sleepUntil`](/docs/reference/functions/step-sleep-until), or [`step.waitForEvent`](/docs/reference/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 flexiblity 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/docs/inngest-api). 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/docs/inngest-api). # Handling Clerk webhook events Source: https://www.inngest.com/docs/guides/clerk-webhook-events Description: Set up Clerk webhooks with Inngest and use Clerk events within Inngest functions.' # Handling Clerk webhook events ![Clerk logo and graphic showing Clerk webhook events](/assets/docs/guides/clerk-webhook-events/featured-image.png) Third party authentication providers like [Clerk](https://clerk.com/) are a fantastic way to add auth, user management, and security features to your application. They also provide drop-in components that can get your auth set up quickly. However, with an external source of truth for auth, you'll often need to: * Sync data from Clerk with your database, * Provision resources for new accounts, or * Trigger other work from events (such as emails). This page offers a guide on setting up a Clerk webhook with Inngest and using Clerk events within Inngest functions. ## Setting up the Clerk webhook Clerk enables [sending events to a webhook endpoint](https://clerk.com/docs/integrations/webhooks/overview) when certain events occur. Inngest's [webhook endpoints](/docs/platform/webhooks) allow you to receive these events within your account just like [events that you send](/docs/events) from your own application. To set up the Clerk webhook, open the Clerk dashboard and navigate to the Webhooks" page. Next, select the "Add Endpoint" button. ![The Webhooks page in the Clerk Dashboard. A red arrow points to the button for Add Endpoint.](/assets/docs/guides/clerk-webhook-events/webhook-page.webp) On the next page, select the "Transformation" template tab and the Inngest template, then click on the "Connect to Inngest" button. ![The Webhooks page in the Clerk Dashboard showing the Inngest transformation template. Red arrows point to the Transformation Template tab, the Inngest template, and the Connect to Inngest button.](/assets/docs/guides/clerk-webhook-events/webhook-transformation-template.webp) A popup window will appear to complete the setup. Select "Approve" to create the webhook. ![The Inngest permissions popup window showing the Approve button.](/assets/docs/guides/clerk-webhook-events/inngest-permissions-dialog.png) After the popup window disappears, the Webhooks page will now display "Connected" with the webhook URL underneath. There is one more step to complete setup. ![The Webhooks page in the Clerk Dashboard showing a connected Inngest account. A red arrow points to the Connected button.](/assets/docs/guides/clerk-webhook-events/webhook-endpoint-connected.webp) To complete the setup, scroll down and select "Create". ![The Webhooks page in the Clerk Dashboard showing the end of the page to create a new endpoint. A red arrow points to the Create button.](/assets/docs/guides/clerk-webhook-events/webhook-create.webp) You'll be redirected to the new endpoint. In your Inngest dashboard, you will see a new webhook created in your account's [production environment](https://app.inngest.com/env/production/manage/webhooks). # Often, one key part of integrating with an auth provider like [Clerk](https://clerk.com/) is handling asynchronous updates with a webhook. Suppose you need to write a function which will insert a new user into the database which will be triggered whenever `clerk/user.created` event occurs. You would use the `inngest.createFunction()` method, like in the example below: ```ts {{ filename: "src/inngest/sync-user.ts" }} inngest.createFunction( { id: 'sync-user-from-clerk' }, { event: 'clerk/user.created' }, async ({ event }) => { // The event payload's data will be the Clerk User json object event.data; user; user.email_addresses.find(e => e.id === user.primary_email_address_id ).email; await database.users.insert({ id, email, first_name, last_name }); } ) ``` The `event` object contains all of the relevant data for the event. The `event.data` will match the `data` object from the standard Clerk webhook [payload structure](https://clerk.com/docs/integrations/webhooks/overview#payload-structure). With this `clerk/user.created` event, the `event.data` will be a Clerk User json object. As you can see, you can choose which events you want to handle with each function. You might write a separate function for `clerk/user.updated` and `clerk/user.deleted` handling the entire lifecycle end to end. Note that multiple functions can also listen to the same event. This pattern is called “[fan-out](/docs/guides/fan-out-jobs).” ## Creating a function to send a welcome email Often, applications need to perform additional tasks when a new user is created, like send a welcome email with tips and useful information. While it is possible to add this logic at the end of your sync function as seen in the [previous section](/docs/guides/clerk-webhook-events#creating-a-function-to-sync-a-new-user-to-a-database), it’s better to decouple unrelated tasks into different functions so issues with one task do not affect the other ones. For example, if your email fails to send, it should not affect starting a trial for that user in Stripe. You can make use of the fact that with Inngest, each function has [automatic retries](/docs/functions/retries), so only the code that has issues is re-run. The code below creates another function using the same `clerk/user.created` event and adds the logic to send the welcome email: ```ts {{ filename: "src/inngest/send-welcome-email.ts" }} inngest.createFunction( { id: 'send-welcome-email' }, { event: 'clerk/user.created' }, async ({ event }) => { event.data; user; user.email_addresses.find(e => e.id === user.primary_email_address_id ).email; await emails.sendWelcomeEmail({ email, first_name }); } ) ``` Now, you have a function that utilizes the same Clerk webhook event for another purpose. Clerk webhook events can be used for all sorts of application lifecycle use cases. For example, adding users to a marketing email list, starting a Stripe trial, or provisioning new account resources. ### Sending a delayed follow-up email To send a follow-up email, you can use the [`step.run()`](/docs/reference/functions/step-run). This method will encapsulate specific code that will be automatically retried ensuring that issues with one part of your function don't force the entire function to re-run. Additionally, you will extend the functionality with [`step.sleep()`](/docs/reference/functions/step-sleep). The code below sends a welcome email, then uses `step.sleep()` to wait for three days before sending another email offering a free trial: ```ts {{ filename: "src/inngest/send-welcome-email.ts" }} inngest.createFunction( { id: 'send-welcome-email' }, { event: 'clerk/user.created' }, async ({ event, step }) => { event.data; user; user.email_addresses.find(e => e.id === user.primary_email_address_id ).email; // Wrapping each distinct task in step.run() ensures that each // will be retried automatically on error and will not be re-run await step.run('welcome-email', async () => { await emails.sendWelcomeEmail({ email, first_name }) }); // wait 3 days before second email await step.sleep('wait-3-days', '3 days'); await step.run('trial-offer-email', async () => { await emails.sendTrialOfferEmail({ email, first_name }) }); } ) ``` ## Next steps To continue learning about how to get the most out of Clerk webhook events, check out the following: * Platform guide: [Consuming webhooks](/docs/platform/webhooks) * Guide: [Fan-out (one-to-many)](/docs/guides/fan-out-jobs) * Guide: [Parallel steps](/docs/guides/step-parallelism) * Reference: [`step.run()`](/docs/reference/functions/step-run) # Concurrency management Source: https://www.inngest.com/docs/guides/concurrency 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. {/* 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). 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 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, }, { event: "ai/summary.requested" }, async ({ event, step }) => { // Your function handler here } ); ``` ```go inngest.CreateFunction( &inngestgo.FunctionOpts{ Name: "generate-ai-summary", Concurrency: []inngest.Concurrency{ { Limit: 10, } }, }, inngestgo.EventTrigger("ai/summary.requested", nil), func(ctx context.Context, input inngestgo.Input) (any, error) { // Your function handler here return nil, nil }, ) ``` ```python @inngest.create_function( fn_id="generate-ai-summary", concurrency=[ inngest.Concurrency( limit=10, ) ] ) async def first_function(event, step): # Your function handler here pass ``` ### 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, }, ], }, { event: "ai/summary.requested" }, async ({ event, step }) => { } ); ``` ```go inngest.CreateFunction( &inngestgo.FunctionOpts{ Name: "generate-ai-summary", Concurrency: []inngest.Concurrency{ { Scope: "fn", Key: "event.data.account_id", Limit: 10, } }, }, inngestgo.EventTrigger("ai/summary.requested", nil), func(ctx context.Context, input inngestgo.Input) (any, error) { // Your function handler here return nil, nil }, ) ``` ```python @inngest.create_function( fn_id="another-function", concurrency=[ inngest.Concurrency( scope="fn", key="event.data.account_id", limit=10, ) ] ) async def first_function(event, step): # Your function handler here pass ``` 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. ### 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, }, ], }, { event: "ai/summary.requested" }, async ({ event, step }) => { } ); ``` ```go inngest.CreateFunction( &inngestgo.FunctionOpts{ Name: "generate-ai-summary", Concurrency: []inngest.Concurrency{ { Scope: "account", Key: `"openai"`, Limit: 60, } }, }, inngestgo.EventTrigger("ai/summary.requested", nil), func(ctx context.Context, input inngestgo.Input) (any, error) { // Your function handler here return nil, nil }, ) ``` ```python @inngest.create_function( fn_id="another-function", concurrency=[ inngest.Concurrency( scope="account", key='"openai"', limit=60, ) ] ) async def first_function(event, step): # Your function handler here pass ``` ### 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 execution 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, }, ], }, { event: "ai/summary.requested" }, async ({ event, step }) => { } ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( &inngestgo.FunctionOpts{ Name: "unique-function-id", Concurrency: []inngest.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 execution 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, }, }, }, inngestgo.EventTrigger("ai/summary.requested", nil), func(ctx context.Context, input inngestgo.Input) (any, error) { // Function implementation here return nil, nil }, ) ``` ```py {{ title: "Python" }} @inngest.create_function( fn_id="unique-function-id", concurrency=[ inngest.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, ), inngest.Concurrency( # Create another virtual concurrency queue for this function only. This # limits all accounts to a single execution 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, ), ], ) async def handle_ai_summary(event, step): # Function implementation here pass ``` 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. 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. Steps that are asynchronous actions, `step.sleep`, `step.sleepUntil`, `step.waitForEvent`, and `step.invoke` do not contribute to the concurrency limit. **Queues are ordered from oldest to newest jobs ([FIFO](https://en.wikipedia.org/wiki/FIFO))** across the same function. Ordering amongst different functions is not guaranteed. This means that within a specific function, Inngest prioritizes finishing older functions above starting newer functions - even if the older functions continue to schedule new steps to run. Different functions, however, compete for capacity, with runs on the most backlogged function much more likely (but not guaranteed) to be scheduled first. Some 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, }, }, { event: "ai/summary.requested" }, async ({ event, step }) => { } ); inngest.createFunction( { id: "func-b", concurrency: { scope: "account", key: `"openai"`, limit: 50, }, }, { event: "ai/summary.requested" }, async ({ event, step }) => { } ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( &inngestgo.FunctionConfig{ Name: "func-a", Concurrency: []inngest.Concurrency{ { Scope: "account", Key: `"openai"`, Limit: 5, } }, }, inngestgo.EventTrigger("ai/summary.requested"), func(ctx context.Context, event *inngestgo.Event, step inngestgo.StepFunction) (any, error) { return nil, nil }, ) inngestgo.CreateFunction( &inngestgo.FunctionConfig{ Name: "func-b", Concurrency: []inngest.Concurrency{ { Scope: "account", Key: `"openai"`, Limit: 50, } }, }, inngestgo.EventTrigger("ai/summary.requested"), func(ctx context.Context, event *inngestgo.Event, step inngestgo.StepFunction) (any, error) { return nil, nil }, ) ``` ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="func-a", trigger=inngest.TriggerEvent(event="ai/summary.requested"), concurrency=[ inngest.Concurrency( scope="account", key='"openai"', limit=5 ) ] ) async def func_a(ctx: inngest.Context, step: inngest.Step): pass @inngest_client.create_function( fn_id="func-b", trigger=inngest.TriggerEvent(event="ai/summary.requested"), concurrency=[ inngest.Concurrency( scope="account", key='"openai"', limit=50 ) ] ) async def func_b(ctx: inngest.Context, step: inngest.Step): pass ``` 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 amongst the same function is guaranteed (with the exception of retries) - Ordering amongst different functions is not guaranteed. Functions compete with each other randomly to be scheduled. ## 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 functions running at the same exact time, but no given customer can have two functions running 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 }, }, { 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" }} inngestgo.CreateFunction( &inngestgo.FunctionConfig{ Name: "Process customer csv import", ID: "process-customer-csv-import", Concurrency: []inngest.Concurrency{ { Limit: 1, Key: `event.data.customerId`, // You can use any piece of data from the event payload }, }, }, inngestgo.EventTrigger("csv/file.uploaded"), func(ctx context.Context, event *inngestgo.Event, step inngestgo.StepFunction) (any, error) { _, err := step.Run(ctx, "process-file", func(ctx context.Context) (any, error) { file, err := bucket.Fetch(event.Data.FileURI) if err != nil { return nil, err } // ... return nil, nil }) if err != nil { return err, nil } return nil, nil }, ) ``` ```py {{ title: "Python" }} @inngest_client.create_function( fn_id="process-customer-csv-import", name="Process customer csv import", trigger=inngest.TriggerEvent(event="csv/file.uploaded"), concurrency=[ inngest.Concurrency( limit=1, key="event.data.customerId" # You can use any piece of data from the event payload ) ] ) async def process_csv_import(ctx: inngest.Context, step: inngest.Step): async def process_file(): file = await bucket.fetch(ctx.event.data.file_uri) # ... await step.run("process-file", process_file) return {"message": "success"} ``` ## 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: 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.' # Debounce 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" }} export default inngest.createFunction( { id: "handle-webhook", debounce: { key: "event.data.account_id", period: "5m", timeout: "10m", }, }, { event: "intercom/company.updated" }, async ({ event, step }) => { // This function will only be scheduled 5 minutes after events are no longer received with the same // `event.data.account_id` field. // // `event` will be the last event in the series received. } ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( &inngestgo.FunctionOpts{ ID: "handle-webhook", Debounce: &inngestgo.Debounce{ Key: "event.data.account_id", Period: "5m", Timeout: "10m", }, }, inngestgo.EventTrigger("intercom/company.updated", nil), func(ctx context.Context, input inngestgo.Input) (any, error) { // This function will only be scheduled 5 minutes after events are no longer received with the same // `event.data.account_id` field. // // `event` will be the last event in the series received. return nil, nil }, ) ``` ```py {{ title: "Python" }} @inngest.create_function( fn_id="handle-webhook", debounce=inngest.Debounce( key="event.data.account_id", period=datetime.timedelta(minutes=5), timeout=datetime.timedelta(minutes=10) ), trigger=inngest.Trigger(event="intercom/company.updated") ) async def handle_webhook(ctx: inngest.Context): // This function will only be scheduled 5 minutes after events are no longer received with the same // `event.data.account_id` field. // // `event` will be the last event in the series received. pass ``` ## * `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/functions/debounce) * [Python SDK Reference](/docs/reference/python/functions/create#configuration) # Delayed Functions Source: https://www.inngest.com/docs/guides/delayed-functions You can easily enqueue jobs in the future with Inngest. Inngest offers two ways to run jobs in the future: delaying jobs for a specific amount of time (up to a year, and for free plan up to seven days), or running code at a specific date and time. There are some benefits to enqueuing jobs using Inngest: - It works across any provider or platform - Delaying jobs is durable, and works across server restarts, serverless functions, and redeploys - You can enqueue jobs 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. ## Delaying jobs You can delay jobs using the [`step.sleep()`](/docs/reference/functions/step-sleep) method: ```ts new Inngest({ id: "signup-flow" }); fn = inngest.createFunction( { id: "send-signup-email" }, { 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/functions/step-sleep). ## Running at specific times You can run jobs at a specific time using the [`step.sleepUntil()`](/docs/reference/functions/step-sleep-until) method: ```ts new Inngest({ id: "signup-flow" }); fn = inngest.createFunction( { id: "send-signup-email" }, { 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/functions/step-sleep-until). You can delay jobs using the [`step.Sleep()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Sleep) method: ```go import ( "time" "github.com/inngest/inngest-go" "github.com/inngest/inngest-go/step" ) inngestgo.CreateFunction( inngest.FunctionOpts{ ID: "send-signup-email", }, inngest.TriggerEvent("app/user.created"), func(ctx *inngest.Context) error { // business logic step.Sleep("wait-for-the-future", 4 * time.Hour) _, err = step.Run("do-some-work-in-the-future", func(ctx *inngest.StepContext) (any, error) { // Code here runs in the future automatically. return nil, nil }) return err, nil }, ) ``` For more information on `step.sleep()` read [the reference](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Sleep). You can delay jobs 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, step: inngest.Step): await step.sleep("wait-for-the-future", timedelta(hours=4)) async def future_work(): # Code here runs in the future automatically pass await step.run("do-some-work-in-the-future", future_work) ``` For more information on `step.sleep()` read [the reference](/docs/reference/functions/step-sleep). ## Running at specific times You can run jobs at 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, step: inngest.Step): async def send_email(): await sesclient.send_email( to=ctx.event.data["user_email"], subject="Welcome to Inngest!", message="..." ) await step.run("send-the-user-a-signup-email", send_email) await 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 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). ## How it works {/* TODO - Revisit this section after we write a How Inngest Works explainer */} In both methods, **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. # Development with Docker Source: https://www.inngest.com/docs/guides/development-with-docker Description: Learn how to develop locally with Inngest and Docker. Inngest provides a Docker image that you can use to run the Inngest Dev Server within a container. This is useful when running Inngest locally or in a CI/CD environment. This guide will explain how to run Inngest using Docker or [Docker Compose](#docker-compose). ## Docker image The [`inngest/inngest`](https://hub.docker.com/r/inngest/inngest) image is available on Docker Hub. Regular updates are made to this image, so we recommend pulling the latest version. You can find the latest version release on [our Github repo](https://github.com/inngest/inngest/releases). ```bash docker pull inngest/inngest ``` ## Standalone Docker container Docker can be useful for running the Inngest Dev Server in a standalone container. This is useful if you do not want to use the `npx inngest-cli@latest` method to run the Dev Server. To run the Inngest container, you'll need to: 1. Expose the Dev Server port (default is `8288`). 2. Use the `inngest dev` command with the `-u` flag to specify the URL where Inngest can find your app. In this example command, our app is running on the host machine on port `3000`. We use the `host.docker.internal` hostname to connect to the host machine from within the Docker container. For ease of reading, the command is broken up into multiple lines. ```bash docker run -p 8288:8288 \ inngest/inngest \ inngest dev -u http://host.docker.internal:3000/api/inngest ``` You will then be able to access the Inngest Dev Server on your host machine at `http://localhost:8288` or whatever hostname you have configured. You may need to adjust the hostname for your app if you are using a different Docker network setup. If you decide to run the Dev Server on another port, you will need to set the `INNGEST_BASE_URL` environment variable in your app to point to the correct port. This value defaults to `http://localhost:8288`. ## Docker Compose If you're using [Docker Compose](https://docs.docker.com/compose/) to run your services locally, you can easily add Inngest to your local environment. Here's an example `docker-compose.yml` file that includes Inngest: ```yaml {{ filename: "docker-compose.yaml" }} services: app: build: ./app environment: - INNGEST_DEV=1 - INNGEST_BASE_URL=http://inngest:8288 ports: - '3000:3000' inngest: image: inngest/inngest:v0.27.0 command: 'inngest dev -u http://app:3000/api/inngest' ports: - '8288:8288' ``` In this example, we have two services: `app` and `inngest`. The `app` service is your application, and the `inngest` service is the Inngest Dev Server. There are a few key configurations to note: * The `INNGEST_DEV=1` environment variable tells the Inngest SDK it should connect to the Dev Server*. * The `INNGEST_BASE_URL=http://inngest:8288` environment variable tells the Inngest SDK where the Dev Server is running. In our example, the `inngest` service is running on port `8288` (the default Dev Server port). * The `command: 'inngest dev -u http://app:3000/api/inngest'` command tells the Dev Server where to find your app within the Docker network. In this example, the `app` service is running on port `3000`. * The `ports` configuration exposes the Dev Server on port `8288` so you can view this on your host machine in the browser. \* - The `INNGEST_DEV` environment variable was added to the TypeScript SDK in version 3.14. Prior to this version, you can set `NODE_ENV=development` to force the SDK to connect to the Dev Server. ## Further reference * [Local development](/docs/local-development) * [Dev Server source code on GitHub](https://github.com/inngest/inngest) * [`inngest/inngest` Docker image on Docker Hub](https://hub.docker.com/r/inngest/inngest) * [TypeScript SDK Environment variable reference](/docs/sdk/environment-variables) * [Python SDK Environment variable reference](/docs/reference/python/overview/env-vars) # Errors & Retries Source: https://www.inngest.com/docs/guides/error-handling Description: Learn how to handle errors and failures in your Inngest functions.' # Errors & Retries Inngest Functions are designed to handle errors or exceptions gracefully and will automatically retry after an error or exception. This adds an immediate layer of durability to your code, ensuring it survives transient issues like network timeouts, outages, or database locks. Inngest Functions come with: } href={'/docs/features/inngest-functions/error-retries/retries'}> Configurable with a custom retry policies to suit your specific use case. } href={'/docs/features/inngest-functions/error-retries/failure-handlers'}> Utilize callbacks to handle all failing retries. } href={'/docs/features/inngest-functions/error-retries/rollbacks'}> Each step within a function can have its own retry logic and be handled individually. # Inngest helps you handle both **errors** and **failures**, which are defined differently. An **error** causes a step to retry. Exhausting all retry attempts will cause that step to **fail**, which means the step will never be attempted again this run. A **failed** step can be handled with native language features such as `try`/`catch`, but unhandled errors will cause the function to **fail**, meaning the run is marked as "Failed" in the Inngest UI and all future executions are cancelled. See how to handle step failure by [performing rollbacks](/docs/features/inngest-functions/error-retries/rollbacks). ## Failures, Retries and Idempotency Re-running a step upon error requires its code to be idempotent, which means that running the same code multiple times won't have any side effect. For example, a step inserting a new user to the database is not idempotent while a step [upserting a user](https://www.cockroachlabs.com/blog/sql-upsert/) is. Learn how to write idempotent steps that can be retried safely by reading ["Handling idempotency"](/docs/guides/handling-idempotency). # Fan-out (one-to-many) Source: https://www.inngest.com/docs/guides/fan-out-jobs Description: How to use the fan-out pattern with Inngest to trigger multiple functions from a single event.' # Fan-out (one-to-many) 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. # 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 of 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' }, { 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' }, { 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" }} func main() { // Initialize your HTTP server mux := http.NewServeMux() // Handle signup route mux.HandleFunc("/signup", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse request body - in a real app you'd validate the input var user struct { Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&user); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Send event to Inngest _, err := inngestgo.Send(r.Context(), inngestgo.Event{ Name: "app/user.signup", Data: map[string]interface{}{ "user": map[string]interface{}{ "email": user.email, }, }, }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) }) // Start the server log.Fatal(http.ListenAndServe(":8080", mux)) } ``` 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" }} import ( "github.com/inngest/inngest-go" "github.com/inngest/inngest-go/step" ) func sendWelcomeEmail() *inngest.Function { return inngestgo.CreateFunction( inngest.FunctionOpts{ ID: "send-welcome-email", }, inngest.TriggerEvent("app/user.signup"), func(ctx *inngest.Context) error { _, err := step.Run("send-email", func(ctx *inngest.StepContext) (any, error) { return sendEmail(&SendEmailInput{ Email: ctx.Event.Data["user"].(map[string]interface{})["email"].(string), Template: "welcome", }) }) return err, nil }, ) } func startStripeTrial() *inngest.Function { return inngestgo.CreateFunction( inngest.FunctionOpts{ ID: "start-stripe-trial", }, inngest.TriggerEvent("app/user.signup"), func(ctx *inngest.Context) (any, error) { customer, err := step.Run("create-customer", func(ctx *inngest.StepContext) (any, error) { return nil, stripe.Customers.Create(&stripe.CustomerParams{ Email: ctx.Event.Data["user"].(map[string]interface{})["email"].(string), }) }) if err != nil { return err, nil } _, err = step.Run("create-subscription", func(ctx *inngest.StepContext) (any, error) { return nil, stripe.Subscriptions.Create(&stripe.SubscriptionParams{ Customer: customer.ID, Items: []*stripe.SubscriptionItemsParams{{Price: "price_1MowQULkdIwHu7ixraBm864M"}}, TrialPeriodDays: 14, }) }) return err, nil }, ) } ``` 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) 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: ```ts {{ title: "Flask route" }} from flask import Flask, request, redirect from src.inngest.client import inngest_client app = Flask(__name__) @app.route('/signup', methods=['POST']) async def signup(): // NOTE - this code is simplified for the example: data = await request.get_json() email = data['email'] password = data['password'] user = await create_user(email=email, password=password) await create_session(user.id) // Send an event to Inngest await inngest_client.send( name="app/user.signup", data={ "user": { "id": user.id, "email": user.email } } ) return redirect('https://myapp.com/dashboard') ``` ```ts {{ title: "FastAPI route" }} from fastapi import FastAPI, Request, Response from fastapi.responses import RedirectResponse from src.inngest.client import inngest_client app = FastAPI() @app.post("/signup") async def signup(request: Request): # NOTE - this code is simplified for the example: data = await request.json() email = data['email'] password = data['password'] user = await create_user(email=email, password=password) await create_session(user.id) # Send an event to Inngest await inngest_client.send( name="app/user.signup", data={ "user": { "id": user.id, "email": user.email } } ) return RedirectResponse(url="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. ```py {{ filename: "inngest/functions.py" }} @inngest_client.create_function( fn_id="send-welcome-email", trigger=inngest.TriggerEvent(event="app/user.signup"), ) async def send_welcome_email( ctx: inngest.Context, step: inngest.Step, ) -> None: await step.run("send-email", lambda: send_email( email=ctx.event.data["user"]["email"], template="welcome" )) @inngest_client.create_function( fn_id="start-stripe-trial", trigger=inngest.TriggerEvent(event="app/user.signup"), ) async def start_stripe_trial( ctx: inngest.Context, step: inngest.Step, ) -> None: customer = await step.run("create-customer", lambda: stripe.Customer.create( email=ctx.event.data["user"]["email"] )) await step.run("create-subscription", lambda: stripe.Subscription.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 [TypeScript](/docs/reference/typescript) 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: Learn how to manage how functions are executed with flow control.'; # Flow Control 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/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 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/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" await inngest.Send(context.Background(), inngest.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/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 }, { trigger: '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/functions/rate-limit) - Limit the number of function executions per period of time * [`debounce`](/docs/reference/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' }, { 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` }, { event: 'ai/generation.requested' }, async ({ event, step }) => { // Track the request } ) ```
# Guides Source: https://www.inngest.com/docs/guides/index hidePageSidebar = true; Learn how to build with Inngest: ## Patterns # Instrumenting GraphQL Source: https://www.inngest.com/docs/guides/instrumenting-graphql {/* 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 denlylists, 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" }, { 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/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 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 ## 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" }, { 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" }, { 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" }, { 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 ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) // Some function we'll call inngest.CreateFunction( inngest.FunctionOpts{ID: "compute-square"}, inngest.EventTrigger("calculate/square"), ComputeSquare, ) func ComputeSquare(ctx *inngest.Context) error { data := struct { Number int `json:"number"` }{} if err := ctx.Event.Data.Decode(&data); err != nil { return err } return ctx.Return(map[string]int{ "result": data.Number * data.Number, }) } // In this function, we'll call ComputeSquare inngest.CreateFunction( inngest.FunctionOpts{ID: "main-function"}, inngest.EventTrigger("main/event"), MainFunction, ) func MainFunction(ctx *inngest.Context) error { square, err := step.Invoke("compute-square-value", &inngest.InvokeOpts{ Function: "compute-square", Data: map[string]interface{}{ "number": 4, }, }) if err != nil { return err } result := square.Data["result"].(int) return ctx.Return(fmt.Sprintf("Square of 4 is %d.", result)) } ``` 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, step: inngest.Step): 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, step: inngest.Step): square = await 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 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 }) => { logger.info("something") // this can be run three times await step.run("fn", () => { logger.info("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. You can use it with the logger of your choice, or if absent, `logger` will default to use `console`. ```ts inngest.createFunction( { id: "my-awesome-function" }, { event: "func/awesome" }, async ({ event, step, logger }) => { logger.info("starting function", { metadataKey: "metadataValue" }); await step.run("do-something", () => { if (somethingBadHappens) logger.warn("something bad happened"); }); return { success: true, event }; } ); ``` The exported logger provides the following interface methods: ```ts export interface Logger { info(...args: any[]): void; warn(...args: any[]): void; error(...args: any[]): void; debug(...args: any[]): void; } ``` These are very typical interfaces and are also on the [RFC5424 guidelines](https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1), so most loggers you choose should work without issues. ## Using your preferred logger Running `console.log` is good for local development, but you probably want something more when running workloads in Production. The following is an example using [winston][winston] as the logger to be passed into Inngest functions. ```ts /// Assuming we're deploying to Vercel. /// Other providers likely have their own pre-defined environment variables you can use. process.env.VERCEL_ENV || "development"; { host: "http-intake.logs.datadoghq.com", path: `/api/v2/logs?dd-api-key=${process.env.DD_API_KEY}&ddsource=nextjs&service=inngest&ddtags=env:${env}`, ssl: true, }; winston.createLogger({ level: "info", exitOnError: false, format: winston.format.json(), transports: [ new winston.transports.Console(), new winston.transports.Http(ddTransportOps), ], }); // Pass `logger` to the Inngest client, and this winston logger will be accessible within functions inngest = new Inngest({ id: "my-awesome-app", logger: logger, // ... }); ``` ## How it works There is a built-in [logging middleware](/docs/reference/middleware/examples#logging) that provides a good default to work with. ### child logger If the logger library supports a child logger `.child()` implementation, the built-in middleware will utilize it to add function runtime metadata for you: - function name - event name - run ID ## Loggers supported The following is a list of loggers we're aware of that work, but is not an exhaustive list: - [Winston][winston] child logger support - [Pino](https://github.com/pinojs/pino) child logger support - [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) [winston]: https://github.com/winstonjs/winston # Multi-Step Functions Source: https://www.inngest.com/docs/guides/multi-step-functions description = `Build reliable workflows with event coordination and conditional execution using Inngest's multi-step functions.` hidePageSidebar = true; Use Inngest's multi-step functions to safely coordinate events, delay execution for hours (or up to a year), retry [individual steps](/docs/learn/inngest-steps), and conditionally run code based on the result of previous steps and incoming events. Critically, multi-step functions are written in code, not config, meaning you create readable, obvious functionality that's easy to maintain. ## Benefits of multi-step functions Creating functions that utilize multiple steps enable you to: - Running retriable blocks of code to maximum reliability. - Pausing execution and waiting for an event matching rules before continuing. - Pausing for an amount of time or until a specified time. This approach makes building reliable and distributed code simple. By wrapping asynchronous actions such as API calls in retriable blocks, we can ensure reliability when coordinating across many services. ## How to write a multi-step function Consider this simple [Inngest function](/docs/learn/inngest-functions) which sends a welcome email when a user signs up: ```ts new Inngest({ id: "my-app" }); export default inngest.createFunction( { id: "activation-email" }, { event: "app/user.created" }, async ({ event }) => { await sendEmail({ email: event.user.email, template: "welcome" }); } ); ``` This function comes with all of the benefits of Inngest: the code is reliable and retriable. If an error happens, you will recover the data. This works for a single-task functions. However, there is a new requirement: if a user hasn't created a post on our platform within 24 hours of signing up, we should send the user another email. Instead of adding more logic to the handler, we can convert this function into a multi-step one. ### 1. Convert to a step function First, let's convert this function into a multi-step function: - Add a `step` argument to the handler in the Inngest function. - Wrap `sendEmail()` call in a [`step.run()`](/docs/reference/functions/step-run) method. ```ts export default inngest.createFunction( { id: "activation-email" }, { event: "app/user.created" }, async ({ event, step }) => { await step.run("send-welcome-email", async () => { return await sendEmail({ email: event.user.email, template: "welcome" }); }); } ); ``` The main difference is that we've wrapped our `sendEmail()` call in a `step.run()` call. This is how we tell Inngest that this is an individual step in our function. This step can be retried independently, just like a single-step function would. ### 2. Add another step: wait for event Once the welcome email is sent, we want to wait at most 24 hours for our user to create a post. If they haven't created one by then, we want to send them a reminder email. Elsewhere in our app, an `app/post.created` event is sent whenever a user creates a new post. We could use it to trigger the second email. To do this, we can use the [`step.waitForEvent()`](/docs/reference/functions/step-wait-for-event) method. This tool will wait for a matching event to be fired, and then return the event data. If the event is not fired within the timeout, it will return `null`, which we can use to decide whether to send the reminder email. ```ts export default inngest.createFunction( { id: "activation-email" }, { event: "app/user.created" }, async ({ event, step }) => { await step.run("send-welcome-email", async () => { return await sendEmail({ email: event.user.email, template: "welcome" }); }); // Wait for an "app/post.created" event await step.waitForEvent("wait-for-post-creation", { event: "app/post.created", match: "data.user.id", // the field "data.user.id" must match timeout: "24h", // wait at most 24 hours }); } ); ``` Now we have a `postCreated` variable, which will be `null` if the user hasn't created a post within 24 hours, or the event data if they have. ### 3. Set conditional action Finally, we can use the `postCreated` variable to send the reminder email if the user hasn't created a post. Let's add another block of code with `step.run()`: ```ts export default inngest.createFunction( { id: "activation-email" }, { event: "app/user.created" }, async ({ event, step }) => { await step.run("send-welcome-email", async () => { return await sendEmail({ email: event.user.email, template: "welcome" }); }); // Wait for an "app/post.created" event await step.waitForEvent("wait-for-post-creation", { event: "app/post.created", match: "data.user.id", // the field "data.user.id" must match timeout: "24h", // wait at most 24 hours }); if (!postCreated) { // If no post was created, send a reminder email await step.run("send-reminder-email", async () => { return await sendEmail({ email: event.user.email, template: "reminder", }); }); } } ); ``` That's it! We've now written a multi-step function that will send a welcome email, and then send a reminder email if the user hasn't created a post within 24 hours. Most importantly, we had to write no config to do this. We can use all the power of JavaScript to write our functions and all the power of Inngest's tools to coordinate between events and steps. Consider this simple [Inngest function](/docs/learn/inngest-functions) which sends a welcome email when a user signs up: ```go import ( "github.com/inngest/inngest-go" ) inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "activation-email", }, inngestgo.EventTrigger("app/user.created"), func(ctx *inngestgo.Context) (any, error) { if err := sendEmail(ctx.Event.Data["user"].(map[string]interface{})["email"].(string), "welcome"); err != nil { return err } return nil, nil }, ) ``` This function comes with all of the benefits of Inngest: the code is reliable and retriable. If an error happens, you will recover the data. This works for a single-task functions. However, there is a new requirement: if a user hasn't created a post on our platform within 24 hours of signing up, we should send the user another email. Instead of adding more logic to the handler, we can convert this function into a multi-step one. ### 1. Convert to a step function First, let's convert this function into a multi-step function: - Add a `github.com/inngest/inngest-go/step` import - Wrap `sendEmail()` call in a [`step.Run()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Run) method. ```go import ( "github.com/inngest/inngest-go" "github.com/inngest/inngest-go/step" ) inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "activation-email", }, inngestgo.EventTrigger("app/user.created"), func(ctx *inngestgo.Context) (any, error) { _, err := step.Run("send-welcome-email", func() (any, error) { return nil, sendEmail(ctx.Event.Data["user"].(map[string]interface{})["email"].(string), "welcome") }) if err != nil { return err } return nil, nil }, ) ``` The main difference is that we've wrapped our `sendEmail()` call in a `step.run()` call. This is how we tell Inngest that this is an individual step in our function. This step can be retried independently, just like a single-step function would. ### 2. Add another step: wait for event Once the welcome email is sent, we want to wait at most 24 hours for our user to create a post. If they haven't created one by then, we want to send them a reminder email. Elsewhere in our app, an `app/post.created` event is sent whenever a user creates a new post. We could use it to trigger the second email. To do this, we can use the [`step.WaitForEvent()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#WaitForEvent) method. This tool will wait for a matching event to be fired, and then return the event data. If the event is not fired within the timeout, it will return `nil`, which we can use to decide whether to send the reminder email. ```go import ( "github.com/inngest/inngest-go" "github.com/inngest/inngest-go/step" ) inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "activation-email", }, inngestgo.EventTrigger("app/user.created"), func(ctx *inngestgo.Context) (any, error) { _, err := step.Run("send-welcome-email", func() (any, error) { return nil, sendEmail(ctx.Event.Data["user"].(map[string]interface{})["email"].(string), "welcome") }) if err != nil { return nil, err } // Wait for an "app/post.created" event postCreated, err := step.WaitForEvent("wait-for-post-creation", &step.WaitForEventOpts{ Event: "app/post.created", Match: "data.user.id", // the field "data.user.id" must match Timeout: "24h", // wait at most 24 hours }) if err != nil { return nil, err } return postCreated, nil }, ) ``` Now we have a `postCreated` variable, which will be `nil` if the user hasn't created a post within 24 hours, or the event data if they have. ### 3. Set conditional action Finally, we can use the `postCreated` variable to send the reminder email if the user hasn't created a post. Let's add another block of code with `step.Run()`: ```go import ( "github.com/inngest/inngest-go" "github.com/inngest/inngest-go/step" ) inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "activation-email", }, inngestgo.EventTrigger("app/user.created", nil), func(ctx context.Context, input inngestgo.Input) (any, error) { // Send welcome email _, err := step.Run("send-welcome-email", func() (any, error) { return nil, sendEmail(input.Event.Data["user"].(map[string]interface{})["email"].(string), "welcome") }) if err != nil { return nil, err } // Wait for post creation event postCreated, err := step.WaitForEvent("wait-for-post-creation", &step.WaitForEventOpts{ Event: "app/post.created", Match: "data.user.id", Timeout: "24h", }) if err != nil { return nil, err } // If no post was created, send reminder email if postCreated == nil { _, err := step.Run("send-reminder-email", func() (any, error) { return nil, sendEmail(input.Event.Data["user"].(map[string]interface{})["email"].(string), "reminder") }) if err != nil { return nil, err } } return nil, nil }, ) ``` That's it! We've now written a multi-step function that will send a welcome email, and then send a reminder email if the user hasn't created a post within 24 hours. Most importantly, we had to write no config to do this. We can use all the power of JavaScript to write our functions and all the power of Inngest's tools to coordinate between events and steps. Consider this simple [Inngest function](/docs/learn/inngest-functions) which sends a welcome email when a user signs up: ```py import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="activation-email", trigger=inngest.TriggerEvent(event="app/user.created"), ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: await sendEmail({ email: ctx.event.user.email, template: "welcome" }) ``` This function comes with all of the benefits of Inngest: the code is reliable and retriable. If an error happens, you will recover the data. This works for a single-task functions. However, there is a new requirement: if a user hasn't created a post on our platform within 24 hours of signing up, we should send the user another email. Instead of adding more logic to the handler, we can convert this function into a multi-step one. ### 1. Convert to a step function First, let's convert this function into a multi-step function: - Add a `step` argument to the handler in the Inngest function. - Wrap `sendEmail()` call in a [`step.run()`](/docs/reference/python/steps/run) method. ```py import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="activation-email", trigger=inngest.TriggerEvent(event="app/user.created"), ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: await step.run("send-welcome-email", lambda: sendEmail({ "email": ctx.event.user.email, "template": "welcome" })) ``` The main difference is that we've wrapped our `sendEmail()` call in a `step.run()` call. This is how we tell Inngest that this is an individual step in our function. This step can be retried independently, just like a single-step function would. ### 2. Add another step: wait for event Once the welcome email is sent, we want to wait at most 24 hours for our user to create a post. If they haven't created one by then, we want to send them a reminder email. Elsewhere in our app, an `app/post.created` event is sent whenever a user creates a new post. We could use it to trigger the second email. To do this, we can use the [`step.wait_for_event()`](/docs/reference/python/steps/wait-for-event) method. This tool will wait for a matching event to be fired, and then return the event data. If the event is not fired within the timeout, it will return `None`, which we can use to decide whether to send the reminder email. ```py import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="activation-email", trigger=inngest.TriggerEvent(event="app/user.created"), ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: await step.run("send-welcome-email", lambda: sendEmail({ "email": ctx.event.user.email, "template": "welcome" })) # Wait for an "app/post.created" event post_created = await step.wait_for_event("wait-for-post-creation", { "event": "app/post.created", "match": "data.user.id", # the field "data.user.id" must match "timeout": "24h", # wait at most 24 hours }) ``` Now we have a `postCreated` variable, which will be `None` if the user hasn't created a post within 24 hours, or the event data if they have. ### 3. Set conditional action Finally, we can use the `postCreated` variable to send the reminder email if the user hasn't created a post. Let's add another block of code with `step.run()`: ```py import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="activation-email", trigger=inngest.TriggerEvent(event="app/user.created"), ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: await step.run("send-welcome-email", lambda: sendEmail({ "email": ctx.event.user.email, "template": "welcome" })) # Wait for an "app/post.created" event post_created = await step.wait_for_event("wait-for-post-creation", { "event": "app/post.created", "match": "data.user.id", # the field "data.user.id" must match "timeout": "24h", # wait at most 24 hours }) if not post_created: # If no post was created, send a reminder email await step.run("send-reminder-email", lambda: sendEmail({ "email": ctx.event.user.email, "template": "reminder" })) ``` That's it! We've now written a multi-step function that will send a welcome email, and then send a reminder email if the user hasn't created a post within 24 hours. Most importantly, we had to write no config to do this. We can use all the power of JavaScript to write our functions and all the power of Inngest's tools to coordinate between events and steps. ## Step Reference You can read more about [Inngest steps](/docs/learn/inngest-steps) or jump directly to a step reference guide: - [`step.run()`](/docs/reference/functions/step-run): Run synchronous or asynchronous code as a retriable step in your function. - [`step.sleep()`](/docs/reference/functions/step-sleep): Sleep for a given amount of time. - [`step.sleepUntil()`](/docs/reference/functions/step-sleep-until): Sleep until a given time. - [`step.invoke()`](/docs/reference/functions/step-invoke): Invoke another Inngest function as a step, receiving the result of the invoked function. - [`step.waitForEvent()`](/docs/reference/functions/step-wait-for-event): Pause a function's execution until another event is received. - [`step.sendEvent()`](/docs/reference/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. Please note that each step is executed as **a separate HTTP request**. To ensure efficient and correct execution, place any non-deterministic logic (such as DB calls or API calls) within a `step.run()` call. [Learn more](/docs/guides/working-with-loops). ## Gotchas ### My function is running twice Inngest will communicate with your function multiple times throughout a single run and will use your use of tools to intelligently memoize state. For this reason, placing business logic outside of a `step.run()` call is a bad idea, as this will be run every time Inngest communicates with your function. ### I want to run asynchronous code `step.run()` accepts an `async` function, like so: ```ts await step.run("do-something", async () => { // your code }); ``` Each call to `step.run()` is a single retriable step - a lightweight transaction. Therefore, each step should have a single side effect. For example, the below code is problematic: ```ts await step.run("create-alert", async () => { await createAlert(); await sendAlertLinkToSlack(alertId); }); ``` If `createAlert()` succeeds but `sendAlertLinkToSlack()` fails, the code will be retried and an alert will be created every time the step is retried. Instead, we should split out asynchronous actions into multiple steps so they're retried independently. ```ts await step.run("create-alert", () => createAlert()); await step.run("send-alert-link", () => sendAlertLinkToSlack(alertId)); ``` ### My variable isn't updating Because Inngest communicates with your function multiple times, memoising state as it goes, code within calls to `step.run()` is not called on every invocation. Make sure that any variables needed for the overall function are _returned_ from calls to `step.run()`: ```ts // This is the right way to set variables within step.run :) await step.run("get-user", () => getRandomUserId()); console.log(userId); // 123 ``` For comparison, here are **two examples of malfunctioning code** (if you're using steps to update variables within the function's closure): ```ts // 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 ``` ### `sleepUntil()` isn't working as expected Make sure to only to use `sleepUntil()` with dates that will be static across the various calls to your function. Always use `sleep()` if you'd like to wait a particular time from _now_. ```ts // ❌ Bad new Date(); tomorrow.setDate(tomorrow.getDate() + 1); await step.sleepUntil("wait-until-tomorrow", tomorrow); // ✅ Good await step.sleep("wait-a-day", "1 day"); ``` ```ts // ✅ Good await step.run("get-user-birthday", async () => { await getUser(); return user.birthday; // Date }); await sleepUntil("wait-for-user-birthday", userBirthday); ``` ### Unexpected loop behavior When using loops within functions, it is recommended to treat each iteration as it's own step or steps. When [functions are run](/docs/learn/how-functions-are-executed), the function handler is re-executed from the start for each new step and previously completed steps are memoized. This means that iterations of loops will be run every re-execution, but code encapsulated within `step.run()` will not re-run. If code within a loop is not encapsulated within a step, it will re-run multiple times, which can lead to confusing behavior, debugging, or [logging](/docs/guides/logging). This is why it is recommended to encapsulate non-deterministic code within a `step.run()` when working with loops. Learn more about [working with loops in Inngest](/docs/guides/working-with-loops). You can read more about [Inngest steps](/docs/learn/inngest-steps) or jump directly to a step reference guide: - [`step.Run()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Run): Run synchronous or asynchronous code as a retriable step in your function. - [`step.Sleep()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Sleep): Sleep for a given amount of time. - [`step.Invoke()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Invoke): Invoke another Inngest function as a step, receiving the result of the invoked function. - [`step.WaitForEvent()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#WaitForEvent): Pause a function's execution until another event is received. Please note that each step is executed as **a separate HTTP request**. To ensure efficient and correct execution, place any non-deterministic logic (such as DB calls or API calls) within a `step.Run()` call. [Learn more](/docs/guides/working-with-loops). ## Gotchas ### My function is running twice Inngest will communicate with your function multiple times throughout a single run and will use your use of tools to intelligently memoize state. For this reason, placing business logic outside of a `step.Run()` call is a bad idea, as this will be run every time Inngest communicates with your function. ### Unexpected loop behavior When using loops within functions, it is recommended to treat each iteration as it's own step or steps. When [functions are run](/docs/learn/how-functions-are-executed), the function handler is re-executed from the start for each new step and previously completed steps are memoized. This means that iterations of loops will be run every re-execution, but code encapsulated within `step.Run()` will not re-run. If code within a loop is not encapsulated within a step, it will re-run multiple times, which can lead to confusing behavior, debugging, or [logging](/docs/guides/logging). This is why it is recommended to encapsulate non-deterministic code within a `step.Run()` when working with loops. Learn more about [working with loops in Inngest](/docs/guides/working-with-loops). You can read more about [Inngest steps](/docs/learn/inngest-steps) or jump directly to a step reference guide: - [`step.run()`](/docs/reference/python/steps/run): Run synchronous or asynchronous code as a retriable step in your function. - [`step.sleep()`](/docs/reference/python/steps/sleep): Sleep for a given amount of time. - [`step.sleep_until()`](/docs/reference/python/steps/sleep-until): Sleep until a given time. - [`step.invoke()`](/docs/reference/python/steps/invoke): Invoke another Inngest function as a step, receiving the result of the invoked function. - [`step.wait_for_event()`](/docs/reference/python/steps/wait-for-event): Pause a function's execution until another event is received. - [`step.send_event()`](/docs/reference/python/steps/send-event): Send event(s) reliably within your function. Use this instead of `inngest.send()` to ensure reliable event delivery from within functions. Please note that each step is executed as **a separate HTTP request**. To ensure efficient and correct execution, place any non-deterministic logic (such as DB calls or API calls) within a `step.run()` call. [Learn more](/docs/guides/working-with-loops). ## Gotchas ### My function is running twice Inngest will communicate with your function multiple times throughout a single run and will use your use of tools to intelligently memoize state. For this reason, placing business logic outside of a `step.run()` call is a bad idea, as this will be run every time Inngest communicates with your function. ### `sleep_until()` isn't working as expected Make sure to only to use `sleep_until()` with dates that will be static across the various calls to your function. Always use `sleep()` if you'd like to wait a particular time from _now_. ```py # ❌ Bad tomorrow = datetime.now() + timedelta(days=1) await step.sleepUntil("wait-until-tomorrow", tomorrow); # ✅ Good await step.sleep("wait-a-day", "1 day"); ``` ```py # ✅ Good user_birthday = await step.run("get-user-birthday", async () => { user = await get_user(); return user.birthday; # Date }); await step.sleep_until("wait-for-user-birthday", user_birthday); ``` ### Unexpected loop behavior When using loops within functions, it is recommended to treat each iteration as it's own step or steps. When [functions are run](/docs/learn/how-functions-are-executed), the function handler is re-executed from the start for each new step and previously completed steps are memoized. This means that iterations of loops will be run every re-execution, but code encapsulated within `step.run()` will not re-run. If code within a loop is not encapsulated within a step, it will re-run multiple times, which can lead to confusing behavior, debugging, or [logging](/docs/guides/logging). This is why it is recommended to encapsulate non-deterministic code within a `step.run()` when working with loops. Learn more about [working with loops in Inngest](/docs/guides/working-with-loops). ## Further reading More information on multi-step functions: - Docs guide: [working with loops in Inngest](/docs/guides/working-with-loops). - Blog post: ["Building an Event Driven Video Processing Workflow with Next.js, tRPC, and Inngest "](/blog/nextjs-trpc-inngest) - Blog post: ["Running chained LLMs with TypeScript in production"](/blog/running-chained-llms-typescript-in-production) - 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). - [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. # Multiple triggers & wildcards Source: https://www.inngest.com/docs/guides/multiple-triggers 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" }, [ { event: "user.created" }, { event: "user.updated" }, { cron: "0 5 * * *" }, // Every morning at 5am ], async ({ event, step }) => { // ... }, ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "resync-user-data"}, inngestgo.MultipleTriggers{ inngestgo.EventTrigger("user.created", nil), inngestgo.EventTrigger("user.updated", nil), inngestgo.CronTrigger("0 5 * * *", nil), }, func(ctx context.Context, input inngestgo.Input) (any, error) { // ... }, ) ``` ```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 type WildcardEvents = { "app/blog.post.*": { name: "app/blog.post.created" | "app/blog.post.published"; data: { postId: string; authorId: string; createdAt: string; } | { postId: string; authorId: string; publishedAt: string; } } } new Inngest({ id: "my-app", schemas: new EventSchemas().fromRecord() }); inngest.createFunction( { id: "blog-updates-to-slack" }, { 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) (any, error) { switch event := input.Event.(type) { case EventA: // `event` is type narrowed to only the `a` event case EventB: // `event` is type narrowed to only the `b` event case inngestgo.FunctionInvokedEvent: // `event` is type narrowed to only the `inngest/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: Learn how to pause an Inngest function.' # Function Pausing 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. # Navigate to the function's page in Inngest Cloud, and click the Pause button near the top right corner: ![Pause button in Inngest Cloud.](/assets/docs/guides/pause-functions/pause-btn.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/guides/pause-functions/pause-modal.png) ## Resuming a function To resume a paused function, navigate to the function's page in Inngest Cloud, and click the Resume button near the top right corner: ![Resume button in Inngest Cloud.](/assets/docs/guides/pause-functions/resume-btn.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 Replay tab. 2. Click the New Replay button to create a new replay. 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/guides/pause-functions/replay-modal.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 adjust the execution order of functions based on any data. Ideal for pushing critical work to the front of the queue.' # Priority 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", }, }, { event: "ai/summary.requested" }, async ({ event, step }) => { // This function will be prioritized based on the account type } ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( &inngestgo.FunctionOpts{ ID: "ai-generate-summary", Priority: &inngest.Priority{ Run: inngestgo.StrPtr("event.data.account_type == 'enterprise' ? 120 : 0"), }, }, inngestgo.EventTrigger("ai/summary.requested", nil), func(ctx context.Context, input inngestgo.Input) (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): ``` ### 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/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 excessive function runs over a given time period by skipping events beyond a specific limit. Ideal for protecting against abuse.' # Rate limiting 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", }, }, { 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( &inngestgo.FunctionOpts{ ID: "synchronize-data", RateLimit: &inngestgo.RateLimit{ 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) (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): ``` ### 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/functions/rate-limit) * [Python SDK Reference](/docs/reference/python/functions/create#configuration) # Integrate email events with Resend webhooks Source: https://www.inngest.com/docs/guides/resend-webhook-events Description: Set up Resend webhooks with Inngest and use Resend events within Inngest functions.' # Integrate email events with Resend webhooks [Resend webhooks](https://resend.com/docs/dashboard/webhooks/introduction) can be used to build functionality into your application based on changes in the email status. In this guide, you will learn: - What webhook events are offered by Resend. - How to set up Inngest to receive Resend webhook events. - How to define Inngest functions in your application using Resend events. - How to build a dynamic drip marketing campaign which responds to a user's behavior. To follow this guide, you need a [Resend](https://resend.com/) and [Inngest](/) accounts and have Inngest [set up](/docs/getting-started/nextjs-quick-start) in your codebase. # Resend uses webhooks to push real-time notifications to your application about the emails you're sending. It offers the following event types: - `email.sent` - the API request was successful and Resend will attempt to deliver the message to the recipient's mail server. - `email.bounced` - the recipient's mail server permanently rejected the email. - `email.delivery_delayed` - the email couldn't be delivered to the recipient's mail server for example, because the recipient's inbox is full, or when the receiving email server experiences a transient issue. - `email.delivered` - Resend successfully delivered the email to the recipient's mail server. - `email.complained` - the recipient marked the delivered email as spam. - `email.opened` - the recipient's opened the email. - `email.clicked` - the recipient's clicked on an email link. These events can be used to build responsive behavior based on changes in the email status. For example, you could use these events in scenarios such as: - If an email bounced, remove the address from the mailing list or flag it in the database. - Create a dynamic marketing drip campaign based on the recipient behavior. - Build incident or retention report for your email campaigns. ### Building with Resend webhooks You can manage webhook events directly in your backend through an endpoint or you could use a tool like [Inngest](/docs) which ensures reliable execution of functions in your codebase. Inngest comes with functionalities such as: - a built-in queue to execute longer-running functions reliably - [Controlling concurrency](/docs/guides/concurrency) to handle spikes without overwhelming your API or database. - Executing multiple functions from a single event ([fan-out jobs](/docs/guides/fan-out-jobs)). - Implementing [delayed code execution](/docs/guides/multi-step-functions) after a specified period. - [Debouncing events](/docs/reference/functions/debounce) to minimize duplicate processing. In short, using Inngest makes your application more resilient, scalable, and easier to recover from an incident. ## Receiving Resend webhook events in Inngest Let's now connect Resend with Inngest. 1. Set up the Inngest webhook. To do so, in Inngest Cloud, navigate to **[Manage → Webhooks](https://app.inngest.com/env/production/manage/webhooks)** page and click on **Create Webhook** button on the right. ![Inngest Cloud website on an the empty Webhooks page.](/assets/docs/guides/resend-webhook-events/inngest-1-no-webhooks.png) 1. In the modal window, specify the name of the webhook (for example, “Resend”): ![Modal window with instruction: "Create a New Webhook". "Resend" is chosen as a name for the new webhook.](/assets/docs/guides/resend-webhook-events/inngest-2-new-webhook.png) You will now see your webhook page with the webhook URL at the top: ![A webhook page on Inngest Cloud.](/assets/docs/guides/resend-webhook-events/inngest-3-webhook-page.png) 1. Paste the following [transform function](/docs/platform/webhooks#defining-a-transform-function) into the **Transform Event** area: ```jsx function transform(evt, headers = {}, queryParams = {}) { return { // Add a prefix to the name of the event name: `resend/${evt.type}`, data: evt.data, }; }; ``` The transform function will translate the incoming data to be compatible with the Inngest event payload format, as well as prefix all events with `resend/`. Next, click **Save Transformed Changes** button to save this function. ![A webhook page on Inngest Cloud featuring Transform Event view.](/assets/docs/guides/resend-webhook-events/inngest-4-transform-function.png) Your Inngest webhook is set up! 1. Go back to the top of the page and copy your webhook URL. ![Top of the webhook page on Inngest Cloud featuring the webhook URL](/assets/docs/guides/resend-webhook-events/inngest-4-screen-with-url.png) 1. Now navigate to the [webhooks](https://resend.com/webhooks) page in the Resend dashboard. Click on the **Add webhook** button: ![An empty webhook page on the Resend dashboard with a message: "You haven't configured any webhooks yet"](/assets/docs/guides/resend-webhook-events/resend-1-webhooks-empty-page.png) 1. In the modal, paste the Inngest webhook URL and choose which events you want to listen to -- tick all of them for now. ![A form in a modal window with a field to paste a webhook URL and a list of possible envents to listen to.](/assets/docs/guides/resend-webhook-events/resend-2-webhooks-subscription.png) Your webhook will be created but there will be no webhook events yet. ![The webhook page on the Resend dashboard now featuring one connected webhook. A message reads: "No webhook events yet"](/assets/docs/guides/resend-webhook-events/resend-3-no-webhook-events.png) 1. To see your webhook in action, [send a test email](https://resend.com/emails) and see the webhook events recorded: ![The webhook page on the Resend dashboard now featuring one connected webhook and two events.](/assets/docs/guides/resend-webhook-events/resend-5-webhook-details.png) Your Resend webhook is set up 🥳 1. Now check your Inngest dashboard to see the **[Events](https://app.inngest.com/env/production/events)** page in Inngest Cloud: ![Events tab in Inngest Cloud featuring two events from Resend](/assets/docs/guides/resend-webhook-events/inngest-5-event-screen.png) Congratulations! Now you have Resend events coming into Inngest. Next, you will use the Resend events you've received in Inngest to trigger functions in your application's codebase. ## Writing your first Inngest function *Please note that this example assumes that you are using TypeScript and Next.js, and that you have already added Inngest to your project, but if you're using Inngest for the first time, you can follow the [Quickstart guide](/docs/getting-started/nextjs-quick-start) to get it set up.* With Inngest set up in your codebase, you can write a function that is triggered every time a specific event arrives. For example, if an email sent to an user bounces, you can mark the user's email invalid in your database: ```tsx inngest.createFunction( { id: 'invalidate-user-email' }, { event: 'resend/email.bounced' }, async ({ event }) => { event.data.to[0]; await db.users.byEmail(email); if (user) { user.email_status = "invalid"; await db.users.update(user); } } ) ``` Now that you've seen how to handle Resend events in your application, let's look at a more advanced example. ## Creating a function to send email Suppose that you want want to send user a welcome email when they sign up to your application. First, create a helper function called `sendEmail` by adapting the example from the [Resend Next.js Quickstart](https://resend.com/docs/send-with-nextjs): ```tsx // resend.ts new Resend(process.env.RESEND_API_KEY); export async function sendEmail( to: string, subject: string, content: React.ReactElement ) { await resend.emails.send({ from: 'Acme ', to: [to], subject, react: content }); if (error) { throw error; } return data; }; ``` Now you're able to send emails! Let's put this new function to use. The below example assumes that your application receives a `app/signup.completed` event when a new user signs up: ```tsx inngest.createFunction( { id: 'send-welcome-email' }, { event: 'app/signup.completed' }, async ({ event }) => { event.data; await sendEmail(user.email, "Welcome to Acme", (

Welcome to ACME, {user.firstName}

)); } ) ``` Now that we've mastered the basics of sending email with Resend from an Inngest function, you can build even more complex functionality. ## Sending a delayed follow-up email Every Inngest function handler comes with an additional [`step` object](/docs/reference/functions/step-sleep-until) which provides tools to create more fine-grained functions. Using `step.run` allows you to encapsulate specific code that will be automatically retried ensuring that issues with one part of your function don't force the entire function to re-run. Additionally, [other tools like `step.sleep`](/docs/reference/functions/step-sleep) are available to extend your app's functionality. The code below sends a welcome email, then uses `step.sleep` to wait for three days before sending another email offering a free trial: ```tsx inngest.createFunction( { id: 'onboarding-emails' }, { event: 'app/signup.completed' }, async ({ event, step }) => { // ← step is available in the handler's arguments event.data user await step.run('welcome-email', async () => { await sendEmail(email, "Welcome to ACME", (

Welcome to ACME, {firstName}

)); }) // wait 3 days before second email await step.sleep('wait-3-days', '3 days') await step.run('trial-offer-email', async () => { await sendEmail(email, "Free ACME Pro trial", (

Hello {firstName}, try our Pro features for 30 days for free

)); }) } ) ``` This is handy, but we can do better. Since Resend sends you webhook events when emails are delivered, opened and clicked, you can build dynamic email campaigns tailored to each user's needs. ## Creating a dynamic drip campaign Let's say you want to create the following campaign: - Send every user a welcome email when they join. - If a `resend/email.clicked` event is received (meaning the user has engaged with your email), wait a day and then follow-up with pro user tips meant for highly engaged users. - Otherwise, wait for up to 3 days and then send them the default trial offer, but only if the user hasn't already upgraded their plan in the meantime. ```tsx inngest.createFunction( { id: "signup-drip-campaign" }, { event: "app/signup.completed" }, async ({ event, step }) => { event.data; user "Welcome to ACME"; await step.run("welcome-email", async () => { return await sendEmail( email, welcome,

Welcome to ACME, {user.firstName}

); }); // Wait up to 3 days for the user open the email and click any link in it await step.waitForEvent("wait-for-engagement", { event: "resend/email.clicked", if: `async.data.email_id == ${emailId}`, timeout: "3 days", }); // if the user clicked the email, send them power user tips if (clickEvent) { await step.sleep("delay-power-tips-email", "1 day"); await step.run("send-power-user-tips", async () => { await sendEmail( email, "Supercharge your ACME experience",

Hello {firstName}, here are tips to get the most out of ACME

); }); // wait one more day before sending the trial offer await step.sleep("delay-trial-email", "1 day"); } // check that the user is not already on the pro plan db.users.byEmail(email); if (dbUser.plan !== "pro") { // send them a free trial offer await step.run("trial-offer-email", async () => { await sendEmail( email, "Free ACME Pro trial",

Hello {firstName}, try our Pro features for 30 days for free

); }); } } ); ``` Voilà! You've created a dynamic marketing drip campaign where subsequent emails are informed by your user's behavior. ## Testing webhook events using the Inngest Dev Server During local development with Inngest, you can use the Inngest Dev Server to run and test your functions on your own machine. To start the server, in your project directory run the following command: ```bash npx inngest-cli@latest dev ``` In your browser open [http://localhost:8288](http://localhost:8288/) to see the development UI. To forward and quickly test events from Inngest Cloud to your Dev Server, head over to [Inngest Cloud](https://app.inngest.com/env/production/events). Choose **Events** tab from the nav bar. Select any individual event, choose **Logs** from the sidebar, and then select the **Send to Dev Server**. ![Events tab in Inngest Cloud. The code area includes a button: "Send to the Dev Server"](/assets/docs/guides/resend-webhook-events/inngest-6-cloud-dev-server.png) You'll now see the event in the Inngest Dev Server's **Stream** tab alongside any functions that it triggered. ![Stream tab in Inngest Dev Server featuring an event](/assets/docs/guides/resend-webhook-events/inngest-7-event-stream-1.png) From here you can select the event, replay it to re-run any functions or edit and replay to edit the event payload to test different types of events. ![Details of a Resend event](/assets/docs/guides/resend-webhook-events/inngest-8-event-details.png) ## Conclusion Congratulations! You've now learned how to use Inngest to create functions that use Resend webhook events. # Crons (Scheduled Functions) Source: https://www.inngest.com/docs/guides/scheduled-functions 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/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" }, { 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" }, { 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/inngest-go" "github.com/inngest/inngest-go/step" ) func init() { // This weekly digest function will run at 12:00pm on Friday in the Paris timezone inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "prepare-weekly-digest", Name: "Prepare Weekly Digest"}, inngestgo.CronTrigger("TZ=Europe/Paris 0 12 * * 5"), func(ctx context.Context, input inngestgo.Input[any]) (any, error) { // Load all the users from your database: users, err := step.Run("load-users", func() ([]*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.SendEvent("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( inngestgo.FunctionOpts{ID: "send-weekly-digest-email"}, inngestgo.EventTrigger("app/send.weekly.digest", nil), func(ctx context.Context, input inngestgo.Input) (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(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. # Sending events from functions Source: https://www.inngest.com/docs/guides/sending-events-from-functions Description: How to send events from within functions to trigger other functions to run in parallel' # Sending events from functions 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 [cancel](/docs/guides/cancel-running-functions) another function * You want to send data to another function [waiting for an event](/docs/reference/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/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" }, { 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" }, { 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. ## By using [`step.sendEvent()`](/docs/reference/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/events/send), the context around the function run is not present. To send events from within functions, you will use [`inngestgo.Send()`](https://pkg.go.dev/github.com/inngest/inngestgo#Send). 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. ```go package main import ( "context" "github.com/inngest/inngest-go" "github.com/inngest/inngest-go/step" ) func loadCron(client *inngest.Client) *inngest.FunctionDefinition { return client.CreateFunction( inngest.FunctionOpts{ ID: "weekly-activity-load-users", }, inngest.CronTrigger("0 12 * * 5"), func(ctx context.Context, event *inngest.Event) error { // Fetch all users var users []User if err := step.Run("fetch-users", func() error { var err error users, err = fetchUsers() return err }); err != nil { return err } // For each user, send us an event. Inngest supports batches of events // as long as the entire payload is less than 512KB. events := make([]inngest.Event, len(users)) for i, user := range users { events[i] = inngest.Event{ Name: "app/weekly-email-activity.send", Data: map[string]interface{}{ "user": user, }, } } // Send all events to Inngest, which triggers any functions listening to // the given event names. if err := inngestgo.Send(ctx, events); err != nil { return err } // Return the number of users triggered return step.Return(map[string]interface{}{ "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. ```go import ( "context" "github.com/inngest/inngest-go" "github.com/inngest/inngest-go/step" ) func sendReminder(client *inngest.Client) *inngest.FunctionDefinition { return client.CreateFunction( inngest.FunctionOpts{ ID: "weekly-activity-send-email", }, inngest.TriggerEvent("app/weekly-email-activity.send"), func(ctx *inngest.Context) error { var data interface{} if err := step.Run("load-user-data", func() error { var err error data, err = loadUserData(ctx.Event.Data["user"].(map[string]interface{})["id"].(string)) return err }); err != nil { return err } if err := step.Run("email-user", func() error { return sendEmail(ctx.Event.Data["user"], data) }); err != nil { return err } return nil }, ) } ``` 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, step: inngest.Step): # Fetch all users async def fetch(): return await fetch_users() users = await 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 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, step: inngest.Step): async def load_data(): return await load_user_data(ctx.event.data["user"]["id"]) data = await step.run("load-user-data", load_data) async def send(): return await send_email(ctx.event.data["user"], data) await 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 # Step parallelism Source: https://www.inngest.com/docs/guides/step-parallelism - 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/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" }, { 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 compelte. 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 step.parallel()` ```py @client.create_function( fn_id="my-fn", trigger=inngest.TriggerEvent(event="my-event"), ) async def fn( ctx: inngest.Context, step: inngest.Step, ) -> None: user_id = ctx.event.data["user_id"] (updated_user, sent_email) = await step.parallel( ( lambda: step.run("update-user", update_user, user_id), lambda: step.run("send-email", send_email, user_id), ) ) ``` #### sync - with `inngest.StepSync` and `step.parallel()` ```py @client.create_function( fn_id="my-fn", trigger=inngest.TriggerEvent(event="my-event"), ) def fn( ctx: inngest.Context, step: inngest.StepSync, ) -> None: user_id = ctx.event.data["user_id"] (updated_user, sent_email) = step.parallel( ( lambda: step.run("update-user", update_user, user_id), lambda: step.run("send-email", send_email, user_id), ) ) ``` At this time, Inngest does not have stable support for `asyncio.gather` or `asyncio.wait`. If you'd like to try out experimental support, use the `_experimental_execution` option when creating your function: ```py @client.create_function( fn_id="my-fn", trigger=inngest.TriggerEvent(event="my-event"), _experimental_execution=True, ) def fn( ctx: inngest.Context, step: inngest.StepSync, ) -> None: user_id = ctx.event.data["user_id"] (updated_user, sent_email) = asyncio.gather( asyncio.create_task(step.run("update-user", update_user, user_id)), asyncio.create_task(step.run("send-email", send_email, user_id)), ) ``` When using `asyncio.wait`, `asyncio.FIRST_COMPLETED` is supported. However, `asyncio.FIRST_EXCEPTION` is not supported due to the way Inngest interrupts the execution of the function. ## 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" }, { event: "app/text.summarize" }, async ({ event, step }) => { splitTextIntoChunks(event.data.text); await Promise.all( chunks.map((chunk) => step.run("summarize-chunk", () => 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: Limit the throughput of function execution over a period of time. Ideal for working around third-party API rate limits.'; # Throttling 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 priority 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", }, } { event: "ai/summary.requested" }, async ({ event, step }) => { } ); ``` ```go {{ title: "Go" }} inngestgo.CreateFunction( &inngestgo.FunctionOpts{ ID: "unique-function-id", Throttle: &inngestgo.Throttle{ 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) (any, error) { // This function will be throttled to 1 run per 5 seconds for a given event payload with matching user_id 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): ``` 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. This defaults to 1, which ensures that requests are smoothed amongst the given `period`. - `key`: An optional expression which returns a throttling key using event data. This allows you to apply unique throttle limits specific to a user. **Configuration information** - The rate limit smooths requests in the given period, allowing `limit/period` requests a second. - 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/functions/create#throttle) * [Python SDK Reference](/docs/reference/python/functions/create#configuration) # Trigger your code from Retool Source: https://www.inngest.com/docs/guides/trigger-your-code-from-retool Internal tools are a pain to build and maintain. Fortunately, [Retool](https://retool.com/) has helped tons of companies reduce the burden. Retool primarily focuses on building dashboards and forms and it integrates well with several databases and cloud APIs. Often though, there are actions that your support or customer success team needs to perform that are too complex for Retool built-in features. You may have even written some of necessary code in your application, but you can't easily run it from Retool. ## The problem Let's say you have an integration built in your application and you backend code imports a bunch of data from that third party. The third party API or your backend may have went down for a few hours and you may have missing data for certain user. When someone reaches out to support, you may need to re-run that import script to backfill the missing data. {/* TBD diagram? */} We're going to walk through how you can do this so your team can trigger important scripts right from your Retool app. This guide assumes you have a basic experience building forms with Retool (*If you don't, check out [this great guide](https://docs.retool.com/docs/create-forms-using-form-component)*). ## The plan The goal is to enable your team to trigger a script anytime they click a button in Retool. To achieve this we will: 1. Create a Retool button that sends an event to Inngest 2. Write an Inngest function that uses our existing script 3. Configure that function to run when our event is received 4. See how it works end to end ## Sending an event from Retool To send data from Retool, we'll need to set up a “[Resource](https://docs.retool.com/docs/resources)” first. On your Resources tab in Retool, click “Create New” then select “Resource.” Then select “Rest API.” Now jump over to the Inngest Cloud dashboard and [create a new Event Key in the Inngest dashboard](/docs/events/creating-an-event-key). Copy your brand new key and in the Retool dashboard, prefix your key with the Inngest Event API URL and path: `https://inn.gs/e/` ```shell https://inn.gs/e/ ``` Your new resource will look like this. When it does, click “Create resource.” ![Inngest Retool resource screenshot](/assets/guides/trigger-your-code-from-retool/retool-resource.png) Now, let's head to the Retool app that you want to add the button form to. Let's say you have already built out the following form called `runBackfillForm` with a single input called `userId` and a submit button: ![Retool form screenshot](/assets/guides/trigger-your-code-from-retool/retool-form.png) Next, create a new “Resource query” from the “Code” panel at the bottom left (use the + button). Let's name our new query `sendBackfillRequested` and select our new “Inngest” resource from the drop down. Update the “Action type” to a `POST` request. In the “Body” section, we need add the data that we want to send to Inngest. Inngest events require a name and some data as JSON. It's useful to prefix your event names to group them, here we'll call our event `"retool/backfill.requested"` and we'll pass the user id from the form and for future auditing purposes, the email of the current Retool user on your team: ```json { "user_id": "{{runBackfillForm.data.userId}}", "agent_id": "{{current_user.email}}" } ``` At the end, your resource query will look like this. Let's save it then click “Run” to test it. ![Retool resource query screenshot](/assets/guides/trigger-your-code-from-retool/retool-resource-query.png) In the Inngest Cloud dashboard's “Events” tab, you should see a brand new `retool/backfill.requested` event. Click on the event and you should be able to select the payload that we just sent. {/* TODO - Update screenshots!! */} ![Inngest Cloud dashboard view event payload](/assets/guides/trigger-your-code-from-retool/inngest-view-event-payload.gif) Now that we've verified the data is sent over to Inngest, you can attach the resource query as an event handler to the submit button. Select the “Default” interaction type and click “+ Add” to select our resource query `sendBackfillRequested`. For fun, you can add an `isFetching` to show loading. ![Retool form submit button event handler](/assets/guides/trigger-your-code-from-retool/retool-resource-query.png) We're halfway there - with this in place any agent from our team can trigger this event as needed. ## Writing our Inngest function Using [the Inngest SDK](/features/sdk?ref=retool-guide) you can define your Inngest function and it's event trigger in one file. We'll create a directory called `inngest` in our project root: ``` mkdir -p inngest ``` Now we'll create a file in this directory for our function - `runBackfillForUser.js`. This will be our Inngest function which will import our existing backfill code, use the `user_id` from the event payload to run that code, and return a http status code in our response to tell Inngest [if it should be retried or not](/docs/functions/retries?ref=retool-guide). ```ts {{ title: "runBackfillForUser.ts" }} export default inngest.createFunction( { id: "run-backfill-for-user" }, // The name displayed in the Inngest dashboard { event: "retool/backfill.requested" }, // The event triggger async ({ event }) => { await runBackfillForUser(event.data.user_id); return { status: result.ok ? 200 : 500, message: `Ran backfill for user ${event.data.user_id}`, }; } ); ``` ```ts {{ title: "client.ts" }} inngest = new Inngest({ id: "my-app" }) ``` That's our function - now, we just need to serve our function. ### Serving our function You need to serve your function to enable Inngest to remotely and securely invoke your function via HTTP. For this guide, we'll explain how to do this with an existing [Express.js](https://expressjs.com/) application. Inngest's default [`serve()`](/docs/reference/serve) handler can be imported and passed to Express.js' `app.use` or `router.use`. You can get your Inngest signing key from [the Inngest dashboard](https://app.inngest.com/env/production/manage/signing-key). ```js app.use("/api/inngest", serve("My API", process.env.INNGEST_SIGNING_KEY, [ runBackfillForUser, ])) // your existing routes... app.get("/api/whatever", ...) app.post("/api/something_else", ...) ``` ## Deploying your function By serving your functions via HTTP, you don't need to deploy your code to Inngest Cloud or set up a new deployment process. After you deploy your code, you need to visit the Inngest dashboard to sync your app. This allows Inngest to discover and remotely execute your functions. After syncing your app, your new function should appear in the Functions tab of the Inngest Cloud dashboard: ![Inngest Cloud dashboard view deployed function](/assets/guides/trigger-your-code-from-retool/inngest-deployed-function.png) ## Bringing it all together Now that our code is pushed to production and we've set the secrets that we need, let's test it end to end. ![Retool submit form](/assets/guides/trigger-your-code-from-retool/retool-submit-form.gif) And a few seconds later in the Inngest cloud dashboard: ![Inngest cloud dashboard view function output](/assets/guides/trigger-your-code-from-retool/inngest-view-function-output.gif) Fantastic. We've now used our Retool form to trigger a backfill script on-demand with no infrastructure required to setup. Every time your support team needs to trigger this script, they can do it and ensure your users are happy. ## Over to you You now know how to get some existing code from your application shipped to Inngest and triggered right from Retool with a full audit trail of who triggered it, for what user and full logs. There was no need to set up a more complex infrastructure with a queue or new endpoint on your production API - Just push your code to Inngest and send an event from Retool - done and dusted. # Build workflows configurable by your users Source: https://www.inngest.com/docs/guides/user-defined-workflows 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/functions/create#step). Our _"Add a Table of Contents"_ leverages Inngest's [step API](/docs/reference/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/features/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 [{ 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: Implement loops in your Inngest functions and avoid common pitfalls.'; # Working with Loops in Inngest 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" }, { 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" }} "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" }, { 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/inngest-go" "github.com/inngest/inngest-go/step" ) inngestgo.CreateFunction( inngestgo.FunctionOpts{ID: "simple-function"}, inngestgo.EventTrigger("test/simple.function", nil), func(ctx context.Context, input inngestgo.Input) (any, error) { fmt.Println("hello") _, err := step.Run("a", func() error { fmt.Println("a") return nil }) if err != nil { return nil, err } _, err = step.Run("b", func() error { fmt.Println("b") return nil }) if err != nil { return nil, err } _, err = step.Run("c", func() error { fmt.Println("c") return 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/inngest-go" "github.com/inngest/inngest-go/step" ) inngest.CreateFunction( "simple-function", inngest.EventTrigger("test/simple.function"), func(ctx context.Context, step inngest.Step) error { if _, err := step.Run("hello", func() error { fmt.Println("hello") return nil }); err != nil { return err } if _, err := step.Run("a", func() error { fmt.Println("a") return nil }); err != nil { return err } if _, err := step.Run("b", func() error { fmt.Println("b") return nil }); err != nil { return err } if _, err := step.Run("c", func() error { fmt.Println("c") return nil }); err != nil { return err } return 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, step: inngest.Step): print("hello") async def step_a(): print("a") await step.run("a", step_a) async def step_b(): print("b") await step.run("b", step_b) async def step_c(): print("c") await 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, step: inngest.Step): await step.run("hello", lambda: print("hello")) await step.run("a", lambda: print("a")) await step.run("b", lambda: print("b")) await 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"}, { 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 inngest.CreateFunction( "shopify-product-import", inngest.EventTrigger("shopify/import.requested"), func(ctx context.Context, event inngest.Event) error { var allProducts []Product var cursor *string 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() session, err := database.GetShopifySession(event.Data["storeId"].(string)) if err != nil { return err } for hasMore { if page, err := step.Run(fmt.Sprintf("fetch-products-%v", cursor), func() error { return shopify.Product.All(&shopify.ProductListOptions{ Session: session, SinceID: cursor, }) }); err != nil { return err } // Combine all of the data into a single list allProducts = append(allProducts, page.Products...) if len(page.Products) == 50 { id := page.Products[49].ID cursor = &id } else { hasMore = false } } // Now we have the entire list of products within allProducts! return nil }, ) ``` 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, step: inngest.Step): 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 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/guides/multi-step-functions). - 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 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/functions/rate-limit), [debounce](/docs/reference/functions/debounce), or [idempotency](/docs/guides/handling-idempotency) * Conditionally matching events for [wait for event](/docs/reference/functions/step-wait-for-event), [cancellation](/docs/guides/cancel-running-functions), or the [function trigger's `if` option](/docs/reference/functions/create#trigger) * Returning values for function [run priority](/docs/reference/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. # Inngest Documentation Source: https://www.inngest.com/docs/index import { RiCloudLine, RiNextjsFill, RiNodejsFill, RiGitPullRequestFill, RiGuideFill, } from "@remixicon/react"; hidePageSidebar = true; Inngest is an event-driven durable execution platform that allows you to run fast, reliable code on any platform, without managing queues, infra, or state. Write functions in TypeScript, Python or Go to power background and scheduled jobs, with steps built in. We handle the backend infra, queueing, scaling, concurrency, throttling, rate limiting, and observability for you. ## Get started } iconPlacement="top" > Add queueing, events, crons, and step functions to your Next app on any cloud provider. } iconPlacement="top" > Write durable step functions in any Node.js app and run on servers or serverless. } iconPlacement="top" > Develop reliable step functions in Python without managing queueing systems or DAG based workflows. } iconPlacement="top" > Write fast, durable step functions in your Go application using the standard library.
Learn how Inngest's Durable Execution Engine ensures that your workflow already run until completion with steps.
## Build with Inngest Users today are demanding customization and integrations. Discover how to build a Workflow Engine for your users using Inngest. Inngest offers tools to support the development of AI-powered applications. Learn how to build a RAG workflow with Inngest. A drip campaign is usually based on your user's behavior. This example will walk you through many examples of email workflows. ## Explore } > Learn how to leverage Function steps to build reliable workflows. } > Add multi-tenant aware prioritization, concurrency, throttling, batching, and rate limiting capabilities to your Inngest Functions. } > Monitor your deployments with Metrics and Function & Events Logs. } > Deploy your Inngest Functions to Vercel, Netlify, Cloudflare Pages and other Cloud Providers. ## LLM Docs An LLM-friendly version of the Inngest docs is available in two formats: `llms.txt` and `llms-full.txt`. These are useful for passing context to LLMs, AI-enabled IDEs, or similar tools to answer questions about Inngest. * [inngest.com/llms.txt](https://www.inngest.com/llms.txt) - A table of contents for the docs, ideal for smaller context windows or tools that can crawl the docs. * [inngest.com/llms-full.txt](https://www.inngest.com/llms-full.txt) - The entire docs in markdown format. ## Community & Support If you need help with Inngest, you can reach out to us in the following ways: * [Ask your questions in our Discord community](/discord) * [Open a ticket in our support center](https://app.inngest.com/support) * [Contact our sales engineering team](/contact?ref=docs) # Glossary Source: https://www.inngest.com/docs/learn/glossary description = `Key terms for Inngest's documentation 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/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: Learn how Inngest functions are executed using Durable Execution. Understand how steps are executed, how errors are handled, and how state is persisted. 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" }, { 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. ## 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: Learn what Inngest functions are and of what they are capable.'; # Inngest Functions Inngest functions 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. This page covers components of an Inngest function, as well as introduces different kinds of functions. If you'd like to learn more about Inngest's execution model, check the [ How Inngest functions are executed"](/docs/learn/how-functions-are-executed) page. # Let's have a look at the following Inngest function: ```ts export default inngest.createFunction( // config { id: "import-product-images" }, // trigger (event or cron) { event: "shop/product.imported" }, // handler function 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); }); // You can include numerous steps in your function await step.run('resize-images', async () => { await resizer.bulk({ urls: s3Urls, quality: 0.9, maxWidth: 1024 }); }) }; ); ``` ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( // 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) (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 } // You can include numerous steps in your function _, err = step.Run("resize-images", func() (any, error) { return nil, resizer.Bulk(ResizerOpts{ URLs: s3Urls, Quality: 0.9, MaxWidth: 1024, }) }) if err != nil { return nil, err } return nil, nil }, ) ``` ```py import inngest from src.inngest.client import inngest_client @inngest_client.create_function( # config id="import-product-images", # trigger (event or cron) trigger=inngest.Trigger(event="shop/product.imported") ) async def import_product_images(ctx: inngest.Context, step: inngest.Step): # Here goes the business logic # By wrapping code in steps, it will be retried automatically on failure s3_urls = await step.run( "copy-images-to-s3", lambda: copy_all_images_to_s3(ctx.event.data["imageURLs"]) ) # You can include numerous steps in your function await step.run( "resize-images", lambda: resizer.bulk( urls=s3_urls, quality=0.9, max_width=1024 ) ) ``` The above code can be explained as: > This Inngest function is called `import-product-images`. When an event called `shop/product.imported` is received, run two steps: `copy-images-to-s3` and `resize-images`. Let's have a look at each of this function's components. {/* 💡 You can test Inngest functions using standard tooling such as Jest or Mocha. To do so, export the job code and run standard unit tests. */} ### Config The first parameter of the `createFunction` method specifies Inngest function's configuration. In the above example, the `id` is specified, which will be used to identify the function in the Inngest system. You can see this ID in the [Inngest Dev Server's](/docs/local-development) function list: [IMAGE] You can also provide other [configuration options](/docs/reference/functions/create#configuration), such as `concurrency`, `throttle`, `debounce`, `rateLimit`, `priority`, `batchEvents`, or `idempotency` (learn more about [Flow Control](/docs/guides/flow-control)). You can also specify how many times the function will retry, what callback function will run on failure, and when to cancel the function. ### Trigger Inngest functions are designed to be triggered by events or crons (schedules). Events can be [sent from your own code](/docs/events) or received from third party webhooks or API requests. When an event is received, it triggers a corresponding function to execute the tasks defined in the function handler (see the ["Handler" section](#handler) below). Each function needs at least one trigger. However, you can also work with [multiple triggers](/docs/guides/multiple-triggers) to invoke your function whenever any of the events are received or cron schedule occurs. ### Handler A "handler" is the core function that defines what should happen when the function is triggered. The handler receives context, which includes the event data, tools for managing execution flow, or logging configuration. Let's take a closer look at them. #### `event` Handler has access to the data which you pass when sending events to Inngest via [`inngest.send()`](/docs/reference/events/send) or [`step.sendEvent()`](/docs/reference/functions/step-send-event). You can see this in the example above in the `event` parameter. #### `step` [Inngest steps](/docs/learn/inngest-steps) are fundamental building blocks in Inngest functions. They are used to manage execution flow. Each step is a discrete task, which can be executed, retried, and recovered independently, without re-executing other successful steps. It's helpful to think of steps as code-level transactions. If your handler contains several independent tasks, it's good practice to [wrap each one in a step](/docs/guides/multi-step-functions). In this way, you can manage complex state easier and if any task fails, it will be retried independently from others. There are several step methods available at your disposal, for example, `step.run`, `step.sleep()`, or `step.waitForEvent()`. In the example above, the handler contains two steps: `copy-images-to-s3` and `resize-images`. ### Config The first parameter of the `createFunction` method specifies Inngest function's configuration. In the above example, the `id` is specified, which will be used to identify the function in the Inngest system. You can see this ID in the [Inngest Dev Server's](/docs/local-development) function list: [IMAGE] You can also provide other [configuration options](https://pkg.go.dev/github.com/inngest/inngestgo#CreateFunction), such as `Concurrency`, `Throttle`, `Debounce`, `RateLimit`, `Priority`, `BatchEvents`, or `Idempotency` (learn more about [Flow Control](/docs/guides/flow-control)). You can also specify how many times the function will retry, what callback function will run on failure, and when to cancel the function. ### Trigger Inngest functions are designed to be triggered by events or crons (schedules). Events can be [sent from your own code](/docs/events) or received from third party webhooks or API requests. When an event is received, it triggers a corresponding function to execute the tasks defined in the function handler (see the ["Handler" section](#handler) below). Each function needs at least one trigger. However, you can also work with [multiple triggers](/docs/guides/multiple-triggers) to invoke your function whenever any of the events are received or cron schedule occurs. ### Handler A "handler" is the core function that defines what should happen when the function is triggered. The handler receives context, which includes the event data, tools for managing execution flow, or logging configuration. Let's take a closer look at them. #### `event` Handler has access to the data which you pass when sending events to Inngest via [`inngest.Send()`](https://pkg.go.dev/github.com/inngest/inngestgo#Send). You can see this in the example above in the `event` parameter. #### `step` [Inngest steps](/docs/learn/inngest-steps) are fundamental building blocks in Inngest functions. They are used to manage execution flow. Each step is a discrete task, which can be executed, retried, and recovered independently, without re-executing other successful steps. It's helpful to think of steps as code-level transactions. If your handler contains several independent tasks, it's good practice to [wrap each one in a step](/docs/guides/multi-step-functions). In this way, you can manage complex state easier and if any task fails, it will be retried independently from others. There are several step methods available at your disposal, for example, `step.Run`, `step.Sleep()`, or `step.WaitForEvent()`. In the example above, the handler contains two steps: `copy-images-to-s3` and `resize-images`. ### Config The first parameter of the `createFunction` method specifies Inngest function's configuration. In the above example, the `id` is specified, which will be used to identify the function in the Inngest system. You can see this ID in the [Inngest Dev Server's](/docs/local-development) function list: [IMAGE] You can also provide other [configuration options](/docs/reference/python/functions/create), such as `concurrency`, `throttle`, `debounce`, `rateLimit`, `priority`, `batchEvents`, or `idempotency` (learn more about [Flow Control](/docs/guides/flow-control)). You can also specify how many times the function will retry, what callback function will run on failure, and when to cancel the function. ### Trigger Inngest functions are designed to be triggered by events or crons (schedules). Events can be [sent from your own code](/docs/events) or received from third party webhooks or API requests. When an event is received, it triggers a corresponding function to execute the tasks defined in the function handler (see the ["Handler" section](#handler) below). Each function needs at least one trigger. However, you can also work with [multiple triggers](/docs/guides/multiple-triggers) to invoke your function whenever any of the events are received or cron schedule occurs. ### Handler A "handler" is the core function that defines what should happen when the function is triggered. The handler receives context, which includes the event data, tools for managing execution flow, or logging configuration. Let's take a closer look at them. #### `event` Handler has access to the data which you pass when sending events to Inngest via [`inngest.send()`](/docs/reference/python/client/send) or [`step.send_event()`](/docs/reference/python/functions/step-send-event). You can see this in the example above in the `event` parameter. #### `step` [Inngest steps](/docs/learn/inngest-steps) are fundamental building blocks in Inngest functions. They are used to manage execution flow. Each step is a discrete task, which can be executed, retried, and recovered independently, without re-executing other successful steps. It's helpful to think of steps as code-level transactions. If your handler contains several independent tasks, it's good practice to [wrap each one in a step](/docs/guides/multi-step-functions). In this way, you can manage complex state easier and if any task fails, it will be retried independently from others. There are several step methods available at your disposal, for example, `step.run`, `step.sleep()`, or `step.wait_for_event()`. In the example above, the handler contains two steps: `copy-images-to-s3` and `resize-images`. ## Kinds of Inngest functions ### Background functions {{anchor: false}} Long tasks can be executed outside the critical path of the main flow, which improves app's performance and reliability. Perfect for communicating with third party APIs or executing long-running code. ### Scheduled functions {{anchor: false}} Inngest's scheduled functions enable you to run tasks automatically at specified intervals using cron schedules. These functions ensure consistent and timely execution without manual intervention. Perfect for routine operations like sending weekly reports or clearing caches. ### Delayed functions {{anchor: false}} You can enqueue an Inngest function to run at a specific time in the future. The task will be executed exactly when needed without manual intervention. Perfect for actions like sending follow-up emails or processing delayed orders. ### Step functions {{anchor: false}} Step functions allow you to create complex workflows. You can coordinate between multiple steps, including waiting for other events, delaying execution, or running code conditionally based on previous steps or incoming events. Each [step](/docs/learn/inngest-steps) is individually retriable, making the workflow robust against failures. Ideal for scenarios like onboarding flows or conditional notifications. ### Fan-out functions {{anchor: false}} Inngest's fan-out jobs enable a single event to trigger multiple functions simultaneously. Ideal for parallel processing tasks, like sending notifications to multiple services or processing data across different systems. ## Invoking functions directly You can [call an Inngest function directly](/docs/guides/invoking-functions-directly) from within your event-driven system by using `step.invoke()`, even across different Inngest SDKs. This is useful when you need to break down complex workflows into simpler, manageable parts or when you want to leverage existing functionality without duplicating code. Direct invocation is ideal for orchestrating dependent tasks, handling complex business logic, or improving code maintainability and readability. ## Further reading - [Quick Start guide](/docs/getting-started/nextjs-quick-start?ref=docs-inngest-functions): learn how to build complex workflows. - ["How Inngest functions are executed"](/docs/learn/how-functions-are-executed): learn more about Inngest's execution model. - ["Inngest steps"](/docs/learn/inngest-steps): understand building Inngest's blocks. - ["Flow Control"](/docs/guides/flow-control): learn how to manage execution within Inngest functions. # Inngest Steps Source: https://www.inngest.com/docs/learn/inngest-steps Description: Learn about Inngest steps and their methods.'; import { GuideSelector, GuideSection } from How Inngest functions are executed"](/docs/learn/how-functions-are-executed) page. # 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" }, { 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); }); } ); ``` 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. Please note that each step is executed as **a separate HTTP request**. To ensure efficient and correct execution, place any non-deterministic logic (such as DB calls or API calls) within a `step.run()` call. ## Available Step Methods ###
step.run()
{{anchor: false}} 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" }, { 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); }); } ); ``` `step.run()` acts as a code-level transaction. The entire step must succeed to complete. ###
step.sleep()
{{anchor: false}} 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" }, { event: "app/user.signup" }, async ({ event, step }) => { await step.sleep("wait-a-couple-of-days", "2d"); // Do something else } ); ``` ###
step.sleepUntil()
{{anchor: false}} 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" }, { event: "app/reminder.scheduled" }, async ({ event, step }) => { new Date(event.data.remind_at); await step.sleepUntil("wait-for-the-date", date); // Do something else } ); ``` ###
step.waitForEvent()
{{anchor: false}} This method pauses the execution until a specific event is received. ```typescript export default inngest.createFunction( { id: "send-onboarding-nudge-email" }, { 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 } ); ``` ###
step.invoke()
{{anchor: false}} 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" }, { 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" }, { 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 } ); ``` ###
step.sendEvent()
{{anchor: false}} 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" }, { 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 } ); ```
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. ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( // 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) (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 }, ) ``` 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. Please note that each step is executed as **a separate HTTP request**. To ensure efficient and correct execution, place any non-deterministic logic (such as DB calls or API calls) within a `step.run()` call. ## Available Step Methods ###
step.Run()
{{anchor: false}} 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. ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( &inngestgo.FunctionOpts{ ID: "import-product-images", }, inngestgo.EventTrigger("shop/product.imported", nil), func(ctx context.Context, input inngestgo.Input) (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 }, ) ``` `step.Run()` acts as a code-level transaction. The entire step must succeed to complete. ###
step.Sleep()
{{anchor: false}} This method pauses execution for a specified duration. 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)). ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( &inngestgo.FunctionOpts{ ID: "send-delayed-email", }, inngestgo.EventTrigger("app/user.signup", nil), // handler function func(ctx context.Context, input inngestgo.Input) (any, error) { step.Sleep("wait-a-couple-of-days", 2 * time.Day) }, ) ``` ###
step.WaitForEvent()
{{anchor: false}} This method pauses the execution until a specific event is received. ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/errors" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( &inngestgo.FunctionOpts{ ID: "send-delayed-email", }, inngestgo.EventTrigger("app/user.signup", nil), // handler function func(ctx context.Context, input inngestgo.Input) (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 } }, ) ``` ###
step.Invoke()
{{anchor: false}} 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. ```go import ( "github.com/inngest/inngestgo" "github.com/inngest/inngestgo/errors" "github.com/inngest/inngestgo/step" ) inngestgo.CreateFunction( &inngestgo.FunctionOpts{ ID: "send-delayed-email", }, inngestgo.EventTrigger("app/user.signup", nil), // handler function func(ctx context.Context, input inngestgo.Input) (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 }, ) ```
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. ```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, step: inngest.Step): uploaded_image_urls = await 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. Please note that each step is executed as **a separate HTTP request**. To ensure efficient and correct execution, place any non-deterministic logic (such as DB calls or API calls) within a `step.run()` call. ## Available Step Methods ###
step.run()
{{anchor: false}} 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. ```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, step: inngest.Step): # Here goes the business logic # By wrapping code in steps, it will be retried automatically on failure uploaded_image_urls = await 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()
{{anchor: false}} This method pauses execution for a specified duration. 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)). ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="send-delayed-email", trigger=inngest.Trigger(event="app/user.signup") ) async def send_delayed_email(ctx: inngest.Context, step: inngest.Step): await step.sleep("wait-a-couple-of-days", datetime.timedelta(days=2)) # Do something else ``` ###
step.sleep_until()
{{anchor: false}} 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)). ```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.Trigger(event="app/reminder.scheduled") ) async def send_scheduled_reminder(ctx: inngest.Context, step: inngest.Step): date = datetime.fromisoformat(ctx.event.data["remind_at"]) await step.sleep_until("wait-for-the-date", date) # Do something else ``` ###
step.wait_for_event()
{{anchor: false}} This method pauses the execution until a specific event is received. ```python import inngest from src.inngest.client import inngest_client @inngest_client.create_function( fn_id="send-onboarding-nudge-email", trigger=inngest.Trigger(event="app/account.created") ) async def send_onboarding_nudge_email(ctx: inngest.Context, step: inngest.Step): onboarding_completed = await 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.invoke()
{{anchor: false}} 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. ```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, step: inngest.Step, ) -> 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, step: inngest.Step, ) -> None: output = await step.invoke( "invoke", function=fn_1, ) # Prints "Hello!" print(output) ``` ###
step.send_event()
{{anchor: false}} This method sends events to Inngest to invoke functions with a matching event. Use `send_event()` 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). ```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, step: inngest.Step, ) -> list[str]: return await 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/guides/multi-step-functions). # Security Source: https://www.inngest.com/docs/learn/security 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. ## 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 [contact our team](/contact?ref=docs-security). ## Signing keys and SDK security Firstly, it's important to understand that in production all communication between Inngest and your servers is encrypted via TLS. The SDK also actively mitigates attacks by use of the [signing key](/docs/platform/signing-keys), a secret pre-shared key unique to each environment. By default, the signing key adds the following: - **Authentication**: requests to your endpoint are authenticated, ensuring that they originate from Inngest. Inngest SDKs reject all requests that are not authenticated with the signing key. - **Replay attack prevention:** requests are signed with a timestamp embedded, and old requests are rejected, even if the requests are authenticated correctly. **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/signing-keys#rotation) with zero downtime. ### Function registration + handshake Functions are defined and served on your own infrastructure using one of our SDKs. In order to run your functions, they must be [synced](/docs/apps/cloud), or registered, with your Inngest account. This is required for Inngest to know which functions your application is serving and the configuration of each function. Syncing functions is done via a secure handshake. Here's how the handshake works: 1. After your SDK's endpoint is live, a PUT request to the endpoint initiates a secure handshake with the Inngest servers. 2. The SDK sends function configuration to Inngest's API, with the signing key as a bearer token. 3. The SDK idempotently updates your apps and functions. If there are no changes, nothing happens. This process is necessary for several reasons, largely as serverless environments are the lowest common denominator. Serverless environments have no default bootup/init process, which means serverless environments can't self-initiate the sync. Secondly, serverless platforms such as AWS Lambda and Vercel create unique URLs for each function deployed, which can't be known ahead of time. The incoming PUT request allows the SDK to introspect the request's URL, which is then used for all function calls. Note that because the SDK only sends HTTP requests to `api.inngest.com` to complete the sync, it never leaks the signing key to clients attempting registration, keeping your key secure. ## End to end encryption 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 is a middleware](/docs/features/middleware/encryption-middleware) which 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. 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. ## 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. If you're not on an enterprise plan, [contact us here](/contact?ref=docs-security), and we'll get you set up. There is no additional charge for SAML authentication below 200 users. # Setting up your Inngest app Source: https://www.inngest.com/docs/learn/serving-inngest-functions description = `Serve the Inngest API as an HTTP endpoint in your application.` hidePageSidebar = true; With Inngest, you define functions or workflows using the SDK and deploy them to whatever platform or cloud provider you want including including serverless and container runtimes. For Inngest to remotely execute your functions, you will need to set up a connection between your app and Inngest. This can be done in one of two ways:

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.
  • Zero changes to your CI/CD pipeline

Connect to Inngest's servers using out-bound 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/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) * [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) * [SvelteKit](#framework-svelte-kit) 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 {{ title: "v3" }} { 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 `serveHost` 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/serve#serve-client-functions-options) for more details. ```ts {{ title: "v3" }} // Your own function handler = serve({ client: inngest, functions: [fnA], }); ``` ```ts {{ title: "v2" }} // Your own function handler = serve(inngest, [fnA]); ``` ### Bun.serve() You can use the `inngest/bun` handler with `Bun.serve()` for a lightweight Inngest server: ```ts Bun.serve({ port: 3000, fetch(request: Request) { new URL(request.url); if (url.pathname === "/api/inngest") { return serve({ client: inngest, functions })(request); } return new Response("Not found", { status: 404 }); }, }); ``` 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 {{ title: "v3" }} // Your own function onRequest = serve({ client: inngest, functions: [fnA], }); ``` ```ts {{ title: "v2" }} // Your own function onRequest = serve({ client: inngest, functions: [fnA], }); ``` ### 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", }), }; ``` 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_HOST = "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 {{ title: "v3" }} // Your own function serve({ client: inngest, functions: [fnA], // Your digitalocean hostname. This is required otherwise your functions won't work. serveHost: "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; ``` ```ts {{ title: "v2" }} // Your own function serve(inngest, [fnA], { // Your digitalocean hostname. This is required otherwise your functions won't work. serveHost: "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; ``` ### 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 {{ title: "v3" }} // 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] }) ); ``` ```ts {{ title: "v2" }} // 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(inngest, [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. ### 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 (v3)" }} 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); } }); ``` ```ts {{ title: "Custom route (v2)" }} Fastify(); fastify.route({ method: ["GET", "POST", "PUT"], handler: serve(inngest, [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 {{ title: "v3" }} // Your own function handler = serve({ client: inngest, functions: [fnA], }); ``` ```ts {{ title: "v2" }} // Your own function handler = serve(inngest, [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 {{ title: "v3" }} // Your own function ff.http( "inngest", serve({ client: inngest, functions: [fnA], servePath: "/", }) ); ``` ```ts {{ title: "v2" }} // Your own function ff.http( 'inngest', serve( inngest, [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 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 {{ title: "v3" }} createApp(); app.use( "/api/inngest", eventHandler( serve({ client: inngest, functions: [fnA], }) ) ); createServer(toNodeListener(app)).listen(process.env.PORT || 3000); ``` ```ts {{ title: "v2" }} createApp(); app.use("/api/inngest", eventHandler(serve(inngest, [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 {{ title: "v3" }} 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 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: "allow"` option to your serve handler: **Next.js 13+** ```ts runtime = "edge"; { GET, POST, PUT } = serve({ client: inngest, functions: [...fns], streaming: "allow", }); ```
**Older versions (Next.js 12)** ```ts {{ title: "v3" }} config = { runtime: "edge", }; serve({ client: inngest, functions: [...fns], streaming: "allow", }); ``` ```ts {{ title: "v2" }} config = { runtime: "edge", }; serve(inngest, [...fns], { streaming: "allow", }); ```
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 {{ title: "v3" }} // Your own function export default defineEventHandler( serve({ client: inngest, functions: [fnA], }) ); ``` ```ts {{ title: "v2" }} // Your own function export default defineEventHandler( serve(inngest, [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 {{ title: "v3" }} // Your own function handler = serve({ client: inngest, functions: [fnA], servePath: "/api/inngest", }); ``` ```ts {{ title: "v2" }} // Your own function handler = serve( inngest, [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 {{ title: "v3" }} // app/routes/api.inngest.ts serve({ client: inngest, functions: [fnA], }); export { handler as action, handler as loader }; ``` ```ts {{ title: "v2" }} // app/routes/api.inngest.ts serve(inngest, [fnA]); export { handler as loader, handler as action }; ``` 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: "allow"` option to your serve handler: ```ts {{ title: "v3" }} config = { runtime: "edge", }; serve({ client: inngest, functions: [...fns], streaming: "allow", }); ``` ```ts {{ title: "v2" }} config = { runtime: "edge", }; serve(inngest, [...fns], { streaming: "allow", }); ``` For more information, check out the [Streaming](/docs/streaming) page. ### Framework: SvelteKit Add the following to `./src/routes/api/inngest/+server.ts`: ```ts {{ title: "v3" }} 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. ### 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" }, { 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() { h := inngestgo.NewHandler("core", inngestgo.HandlerOpts{}) f := inngestgo.CreateFunction( inngestgo.FunctionOpts{ ID: "account-created", Name: "Account creation flow", }, // Run on every api/account.created event. inngestgo.EventTrigger("api/account.created", nil), AccountCreated, ) h.Register(f) http.ListenAndServe(":8080", h) } ``` 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.Context, step: inngest.StepSync, ) -> 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, step: inngest.Step, ) -> 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 provide this as a signing key, you can pass it in to `serve` when setting up your framework. [Read the reference for more information](/docs/sdk/reference/serve#reference). ## Reference For more information about the `serve` handler, read the [the reference guide](/docs/reference/serve), which includes: * [`server()` configuration options](/docs/reference/serve#serve-client-functions-options) * [How the serve handler works](/docs/reference/serve#how-the-serve-api-handler-works) # Versioning Source: https://www.inngest.com/docs/learn/versioning Description: Learn how . Long-running functions inevitably change over time. Inngest enables developers to implement multiple strategies for changing long-running code over time. To manage these changes effectively, it's crucial to understand how the SDK implements determinism and [executes steps](/docs/learn/how-functions-are-executed). ## Determinism in functions Determinism is consistent in *every* Inngest language SDK. Except for language-specific idioms, all SDKs implement the same logic. In every SDK, functions in Inngest are a series of steps. Each step runs reliably, will retry on failure, and is as close to exactly-once execution as possible (excluding outbound network failures when reporting completed steps). ### How the SDK works with steps As covered in [_How Inngest functions are executed_](/docs/learn/how-functions-are-executed), each step in a function has a unique identifier, represented as a string. Each time a step is found, the SDK checks whether the step has been executed. It does this by: 1. Hashing the step's unique identifier along with a counter of the number of times the step has been called. This enables steps to be used in a loop. 2. Looking up the resulting hash in function run state. 1. If the hash is present, the step has been executed. The SDK returns the memoized state and skips execution. 2. If the hash isn't found, the SDK executes the step and returns the output to Inngest to be stored in the function run state. After a step completes, the function execution immediately ends. The function is re-executed from the top with the updated memoized state until the function completes. ### Handling determinism The SDK handles determinism *gracefully* by default. The SDK keeps track of the order in which every step is executed. If new steps are added, they're executed when they're first discovered. This means that: - The SDK always knows if functions are deterministic, even over months or years. - **New steps, or steps with changed IDs, are executed when they're discovered.** If the order of step executions change, a warning is logged by default{/* (your functions can be made to permanently fail by enabling strict mode)*/}. Logging a warning allows you to comfortably extend and improve functions over time, without worrying about in-progress functions failing completely or panicking. ## Change management across versions Given the above, there are a few strategies for change management: - **Adding new steps to a function is generally safe.** New steps will be executed when the functions re-run (after a step completes). Imagine a function has steps `[A, B, C]`. When you add a new step `Z` in-between the first steps, the executor will run steps `[A, Z, B, C]` and log a warning. The caveat here is that you must take care to ensure that the new step can run out-of-order and doesn't reference undefined variables. Note that step `B` and `C` will *not* automatically re-run. Instead, a warning will be logged by default. You can change logging a warning and instead permanently fail by enabling strict mode. Failing runs permanently is acceptable, and you can use [Replay](/docs/platform/replay) to bulk-replay permanent failures. - **Forcing steps to re-run by changing step IDs.** This changes the hash, which forces re-evaluation as the step's state is not found. Note that the SDK will log a warning by default as the order of step execution changes. If you change step `C`'s ID to `E`, your run's state will expect steps `[A, B, C]` to run and instead will see `[A, B, E]`. - **For complete changes in logic, create a new function which subscribes to the same triggering event**. Update the existing function's trigger to include an [`if` expression](/docs/reference/functions/create#trigger) to only handle events before a certain [timestamp](/docs/events#event-payload-format) (for example: `event.ts < ${EPOCH_MS}`). Then create a new function with the updated logic, the same original event trigger, and a new `id` (for example: `process-upload-v2`). This allows you to safely transition to the new function without losing any data. A caveat is that this creates a new function in your app and therefore the Inngest dashboard. ## Conclusion Understanding how determinism works should allow you to gracefully evolve functions over time. Consider these strategies when making changes to long-running functions to ensure that they can run successfully to completion over time. # Local development Source: https://www.inngest.com/docs/local-development import { RiFunctionLine, RiQuestionLine, RiTerminalBoxLine, } from "@remixicon/react"; Inngest's tooling makes it easy to develop your functions locally with any framework using the Inngest Dev Server. The [Inngest Dev Server](/docs/dev-server) is a fully-featured and [open-source](https://github.com/inngest/inngest) local version of the [Inngest Platform](/docs/platform/deployment) enabling a seamless transition from your local development to feature, staging and production environments. ![The Inngest Dev Server on the Functions page](/assets/docs/features/local-development/DevServer-functions.png) Get started with your favorite setup: } href={'/docs/dev-server'}> Start Inngest locally with a single command. } href={'/docs/guides/development-with-docker'}> Run Inngest locally using Docker or Docker Compose. ## Development Flow with Inngest The Inngest Dev Server provides all the features available in the Inngest Platform, guaranteeing a smooth transition from local dev to production environments. Developing with Inngest looks as it follows: 1. [Configure the Inngest SDK in your application](/docs/sdk/overview) 2. [Connecting the Inngest Dev Server to your local application](/docs/learn/inngest-functions) 3. Develop your Inngest Functions with [Steps](/docs/learn/inngest-steps), [Flow Control](/docs/guides/flow-control) and [more](/docs/features/inngest-functions) 5. _(Optional) - Configure Preview environments with [our Vercel Integration](/docs/deploy/vercel)_ **Moving to production environments (preview envs, staging or production)** Deploying your application to preview, staging and production environments does not require any code change: 6. [Create an Inngest App](/docs/apps) on the Inngest Platform and [configure its Event and Signing Keys on your Cloud](/docs/platform/deployment). 7. Leverage the Inngest Platform to manage and monitor Events and Function Runs ## CLI and SDKs {/* } href={'/todo'}> Explore the options to configure the Inngest Dev Server */} } href={'/docs/reference/typescript'}> Setup the Inngest SDK in your TypeScript application. } href={'/docs/reference/python'}> Setup the Inngest SDK in your Python application. } href={'https://pkg.go.dev/github.com/inngest/inngestgo'}> Setup the Inngest SDK in your Go application. ## FAQs }> The Inngest Dev Server is not designed to be run in production, but you can run it anywhere that you want including testing environments or CI/CD pipelines. }> Webhooks configured on the Platform [can be sent to the Dev Server](/docs/platform/webhooks#local-development). }> External webhooks from Stripe and Clerk must go through a tunnel solution (such as [ngrok](https://ngrok.com/) or [localtunnel](https://theboroer.github.io/localtunnel-www/)) to reach the Dev Server. }> Yes. You can also trigger a function at any time by [using the "Invoke" button from the Dev Server Functions list view](/docs/dev-server). {/* }> Please take a look at our [Dev Server Troubleshooting guide](#). */} Find more answers in our [Discord community](/discord). # Deployment Source: https://www.inngest.com/docs/platform/deployment import { RiCloudLine, } from "@remixicon/react"; Moving to production requires deploying your Inngest Functions on your favorite Cloud Provider and configuring it to allow the Inngest Platform to orchestrate runs: } href={'/docs/apps/cloud#sync-a-new-app-in-inngest-cloud'}> Inngest Functions can be deployed to any serverless cloud or container running on any server. } href={'/docs/deploy/vercel'}> Use our Vercel Integration to deploy your Inngest Functions. } href={'/docs/deploy/cloudflare'}> Deploy your Inngest Functions on Cloudflare Pages. } href={'/docs/deploy/render'}> Deploy your Inngest Functions on Render. ## How Inngest handles Function Runs The Inngest Platform hosts the Inngest Durable Execution Engine, responsible for triggering and maintaining the state of **Function runs happening on your Cloud Provider**: The Inngest Platform relies on [Event](/docs/events/creating-an-event-key) and [Signing Keys](/docs/platform/signing-keys), as well as [other security mechanisms](/docs/learn/security), to communicate securely and reliably with the Inngest SDK. Learn more on Inngest's Durable Execution Engine in our ["How Inngest Functions are executed" guide](/docs/learn/how-functions-are-executed). # Environments Source: https://www.inngest.com/docs/platform/environments Inngest accounts all have multiple environments that help support your entire software development lifecycle. Inngest has different types of environments: - **Production Environment** for all of your production applications, functions and event data. - [**Branch Environments**](#branch-environments) are sandbox environments that enables developers on your team to test your changes specific to current Git feature branch. These are designed to work with platforms that support branch-based deployment previews like Vercel or Netlify. - [**Custom Environments**](#custom-environments) are used to create shared, non-production environments like staging, QA, or canary. - [**Local Environment**](/docs/local-development) leverages the Inngest Dev Server (`npx inngest-cli@latest dev`) to test and debug functions on your own machine. The key things that you need to know about environments: - Data is isolated within each environment. Event types or functions may share the same name, but their data and logs are fully separated. - Each environment uses [Event Keys](/docs/events/creating-an-event-key) and [Signing Keys](/docs/platform/signing-keys) to securely send data or sync apps within a given environment. - You can sync multiple applications with each environment. {/*Learn more about using Inngest with multiple applications here.*/} - You are billed for your usage across all environments, _except of course your local environment_. ## Branch Environments Most developer workflows are centered around branching, whether feature branches or a variant of GitFlow. Inngest's Branch Environments are designed to give you and your team an isolated sandbox for every non-production branch that you deploy. For example, ![Branch Environments mapping to your hosting platform's deployment previews](/assets/docs/environments/branch-environments-with-your-platform.svg) Branch deployments: - Are created on-demand when you send events or register your functions for a given environment - Share Event Keys and Signing Keys to streamline your developer workflow (see: Configuring Branch Environments) It can be helpful to visualize the typical Inngest developer workflow using Branch environments and your platform's deploy previews: ![The software development lifecycle from local development to Branch Environments to Production](/assets/docs/environments/branch-environments-sdlc.svg) ## Configuring Branch Environments As Branch Environments are created on-demand, all of your Branch Environments share the same Event Keys and Signing Key. This enables you to use the same environment variables in each of your application's deployment preview environments and set the environment dynamically using the `env` option with the `Inngest` client: ```ts {{ title: "TypeScript" }} new Inngest({ id: "my-app", env: process.env.BRANCH, }); // Alternatively, you can set the INNGEST_ENV environment variable in your app // Pass the client to the serve handler to complete the setup serve({ client: inngest, functions: [myFirstFunction, mySecondFunction] }); ``` ```python {{ title: "Python" }} import inngest inngest_client = inngest.Inngest( app_id="flask_example", env=os.getenv("BRANCH"), ) ``` ### Automatically Supported Platforms The Inngest SDK tries to automatically detect your application's branch and use it to set the `env` option when deploying to certain supported platforms. Here are the platforms that are automatically supported and what environment variable is _automatically_ used: - **Vercel** - `VERCEL_GIT_COMMIT_REF` - This works perfectly with our Vercel integration. You can always override this using [`INNGEST_ENV`](/docs/sdk/environment-variables#inngest-env) or by manually passing `env` to the `Inngest` client. ### Other Platforms Some platforms only pass an environment variable at build time. This means you'll have to explicitly set `env` to the platform's specific environment variable. For example, here's how you would set it on Netlify: ```ts {{ title: "TypeScript" }} new Inngest({ id: "my-app", env: process.env.BRANCH, }); ``` ```python {{ title: "Python" }} import inngest inngest_client = inngest.Inngest( app_id="flask_example", env=os.getenv("BRANCH"), ) ``` - **Netlify** - `BRANCH` ([docs](https://docs.netlify.com/configure-builds/environment-variables/#git-metadata)) - **Cloudflare Pages** - `CF_PAGES_BRANCH` ([docs](https://developers.cloudflare.com/pages/platform/build-configuration/#environment-variables)) - **Railway** - `RAILWAY_GIT_BRANCH` ([docs](https://docs.railway.app/develop/variables#railway-provided-variables)) - **Render** - `RENDER_GIT_BRANCH` ([docs](https://render.com/docs/environment-variables#all-services)) ### Sending Events to Branch Environments As all branch environments share Event Keys, all you need to do to send events to your branch environment is set the `env` option with the SDK. This will configure the SDK's `send()` method to automatically routes events to the correct environment. If you are sending events without an Inngest SDK, you'll need pass the `x-inngest-env` header along with your request. For more information about this and sending events from any environment with **the Event API**, [read the `send()` reference](/docs/reference/events/send#send-events-via-http-event-api). ### Archiving Branch Environments By default, branch environments are archived 3 days after their latest deploy. Each time you deploy, the auto archive date is extended by 3 days. Archiving a branch environment doesn't delete anything; it only prevents the environment's functions from triggering. If you'd like to disable auto archive on a branch environment, click the toggle in the [environments page](https://app.inngest.com/env). There's also a button that lets you manually archive/unarchive branch environments at any time. ### Disabling Branch Environments in Vercel The recommended way to disable branch environments is through the Vercel UI. Delete the "Preview" Inngest environment variables: ![Vercel environment keys](/assets/docs/environments/environment-keys.jpg) ## Custom Environments Many teams have shared environments that are used for non-production purposes like staging, QA, or canary. Inngest's Custom Environments are designed to give you and your team an isolated sandbox for every non-production environment that you deploy. You can create an environment from the [environments page](https://app.inngest.com/env) in the Inngest dashboard. Some key things to know about custom environments: * Each environment has its own keys, event history, and functions. All data is isolated within each environment. * You can deploy multiple apps to the same environment to fully simulate your production environment. * You can create as many environments as you need. [Create an environment in the dashboard](https://app.inngest.com/create-environment) ## Viewing and Switching Environments In the Inngest dashboard you can quickly switch between environments in the environment switcher dropdown in the top navigation. You can click “[View All Environments](https://app.inngest.com/env)” for a high level view of all of your environments. ![The environment switcher dropdown menu in the Inngest dashboard](/assets/docs/environments/branch-dropdown.png) # Platform Guides Source: https://www.inngest.com/docs/platform/index hidePageSidebar = true; Learn how to use the Inngest platform # Apps Source: https://www.inngest.com/docs/platform/manage/apps Inngest enables you to manage your Inngest Functions deployments via [Inngest Environments and Apps](/docs/apps). Inngest Environments (ex, production, testing) can contain multiple Apps that can be managed using: - [The Apps Overview](#apps-overview) - A quick access to all apps, including the unattached syncs - [Syncs management](#syncs) - Run App diagnostics and access all syncs history - [Archiving](#archive-an-app) - Archive inactive Apps ## Apps Overview The Apps Overview is the main entry page of the Inngest Platform, listing the Apps of the active Environment, visible in the top left Environment selector: ![The home page of the Inngest Platform is an Apps listing. Each App item display the App status along with some essential information such as active Functions count and the SDK version used.](/assets/docs/platform/manage/environments-apps/apps-overview.png) The Apps Overview provides all the essential information to assess the healthy sync status of your active Apps: Functions identified, Inngest SDK version. You can switch to “Archived Apps” using the top left selector. ### Unattached Syncs Automatic syncs or an App misconfiguration can result in syncs failing to attach to an existing app. These unsuccessful syncs are listed in the “Unattached Syncs” section where detailed information are available, helping in resolving the issue: ![The Unattached Syncs list provides detailed information regarding failed syncs.](/assets/docs/platform/manage/environments-apps/unattached-syncs.png) Please read our [Syncs Troubleshooting section](/docs/apps/cloud#troubleshooting) for more information on how to deal with failed sync. ## App management ### Overview Navigating to an App provides more detailed information (SDK version and language, deployment URL) that can be helpful when interacting with our Support. You will also find quick access to all active functions and their associated triggers: ![Clicking on an App from the home page will give you more detailed information about the current App deployment such as: the Functions list, the target URL. Those information can be useful when exchanging with Support.](/assets/docs/platform/manage/environments-apps/app-overview.png) ### Syncs Triggering a Manual Sync from the Inngest Platform sends requests to your app to fetch the up-to-date configuration of your applications's functions. At any time, access the history of all syncs from the App page: ![The list of an App Syncs provide helpful information to navigate through recent deployments and their associated Functions changes.](/assets/docs/platform/manage/environments-apps/app-syncs.png) If a sync fails, try running an App Diagnostic before reaching out to Support: ![A App Diagnostic tool is available to help mitigating any sync issues. You can access it by opening the top left menu from the App Syncs listing page.](/assets/docs/platform/manage/environments-apps/check-app-health.png) ### Archive an app Apps can be archived and unarchived at any time. Once an app is archived, all of its functions are archived. When the app is archived: - New function runs will not be triggered. - Existing function runs will continue until completion. - Functions will be marked as archived, but will still be visible, including their run history. If you need to cancel all runs prior to completion, read our [cancellation guide](/docs/guides/cancel-running-functions). **How to archive an app** 1. Navigate to the app you want to archive. You will find an “Archive” button at the top-right corner of the page. ![Archiving an app is accessible from an App page by using the top left menu.](/assets/docs/platform/manage/environments-apps/archive-app.png) 2. Confirm that you want to archive the app by clicking "Yes". ![A confirmation modal will open to confirm the action. Please note that archiving is not an irreversible action.](/assets/docs/platform/manage/environments-apps/archiving-app.png) 3. Your app is now archived. 🎉 ![An archived App features a top informative banner.](/assets/docs/platform/manage/environments-apps/archived-app.png) In the image below, you can see how archived apps look like in Inngest Cloud: ![An archived App is still accessible from the Home page, by switching the top left filter to "Archived Apps".](/assets/docs/platform/manage/environments-apps/archived-apps.png) # Function runs Bulk Cancellation Source: https://www.inngest.com/docs/platform/manage/bulk-cancellation In addition to providing [SDK Cancellation features](/docs/features/inngest-functions/cancellation/cancel-on-events) and a [dedicated REST API endpoint](/docs/guides/cancel-running-functions), the Inngest Platform also features a Bulk Cancellation UI. This feature comes in handy to quickly stop unwanted runs directly from your browser. This feature is now generally available. [Read the announcement here](/blog/bulk-cancellation). ## Cancelling Function runs To cancel multiple Function runs, navigate to the Function's page on the Inngest Platform and open the “All actions” top right menu: ![The bulk cancellation button can be found from a Function page, in the top right menu.](/assets/docs/platform/manage/bulk-cancellation/function-runs-cancel-button.png) Clicking the “Bulk cancel” menu will open the following modal, asking you to select the date range that will be used to select and cancel Function runs: ![The Bulk cancel modal is composed, from top to bottom, of an input to name the cancellation process and a date range selector. Once those information filled, a estimation of the impacted Function Runs. The cancellation cannot be started if no Function runs match the criteria.](/assets/docs/platform/manage/bulk-cancellation/bulk-cancel-modal.png) The Bulk Cancellation will start cancelling the matching Function runs immediately; the Function runs list will update progressively, showing runs as cancelled: ![Once the Bulk Cancellation completed, the impacted Function Runs will appear as "cancelled" in the Function Runs list.](/assets/docs/platform/manage/bulk-cancellation/function-runs-cancelled.png) You can access the history of running or completed Bulk Cancellation processes via the "Cancellation history" tab: ![The "Cancellation history" tab lists all the Bulk Cancellations.](/assets/docs/platform/manage/bulk-cancellation/bulk-cancel-history.png) **Considerations** Cancellation is useful to stop running Functions or cancel scheduled ones; however, keep in mind that: - Steps currently running on your Cloud Provider won't be forced to stop; the Function will cancel upon the current step's completion. - Cancelling a Function run does not prevent new runs from being enqueued. If you are looking to mitigate an unwanted loop or to cope with an abnormal number of executions, consider using [Function Pausing](/docs/guides/pause-functions). # Inspecting a Function run Source: https://www.inngest.com/docs/platform/monitor/inspecting-function-runs You identified a failed Function run and want to identify the root cause? Or simply want to dig into a run's timings? The Function run details will provide all the information to understand how this run ran and the tools to reproduce it locally. ## Accessing Function runs Functions runs across all application of the currently [selected environment](/docs/platform/environments) are accessible via the "Runs" page in the left side navigation. ![The "Handle failed payments" Function runs list features a run in a failing state.](/assets/docs/platform/monitor/inspecting-function-runs/function-runs.png) _Runs can be filtered using a Status, Queued or Started at and Application filters._ Accessing the runs of a specific Function is achieved via the "Functions" menu, as described in [the function run details section](/docs/platform/monitor/inspecting-function-runs#the-function-run-details). ## Searching Function runs Advanced filters are available using a [CEL expression](/docs/guides/writing-expressions). The search feature is available by clicking on the "Show search" button next to the other run filters. ![The runs list features an advance search feature that filters results using a CEL query.](/assets/docs/platform/monitor/inspecting-function-runs/function-runs-search.png) ### Searchable properties Only basic search operators and `event` and `output` variables are available for now: |Field name|Type|Operators| |-----------|-----------|-----------| event.id| `string`| `==`, `!=` event.name| `string`| `==`, `!=` event.ts| `int64`| `==`, `!=`, `>`, `>=`, `<`, `<=` event.v| `string`| `==`, `!=` event.data| `map[string]any`| `==`, `!=`, `>`, `>=`, `<`, `<=` output| `any`| `==`, `!=`, `>`, `>=`, `<`, `<=` A few examples of valid search queries are `event.data.hello == "world"` and `output.success != true`. [Learn more about how expressions are used in Inngest.](/docs/guides/writing-expressions) You can combine multiple search queries using the `&&` operator or `||` operator. Adding a new line is the equivalent of using the `&&` operator. ### Searching for errors Errors are serialized as JSON on the `output` object. When supported by the language SDK, errors are deserialized into a structured object. Here is an example of a error in TypeScript: ```typescript {{ title: "Example TypeScript code" }} throw new NonRetriableError("Failed to import data"); ``` ```json {{ title: "Example TypeScript error" }} { "name": "NonRetriableError", "message": "Failed to import data", "stack": "NonRetriableError: Failed to import data\n at V1InngestExecution.userFnToRun (/opt/render/project/src/build/inngest/ai.js:143:15) ..." } ``` This error can be searched using the following CEL expression: ``` output.name == "NonRetriableError" && output.message == "Failed to import data" ``` Using custom error types in TypeScript can make it easier to search by the type of error:
Example TypeScript code ```typescript class UserNotFoundError extends NonRetriableError { constructor(message: string) { super(message); this.name = "UserNotFoundError"; } } inngest.createFunction( { id: "my-fn" }, { event: "user" }, async ({ step, event }) => { await step.run("get-user", async () => { await getUser(event.data.userId); if (!user) { throw new UserNotFoundError(`User not found (${event.data.userId})`); } // ... }); } ); ``` ``` {{ title: "Example search query" }} event.data.userId == "12345" && output.name == "UserNotFoundError" ```
## The Function run details A _Handle failed payments_ function failed after retrying 5 times: ![The "Handle failed payments" Function runs list features a run in a failing state.](/assets/docs/platform/monitor/inspecting-function-runs/handle-failed-payments-function-run-failed.png) Clicking on the failed Function Runs expands the run detail view: ![The Function run details view displays the event payload on the left, some technical attributes (function version, timings) on the right and a timeline of steps on the bottom left.](/assets/docs/platform/monitor/inspecting-function-runs/function-run-logs.png) The Function run details panel is divided in 3 parts: - On the top right: the **Trigger details** helpful when exchanging with Support - On the right: the **Event payload** that triggered the Function run - On the bottom right: the **Run details** with its timeline, a clear with of the Function's steps execution The Function run details informs us that our Function run failed because of an `Error: Failed to downgrade user` error. This is a first clue, let's have a closer look at the Timeline to identify the root cause:

We can now spot that the `downgrade-account-billing-plan` failed. Let's expand this step to look at the retries and errors. ![The Timelime of steps features two steps: a first one to fetch the subscription from Stripe and second one to update it. The second is marked as failed.](/assets/docs/platform/monitor/inspecting-function-runs/function-run-logs-timeline-1.png) ![Expanding the second step lists all the attempted retries along with their respective error.](/assets/docs/platform/monitor/inspecting-function-runs/function-run-logs-timeline-2.png)

Expanding a step provides the same level of details (the error message and timings) along with retries information. It seems that our `downgrade-account-billing-plan` step raised the same error during the following 5 retries, we might have to perform a fix in the database. 💡 **Tips**
Clicking on the
icon next to "Run details" open it in a new tab with a full-page layout.
![Clicking on the icon next to "Run details" open it in a new tab with a full-page layout](/assets/docs/platform/monitor/inspecting-function-runs/function-runs-details-open-new-tab.png) It is useful for Function having a lot of steps or retries!
## Performing actions from the Function run details The Function run details provides two main actions: replay the Function Run or sending the trigger event to your local Inngest Dev Server. Sending the trigger Event to your local Inngest Dev Server provides a quick way to reproduce issues that are not linked to external factors (ex: new function version recently deployed, data issues). After looking at the Function run details, the failure is judged temporary or fixed by a recent deployment, you can replay the Function run by using the "Rerun" button at the top right of the screen. ![The rerun button is accessible in the header of the "run details" section of the Function run detail](/assets/docs/platform/monitor/inspecting-function-runs/function-runs-details-open-new-tab.png) # Observability & Metrics Source: https://www.inngest.com/docs/platform/monitor/observability-metrics With hundreds to thousands of events going through your Inngest Apps, triggering multiple Function runs, getting a clear view of what is happening at any time is crucial. The Inngest Platform provides observability features for both Events and Function runs, coupled with Event logs and [a detailed Function Run details to inspect arguments and steps timings](/docs/platform/monitor/inspecting-function-runs). ## Function runs observability The Functions list page provides the first round of essential information in one place with: - **Triggers**: Events or Cron schedule - **Failure rate**: enabling you to quickly identify a surge of errors - **Volume**: helping in identifying possible drops in processing ![The Functions list page lists all available Functions with essential information such as associated Events, Failure rate and Volume.](/assets/docs/platform/monitor/observability-metrics/functions-list.png) ## Function metrics Navigating to a Function displays the Function metrics page, composed of 7 charts: ![Clicking on a Function leads us to the Function view, composed of 7 charts.](/assets/docs/platform/monitor/observability-metrics/function-view.png) All the above charts can be filtered based on a time range (ex: last 24 hours), a specific Function or [App](/docs/platform/manage/apps). Let's go over each chart in detail: ### Function Status ![The Function Status chart is a pie chart where each part represents a function status (failed, succeed or cancelled).](/assets/docs/platform/monitor/observability-metrics/function-metrics-runs.png)
The Function Status chart provides a snapshot of the number of Function runs grouped by status. **How to use this chart?** This chart is the quickest way to identify an unwanted rate of failures at a given moment.
### Failed Functions The _Failed Functions_ chart displays the top 6 failing functions with the frequency of failures. **How to use this chart?** You can leverage this chart to identify a possible elevated rate of failures and quickly access the Function runs details from the "View all" button. ![](/assets/docs/platform/monitor/observability-metrics/failed-functions-chart.png) ### Total runs throughput ![The Total runs throughput is a line chart featuring the total number of Function runs per application.](/assets/docs/platform/monitor/observability-metrics/function-metrics-throughput.png) The _Total runs throughput_ is a line chart featuring the **rate of Function runs started per app**. This shows the performance of the system of how fast new runs are created and are being handled. **How to use this chart?** Flow control might intentionally limit throughput, this chart is a great way to visualize it. ### Total steps throughput ![The Total steps throughput is a line chart featuring the total number of Function steps running at a given time, per application.](/assets/docs/platform/monitor/observability-metrics/total-steps-throughput.png) The _Total steps throughput_ chart represents **the rate of which steps are executed, grouped by the selected Apps**. **How to use these charts?** The _Total steps throughput_ chart is helpful to assess the configuration of your Inngest Functions. For example, a low _Total steps throughput_ might be linked to a high number of concurrent steps combined with a restrictive concurrency configuration. {/* ### SDK request throughput The _SDK request throughput_ chart indicates the throughput at which SDK requests (or steps) are queued and executed, **across all selected Apps**. **How to use this chart?** This chart is a useful tool to evaluate in the requests sent by the Inngest Platform matches the number of steps created by Functions runs. The _SDK request throughput_ chart is also useful to evaluate the number of requests sent to your application over time. ![The SDK request throughput is a line chart featuring three series: queued, started and ended Function runs.](/assets/docs/platform/monitor/observability-metrics/function-metrics-sdk-request-throughput.png) */} ### Backlog ![The_Backlog highlights the number of Function runs waiting to processed at a given time bucket.](/assets/docs/platform/monitor/observability-metrics/backlog.png) The _Backlog_ highlights the number of **Function runs waiting to processed at a given time bucket, grouped by the selected Apps**. **How to use this chart?** This chart is useful to assess the _Account Concurrency_ capacity of your account and to identify potential spikes of activity. {/* ### Account concurrency The _Account concurrency_ displays the **[concurrency](/docs/guides/concurrency) usage in time, at the account level**. The red line illustrates the maximum concurrency capacity of the account. **How to use this chart?** This chart is useful to quickly identify is some increase in _Backlog_ or drop in _Total run throughput_ is linked to your account's [concurrency capacity](/docs/guides/concurrency). Each account gets a maximum concurrency capacity, computed by adding the total number of running steps across all applications. ![The SDK request throughput is a line chart featuring three series: queued, started and ended Function runs.](/assets/docs/platform/monitor/observability-metrics/account-concurrency.png) */} ## Events observability Events volume and which functions they trigger can become hard to visualize. Thankfully, the Events page gives you a quick overview of the volume of Events being sent to your Inngest account: ![The Events page lists the available Event type. Each list item features the event name along with its associated Functions and a events volume indicator.](/assets/docs/platform/monitor/observability-metrics/events-list.png) Get more detailed metrics for a dedicated event by navigating to it from the list: ## Events metrics and logs The Event page helps quickly visualize the throughput (the rate of event over time) and functions associated with this event. The event occurrences feature a “Source” column, which is helpful when an event is triggered from multiple Apps (ex, using different languages): ![Clicking on an Events leads us to the Event page that displays, at the top, a chart of events occurrences over the last 24 hours and at the list of associated events.](/assets/docs/platform/monitor/observability-metrics/event-view.png) Clicking on a specific event will redirect you to its Logs. The Event Logs view provides the most precise information, with the linked Function run and raw event data. Such information, combined with the ability to forward the event to your Local Dev Server instance, makes debugging events much quicker: ![Clicking on an event of the below list open the Event Logs view, providing much detailed information such as the Event Payload and triggered Functions.](/assets/docs/platform/monitor/observability-metrics/event-logs-view.png) # Prometheus metrics export integration Source: https://www.inngest.com/docs/platform/monitor/prometheus-metrics-export-integration Inngest supports exporting [Prometheus](https://prometheus.io/) metrics via scrape endpoints. This enables you to monitor your Inngest functions from your existing Prometheus or Prometheus-compatible monitoring tools like [Grafana](https://prometheus.io/docs/visualization/grafana/), [New Relic](https://docs.newrelic.com/docs/infrastructure/prometheus-integrations/get-started/send-prometheus-metric-data-new-relic/), or similar. ## Setup To get started, navigate to the Prometheus integration page in the Inngest dashboard's "Integrations" section. Select the Inngest environment you want to export metrics from. The scrape config will be automatically generated including your Inngest API key required to authenticate with the scrape endpoint. You can use this configuration in your Prometheus instance or similar tool that supports scraping from a URL. ![Prometheus integration page](/assets/docs/platform/monitor/prometheus-exports/prometheus-integration-page.png) ## Metrics The following metrics are exported: | Metric | Type | Tags | |---|---|---| | `inngest_function_run_scheduled_total`| counter | `fn`, `date` | | `inngest_function_run_started_total`| counter | `fn`, `date` | | `inngest_function_run_ended_total`| counter | `fn`, `date`, `status` | | `inngest_sdk_req_scheduled_total`| counter | `fn`, `date` | | `inngest_sdk_req_started_total`| counter | `fn`, `date` | | `inngest_sdk_req_ended_total`| counter | `fn`, `date`, `status` | | `inngest_step_output_bytes_total`| counter | `fn`, `date` | | `inngest_steps_scheduled`| gauge | `fn` | | `inngest_steps_running`| gauge | `fn` |
Example output: ```yaml # HELP inngest_function_run_ended_total The total number of function runs ended # TYPE inngest_function_run_ended_total counter inngest_function_run_ended_total{date="2025-02-12",fn="my-app-my-function",status="Completed"} 480 inngest_function_run_ended_total{date="2025-02-12",fn="my-app-my-function",status="Failed"} 20 # HELP inngest_function_run_scheduled_total The total number of function runs scheduled # TYPE inngest_function_run_scheduled_total counter inngest_function_run_scheduled_total{date="2025-02-12",fn="my-app-my-function"} 500 # HELP inngest_function_run_started_total The total number of function runs started # TYPE inngest_function_run_started_total counter inngest_function_run_started_total{date="2025-02-12",fn="my-app-my-function"} 500 # HELP inngest_sdk_req_ended_total The total number of SDK invocation/step execution ended # TYPE inngest_sdk_req_ended_total counter inngest_sdk_req_ended_total{date="2025-02-12",fn="my-app-my-function",status="errored"} 17 inngest_sdk_req_ended_total{date="2025-02-12",fn="my-app-my-function",status="failed"} 15 inngest_sdk_req_ended_total{date="2025-02-12",fn="my-app-my-function",status="success"} 740 # HELP inngest_sdk_req_scheduled_total The total number of SDK invocation/step execution scheduled # TYPE inngest_sdk_req_scheduled_total counter inngest_sdk_req_scheduled_total{date="2025-02-12",fn="my-app-my-function"} 772 # HELP inngest_sdk_req_started_total The total number of SDK invocation/step execution started # TYPE inngest_sdk_req_started_total counter inngest_sdk_req_started_total{date="2025-02-12",fn="my-app-my-function"} 772 # HELP inngest_step_output_bytes_total The total number of bytes used by step outputs # TYPE inngest_step_output_bytes_total counter inngest_step_output_bytes_total{date="2025-02-12",fn="my-app-my-function"} 2804 # HELP inngest_steps_running The number of steps currently running # TYPE inngest_steps_running gauge inngest_steps_running{fn="my-app-my-function"} 7 # HELP inngest_steps_scheduled The number of steps scheduled # TYPE inngest_steps_scheduled gauge inngest_steps_scheduled{fn="my-app-my-function"} 30 ```
## Limits The Prometheus integration is available to all paid plans and is subject to the following limits. | Plan | Granularity | Delay | |---|---|---| | Basic | 15 minutes | 15 minutes | | Pro | 5 minutes | 5 minutes | | Enterprise | 1 minute | Immediate | All plans are subject to a rate limit of 30 requests per minute. # Function Replay Source: https://www.inngest.com/docs/platform/replay Functions will fail. It's unavoidable. When they do, you need to recover from the failure quickly. When a large number of functions fail, you can easily _replay_ them in bulk from the Inngest dashboard. ![Relay graphic](/assets/docs/platform/replay/featured-image-v2.png) The recovery flow in other systems may require dead-letter queues or some other form of manual intervention. With Replay, you can replay functions in bulk from the Inngest dashboard: 1. You detect an issue with your functions (e.g. a failure due to a bug or external system) 2. You fix the issue and push to production 3. You use Replay to replay the functions from the time range when the issues occurred Let's learn how you can use Replay to recover from function failures: ## How to create a new Replay To replay a function, click the replay button which is present on both the function runs page and the function replay page. This will open a modal where you can select the runs you want to replay. ![Replay button in function runs page](/assets/docs/platform/replay/replay-function-runs.png) Each replay requires a name, a time range and status(es) to filter the runs to be replayed. We recommend using a name that describes the incident that you're resolving so your team can understand this later, or maybe just mention the bug tracker issue: e.g. "Bug fix from PR #958", "API-395: Networking blip." ![Replay modal form](/assets/docs/platform/replay/replay-function-modal-empty.png) Here's an example of a Replay that fixed a bug triggered by daylight savings time between the given timestamp. For this issue, we only want to target the "Failed" function runs statuses. You can select multiple run statuses in case your function might have had a bug that failed silently, so you want to replay anything previously marked as "Succeeded" as well. ![Replay modal form filled](/assets/docs/platform/replay/replay-function-modal-filled.png) Once you have selected the runs you want to replay, click the replay button to start the replay. You will be redirected to the replay page where you can see the progress of the replay. ![List of all Replays](/assets/docs/platform/replay/replay-function-replays.png) The replay will spread out the runs over time as to not overwhelm your application with requests. Depending on the number of runs to be replayed, this could take seconds or minutes to complete. When all the runs have been replayed, the replay will be marked as "Completed." # Signing keys Source: https://www.inngest.com/docs/platform/signing-keys Description: Learn about how signing keys are used to secure communication between Inngest and your servers, how to rotate them, and how to set them in your SDK.' # Signing keys Inngest uses signing keys to secure communication between Inngest and your servers. The signing key is a _secret_ pre-shared key unique to each [environment](/docs/platform/environments). The signing key adds the following security features: * **Serve endpoint authentication** - All requests sent to your server are signed with the signing key, ensuring that they originate from Inngest. Inngest SDKs reject all requests that are not authenticated with the signing key. * **API authentication** - Requests sent to the Inngest API are signed with the signing key, ensuring that they originate from your server. The Inngest API rejects all requests that are not authenticated with the signing key. For example, when syncing functions with Inngest, the SDK sends function configuration to Inngest's API with the signing key as a bearer token. * **Replay attack prevention** - Requests are signed with a timestamp embedded, and old requests are rejected, even if the requests are authenticated correctly. 🔐 **Signing keys are secrets and you should take precautions to ensure that they are kept secure**. Avoid storing them in source control. You can find your signing key within each environment's [Signing Key tab](https://app.inngest.com/env/production/manage/signing-key). # You can set the signing key in your SDK by setting the `INNGEST_SIGNING_KEY` environment variable. Alternatively, you can pass the signing key as an argument when creating the SDK client, but we recommend never hardcoding the signing key in your code. Additionally, you'll set the `INNGEST_SIGNING_KEY_FALLBACK` environment variable to ensure zero downtime when rotating your signing key. Read more about that [below](#rotation). ## Local development Signing keys should be omitted when using the Inngest [Dev Server](/docs/local-development). To simplify local development and testing, the Dev Server doesn't require a signing key. Each language SDK attempts to detect if your application is running in production or development mode. If you're running in development mode, the SDK will automatically disable signature verification. To force development mode, set `INNGEST_DEV=1` in your environment. This is useful when running in an automated testing environment. ## Rotation Signing keys can be rotated to mitigate the risk of a compromised key. We recommend rotating your signing keys periodically or whenever you believe they may have been exposed. Inngest supports zero downtime signing key rotation if your SDK version meets the minimum version: | Language | Minimum Version | | ----------- | --------------- | | Go | `0.7.2` | | Python | `0.3.9` | | TypeScript | `3.18.0` | You can still rotate your signing key if you use an older SDK version, but you will experience downtime. To begin the rotation process, navigate to the [Signing Key tab](https://app.inngest.com/env/production/manage/signing-key) in the Inngest dashboard. Click the "Create new signing key" button and then follow the instructions in the **Rotate key** section. 🤔 **Why do I need a "fallback" signing key?** As requests are signed with the current signing key, your code must have both the current and the new signing key available to verify requests during the rotation. To ensure there is zero downtime, the SDKs will retry authentication failures with the fallback key. ### Vercel integration To rotate signing keys for Vercel projects, you must manually update the `INNGEST_SIGNING_KEY` environment variable in your Vercel project. During initial setup, the Vercel integration automatically sets this key, but the integration **_will not_** automatically rotate the key for you. You must follow the manual process as guided within the Inngest dashboard. ## Signing keys and branch environments All [branch environments](/docs/platform/environments#configuring-branch-environments) within your account share a signing key. This enables you to set a single environment variable for preview environments in platforms like [Vercel](/docs/deploy/vercel) or [Netlify](/docs/deploy/netlify) and each platform can dynamically specify the correct branch environment through secondary environment variables. ## Further reading * [TypeScript SDK serve reference](/docs/reference/serve#signingKey) * [Python SDK client reference](/docs/reference/python/client/overview#signing_key) # Consuming webhook events Source: https://www.inngest.com/docs/platform/webhooks import { RiTerminalLine, RiGithubFill } from "@remixicon/react"; At its core, Inngest is centered around functions that are triggered by events. Webhooks are one of the most ubiquitous sources for events for developers. Inngest was designed to support webhook events. This guide will show you how to use Inngest to consume your webhook events and trigger functions.
When talking about webhooks, it's useful to define some terminology: - **Producer** - The service sending the webhook events as HTTP post requests. - **Consumer** - The URL endpoint which receives the HTTP post requests. Inngest enables you to create any number of unique URLs which act as webhook consumers. You can create a webhook for each third party service that you use (e.g. Stripe, Github, Clerk) along with custom rules on how to handle that webhook. ## Creating a webhook First, you'll need to head to the **Manage** tab in the Inngest dashboard and then click **Webhooks**. From there, click **Create Webhook**. Now you'll have a uniquely generated URL that you can provide to any Producer service to start sending events. These URLs are configured in different ways for different producer services. For example, with Stripe, you need to enter "developer mode" and configure your webhook URLs. Give your webhook a name and save it. Next we'll explore how to turn the request payload into Inngest events. ![Inngest dashboard showing a newly created webhook](/assets/docs/platform/dashboard-webhook-screenshot.png) ## Defining a transform function Most webhooks send event data as JSON within the POST request body. These raw events must be transformed slightly to be compatible with [the Inngest event payload format](/docs/reference/events/send#inngest-send-event-payload-event-payload-promise). Mainly, we must have `name` and `data` set in the Inngest event. Fortunately, Inngest includes **_transform_** functions for every webhook. You can define a short JavaScript function used to transform the shape of the payload. This transform runs on Inngest's servers so there is no added load or cost to your infra. Here is [an example of a raw webhook payload from Clerk](https://clerk.com/docs/integrations/webhooks/overview#payload-structure) on the left and our transformed event: ```json {{ title: "Example Clerk webhook payload"}} { "type": "user.created", "object": "event", "data": { "created_at": 1654012591514, "external_id": "567772", "first_name": "Example", "id": "user_29w83sxmDNGwOuEthce5gg56FcC", "last_name": "Example", "last_sign_in_at": 1654012591514, "object": "user", "primary_email_address_id": "idn_29w83yL7CwVlJXylYLxcslromF1", // ... simplified for example }, } ``` ```json {{ title: "Example Inngest event format"}} { "name": "clerk/user.created", "data": { "created_at": 1654012591514, "external_id": "567772", "first_name": "Example", "id": "user_29w83sxmDNGwOuEthce5gg56FcC", "last_name": "Example", "last_sign_in_at": 1654012591514, "object": "user", "primary_email_address_id": "idn_29w83yL7CwVlJXylYLxcslromF1", // ... simplified for example } } ``` Transforms are defined as simple JavaScript functions that accept three arguments and expect the Inngest event payload object in the returned value. The arguments are: The raw JSON payload from the POST request body A map of HTTP headers sent along with the request as key-value pairs. Header names are case-insensitive and are canonicalized by making the first character and any characters following a hyphen uppercase and the rest lowercase. For more details, [check out](https://pkg.go.dev/net/http#CanonicalHeaderKey) the underlying implementation reference. A map of parsed query string parameters sent to the webhook URL. Values are all arrays to support multiple params for a single key. Here's a simple transform function for the Clerk example shown above: ```ts function transform(evt, headers = {}, queryParams = {}) { return { name: `clerk/${evt.type}`, data: evt.data, // You can optionally set ts using data from the raw json payload // to explicitly set the timestamp of the incoming event. // If ts is not set, it will be automatically set to the time the request is received. } } ``` 👉 We also recommend prefixing each event name with the name of the producer service, e.g. `clerk/user.created`, `stripe/charge.failed`. ### Example transforms 💡 Header names are case-insensitive and are canonicalized by making the first character and any characters following a hyphen uppercase and the rest lowercase. Remember to check your transforms for header usage and make sure to use the correct case.
**Github** - Using headers Github uses a `X-Github-Event` header to specify the event type: ```js function transform(evt, headers = {}, queryParams = {}) { headers["X-Github-Event"]; return { // Use the event as the data without modification data: evt, // Add an event name, prefixed with "github." based off of the X-Github-Event data name: "github." + name.trim().replace("Event", "").toLowerCase(), }; } ```
**Stripe** - Using an `id` for deduplication Stripe sends an `id` with every event to deduplicate events. We can use this as the `id` for the Inngest event for the same reason: ```js function transform(evt, headers = {}, queryParams = {}) { return { id: evt.id, name: `stripe/${evt.type}`, data: evt, }; } ```
**Linear** - Creating useful event names ```js function transform(evt, headers = {}, queryParams = {}) { return { // type (e.g. Issue) + action (e.g. create) name: `linear/${evt.type.toLowerCase()}.${evt.action}`, data: evt, }; } ```
**Intercom** - Setting the `ts` field ```js function transform(evt, headers = {}, queryParams = {}) { return { name: `intercom/${evt.topic}`, // the top level obj only contains webhook data, so we omit that data: evt.data, ts: evt.created_at * 1000, }; }; ```
**Resend** ```js function transform(evt, headers = {}, queryParams = {}) { return { name: `resend/${evt.type}`, data: evt.data, }; }; ```
### Testing transforms The Inngest dashboard includes a tool to quickly test your transform function. You can paste the incoming payload from the webhook producer in the "Incoming Event JSON" editor and immediately preview what the transformed event will look like. ![Inngest dashboard transform testing](/assets/docs/platform/dashboard-webhook-transform-testing.png) Some webhook producers do not provide example payloads in their documentation. If that's the case, you can use a tool that we built, [TypedWebhooks.tools](https://typedwebhook.tools/?ref=) to test webhooks and browse payloads. ## Advanced configuration Additionally, you can configure allow/deny lists for event names and IP addresses. This can be useful if you want a bit more control over what events are ingested. ## Managing webhooks via REST API Webhooks can be created, updated and deleted all via the Inngest REST API. This is very useful if you want to manage all transforms within your codebase and sync them to the Inngest platform. Check out the documentation below to learn more: } href={'https://api-docs.inngest.com/docs/inngest-api/b539bae406d1f-get-all-webhook-endpoints-in-given-environment'}> Read the documentation about managing Webhooks via the Inngest REST API } href={'https://github.com/inngest/webhook-transform-sync'}> View an end-to-end example of how to test and sync Webhooks in your codebase. ## Local development To test your webhook locally, you can forward events to the [Dev Server](/docs/local-development) from the Inngest dashboard using the "Send to Dev Server" button. This button is found anywhere that an event payload is visible on the Inngest dashboard. This will send a copy of the event to your local machine where you can test your functions. [IMAGE] ## Writing functions Now that you have events flowing into Inngest, you can write functions that that handle the events that you care about. You can also explore the list of events that have been received at any time by heading to the _Events_ tab of the Inngest dashboard. ```ts {{ title: "Example: Send a welcome email when the clerk/user.created event is received"}} inngest.createFunction( { name: "Send welcome email", id: "send-welcome-email" }, { event: "clerk/user.created" }, async ({ event, step }) => { event.data.email_addresses[0].email_address; await step.run('send-email', async () => { return await resend.emails.send({ to: emailAddress, from: "noreply@inngest.com", subject: "Welcome to Inngest!", react: WelcomeEmail(), }) }); } ) ``` 💡 **Tip**: To test functions locally, copy an event from a webhook from the Inngest dashboard and use it with the Inngest dev server's `Send test` button. ## Verifying request signatures Many webhook producers sign their requests with a secret key to ensure that the request is coming from them. This establishes trust with the webhook producer and ensures that the event data has not been tampered with. To verify a webhook signature, you'll need to return the signature and raw request body string in your transform. For example, the following transform function could be used for Stripe webhooks: ```ts function transform(evt, headers, queryParams, raw) { return { name: `stripe/${evt.type}`, data: { raw, sig: headers["Stripe-Signature"], } }; }; ``` Then you can use that data to verify the signature in your Inngest functions: ```ts inngest.createFunction( { id: "stripe/charge.updated" }, { event: "stripe/charge.updated" }, async ({ attempt, event, step }) => { if (!verifySig(event.data.raw, event.data.sig, stripeSecret)) { throw new NonRetriableError("failed signature verification"); } // Now it's safe to use the event data. JSON.parse(event.data.raw); } ); ``` ## Branch environments All branch environments share the same webhooks. They are centrally-managed in a [single page](https://app.inngest.com/env/branch/manage/webhooks). Additionally, the target branch environment must be specified using either an `x-inngest-env` query param or header. For example, the following command will send an webhook to the `branch-1` branch environment: ```sh curl 'https://inn.gs/e/REDACTED?x-inngest-env=branch-1' -d '{"msg": "hi"}' ``` If the branch environment is not specified with the header or query param, the webhook will be sent to [this page](https://app.inngest.com/env/branch/events) and will not trigger any functions. Events will also go here if the branch environment does not exist. The value for `x-inngest-env` is the name of the branch environment, not the ID in the URL. # Create the Inngest Client Source: https://www.inngest.com/docs/reference/client/create The `Inngest` client object is used to configure your application, enabling you to create functions and send events. ```ts {{ title: "v3" }} new Inngest({ id: "my-application", }); ``` ```ts {{ title: "v2" }} new Inngest({ name: "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 wrappng it in an anonymous function. 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 the following interfaces (`.info()`, `.warn()`, `.error()`, `.debug()`). Defaults to using `console` if not provided. Overwrites [`INNGEST_LOG_LEVEL`](/docs/sdk/environment-variables#inngest-log-level) if set. See [logging guide](/docs/guides/logging) for more details. A stack of [middleware](/docs/reference/middleware/overview) to add to the client. Event payload types. See [Defining Event Payload Types](#defining-event-payload-types). 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. ## Defining Event Payload Types You can leverage TypeScript or Zod to define your event payload types. When you pass types to the Inngest client, events are fully typed when using them with `inngest.send()` and `inngest.createFunction()`. This can more easily alert you to issues with your code during compile time. Click the toggles on the top left of the code block to see the different methods available! ```ts {{ title: "Zod" }} // Define each event individually z.object({ name: z.literal("shop/product.purchased"), data: z.object({ productId: z.string() }), }); // You can use satisfies to provide some autocomplete when writing the event z.object({ name: z.literal("shop/product.viewed"), data: z.object({ productId: z.string() }), }) satisfies LiteralZodEventSchema; // Or all together in a single object { "app/account.created": { data: z.object({ userId: z.string(), }), }, "app/subscription.started": { data: z.object({ userId: z.string(), planId: z.string(), }), }, }; inngest = new Inngest({ schemas: new EventSchemas() .fromZod([productPurchasedEvent, productViewedEvent]) .fromZod(eventsMap), }); ``` ```ts {{ title: "Union" }} type AppAccountCreated = { name: "app/account.created"; data: { userId: string; }; }; type AppSubscriptionStarted = { name: "app/subscription.started"; data: { userId: string; planId: string; }; }; type Events = AppAccountCreated | AppSubscriptionStarted; inngest = new Inngest({ schemas: new EventSchemas().fromUnion(), }); ``` ```ts {{ title: "Record"}} type Events = { "app/account.created": { data: { userId: string; }; }; "app/subscription.started": { data: { userId: string; planId: string; }; }; }; inngest = new Inngest({ schemas: new EventSchemas().fromRecord(), }); ``` ```ts {{ title: "Stacking" }} type Events = { "app/user.created": { data: { id: string }; }; }; { "app/account.created": { data: z.object({ userId: z.string(), }), }, }; type AppPostCreated = { name: "app/post.created"; data: { id: string }; }; type AppPostDeleted = { name: "app/post.deleted"; data: { id: string }; }; inngest = new Inngest({ schemas: new EventSchemas() .fromRecord() .fromUnion() .fromZod(zodEventSchemas), }); ``` ### Reusing event types You can use the `GetEvents<>` generic to access the final event types from an Inngest client. It's recommended to use this instead of directly reusing your event types, as Inngest will add extra properties and internal events such as `ts` and `inngest/function.failed`. ```ts inngest = new Inngest({ schemas: new EventSchemas().fromRecord<{ "app/user.created": { data: { userId: string } }; }>(), }); type Events = GetEvents; type AppUserCreated = Events["app/user.created"]; ``` For more information on this and other TypeScript helpers, see [TypeScript - Helpers](/docs/typescript#helpers). ## 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 neither is set, the SDK will attempt to infer which mode it should be in based on environment variables such as `NODE_ENV`. Most of the time, this inference is all you need and explicitly setting a mode isn't required. ## 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 {{ title: "v3" }} inngest = new Inngest({ id: "my-app" }); ``` ```ts {{ title: "v2" }} inngest = new Inngest({ name: "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 {{ title: "v3" }} // inngest/client.ts inngest = new Inngest({ id: "my-app", middleware: [nodeMiddleware], }); // inngest/edgeClient.ts inngest = new Inngest({ id: "my-app-edge", }); ``` ```ts {{ title: "v2" }} // inngest/client.ts inngest = new Inngest({ name: "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. # Send events Source: https://www.inngest.com/docs/reference/events/send description = `Send one or more events to Inngest via inngest.send() in the TypeScript SDK.` 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 of the context of a function, use [`step.sendEvent()`](/docs/reference/functions/step-send-event). --- ## `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/docs/inngest-api/pswkqb7u3obet-get-an-event). ```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 # Create Function Source: https://www.inngest.com/docs/reference/functions/create Define your functions using the `createFunction` method on the [Inngest client](/docs/reference/client/create). ```ts export default inngest.createFunction( { id: "import-product-images" }, { event: "shop/product.imported" }, async ({ event, step, runId }) => { // Your function code } ); ``` --- ## `inngest.createFunction(configuration, trigger, 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. Limit the number of concurrently running functions ([reference](/docs/functions/concurrency)) The maximum number of concurrently running steps. 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 generate. 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 runs allowed to start in the given window in a single burst. This defaults to 1, which ensures that requests are smoothed amongst the given `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/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/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/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'` 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/functions/handling-failures)) Define events that can be used to cancel a running or sleeping function ([reference](/docs/reference/typescript/functions/cancel-on)) The event name which will be used to cancel 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`. {/* TODO - Document fns arg */} ### Trigger One of the following function triggers is **Required**. You can also 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 name of the event that will trigger this event to run 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/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/functions/step-run) - Run synchronous or asynchronous code as a retriable step in your function - [`step.sleep()`](/docs/reference/functions/step-sleep) - Sleep for a given amount of time - [`step.sleepUntil()`](/docs/reference/functions/step-sleep-until) - Sleep until a given time - [`step.invoke()`](/docs/reference/functions/step-invoke) - Invoke another Inngest function as a step, receiving the result of the invoked function - [`step.waitForEvent()`](/docs/reference/functions/step-wait-for-event) - Pause a function's execution until another event is received - [`step.sendEvent()`](/docs/reference/functions/step-send-event) - Send event(s) reliability 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/functions/debounce 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", }, }, { 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). # Handling Failures Source: https://www.inngest.com/docs/reference/functions/handling-failures Define any failure handlers for your function with the [`onFailure`](/docs/reference/functions/create#configuration) option. This function will be automatically called when your function fails after it's maximum number of retries. Alternatively, you can use the [`"inngest/function.failed"`](#the-inngest-function-failed-event) system event to handle failures across all functions. ```ts export default inngest.createFunction( { id: "import-product-images", 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 }, }, { event: "shop/product.imported" }, 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/functions/retries) 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"`](#the-inngest-function-failed-event) 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/functions/create#handler) when creating a function, but also received 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/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/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", // 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; }, }, { event: "shop/product_sync.requested" }, 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.datadoghq.com/api/latest/events/) 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" }, { 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.} - ${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, }, }); // Flush the Sentry queue to ensure the error is sent: return await Sentry.flush(); } ); ``` ### Additional examples # Rate limit function execution Source: https://www.inngest.com/docs/reference/functions/rate-limit 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", rateLimit: { key: "event.data.company_id", limit: 1, period: "4h", }, }, { event: "intercom/company.updated" }, 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", rateLimit: { key: "event.data.company_id", limit: 1, period: "4h", }, }, { event: "intercom/company.updated" }, 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", 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", }, }, { event: "service/check.failed" }, 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, ...`, }); }); } ); ``` # Function run priority Source: https://www.inngest.com/docs/reference/functions/run-priority 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", 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", }, }, { event: "ai/summary.requested" }, 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". # Invoke Source: https://www.inngest.com/docs/reference/functions/step-invoke description = `Calling Inngest functions directly from other functions with step.invoke()` 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" }, { 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" }, { 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 } ); ``` ## `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. {/* Purposefully not mentioning the default timeout of 1 year, as we expect to lower this very soon. */} The amount of time to wait for the invoked function to complete. The time to wait can be specified using a `number` of milliseconds, an `ms`-compatible time string like `"1 hour"`, `"30 mins"`, or `"2.5d"`, or a `Date` object. 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. Alternatively, if a function ID `string` is passed, the Inngest SDK will assert the ID is correct at runtime. See [Error handling](#error-handling) for more information about this point. 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. ## 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`). This may change on the future - [let us know](https://roadmap.inngest.com/roadmap?ref=docs) if you'd like to change this. ## 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`. ## 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/functions/step-run description = `Define steps to execute with step.run()` 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" }, { 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/functions/retries)). ```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", () => {/* ... */}), ]); ``` ## 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/functions/step-send-event description = `Send one or more events reliably from within your function with step.sendEvent().`; Use to send event(s) reliably within your function. Use this instead of [`inngest.send()`](/docs/reference/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 {{ title: "v3" }} export default inngest.createFunction( { id: "user-onboarding" }, { 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 } ); ``` ```ts {{ title: "v2" }} export default inngest.createFunction( { name: "User onboarding" }, { event: "app/user.signup" }, async ({ event, step }) => { // Do something await step.sendEvent({ 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/events/send). --- ## `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/events/send#inngest-send-event-payload-event-payload-promise) for the event payload format. ```ts {{ title: "v3" }} // 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" }, }, ]); ``` ```ts {{ title: "v2" }} // Send a single event await step.sendEvent({ name: "app/user.activated", data: { userId: "01H08SEAXBJFJNGTTZ5TAWB0BD" }, }); // Send an array of events await step.sendEvent([ { 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/docs/inngest-api/pswkqb7u3obet-get-an-event). ```ts await step.sendEvent([ { 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/functions/step-sleep-until description = `Sleep until a specific date time with step.sleepUntil()`; ## `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 {{ title: "v3" }} // 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); ``` ```ts {{ title: "v2" }} // Sleep until the new year await step.sleepUntil("2024-01-01"); // Sleep until September ends await step.sleepUntil("2023-09-30T11:59:59"); // Sleep until the end of the this week dayjs().endOf('week').toDate(); await step.sleepUntil(date) ``` `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/functions/step-sleep ## `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 {{ title: "v3" }} // Sleep for 30 minutes Temporal.Duration.from({ minutes: 5 }); 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); ``` ```ts {{ title: "v2" }} // Sleep for 30 minutes await step.sleep("30m"); await step.sleep("30 minutes"); await step.sleep(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/functions/step-wait-for-event description = `Wait for a particular event to be received before continuing with step.waitForEvent()`; ## `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. The amount of time to wait to receive the event. A time string compatible with the [ms](https://npm.im/ms) package, e.g. `"30m"`, `"3 hours"`, or `"2.5d"` 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 {{ title: "v3" }} // 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'", }); ``` ```ts {{ title: "v2" }} // Wait 7 days for an approval and match invoice IDs await step.waitForEvent("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("app/subscription.created", { timeout: "30d", if: "event.data.userId == async.data.userId && async.data.billing_plan == 'pro'", }); ``` `step.waitForEvent()` must be called using `await` or some other Promise handler to ensure your function sleeps correctly. # Go SDK migration guide: v0.7 to v0.8 Source: https://www.inngest.com/docs/reference/go/migrations/v0.7-to-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 import ( "context" "net/http" "github.com/inngest/inngestgo" ) func main() { client, err := inngestgo.NewClient(inngestgo.ClientOpts{AppID: "my-app"}) if err != nil { panic(err) } _, err = inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ID: "my-fn"}, inngestgo.EventTrigger("my-event", nil), func( ctx context.Context, input inngestgo.Input[inngestgo.GenericEvent[any, any]], ) (any, error) { return "Hello, world!", nil }, ) if err != nil { panic(err) } _ = http.ListenAndServe(":8080", client.Serve()) } ``` ## `Client` The `DefaultClient` was removed. You should now use the `NewClient` function to create a new client: ```go client, err := inngest.NewClient(inngest.ClientOpts{AppID: "my-app"}) ``` `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 _, err := inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ID: "my-fn"}, inngestgo.EventTrigger("my-event", nil), func( ctx context.Context, input inngestgo.Input[inngestgo.GenericEvent[any, any]], ) (any, error) { return "Hello, world!", nil }, ) ``` `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 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 type MyEventData struct { Message string `json:"message"` } _, err = inngestgo.CreateFunction( client, inngestgo.FunctionOpts{ID: "my-fn"}, inngestgo.EventTrigger("my-event", nil), func( ctx context.Context, input inngestgo.Input[MyEventData], ) (any, error) { fmt.Println(input.Event.Data.Message) return nil, nil }, ) ``` ## `GenericEvent` The `GenericEvent` type no longer accepts the event user type as a generic parameter. ```go type MyEventData struct { Message string `json:"message"` } type MyEvent = inngestgo.GenericEvent[MyEventData] ``` # Reference Source: https://www.inngest.com/docs/reference/index import { CommandLineIcon } from "@heroicons/react/24/outline"; hidePageSidebar = true; Learn about our SDKs: # Example middleware Source: https://www.inngest.com/docs/reference/middleware/examples The following examples show how you might use middleware in some real-world scenarios. - [Cloudflare Workers AI](#cloudflare-workers-ai) - [Common actions for every function](#common-actions-for-every-function) - [Logging](#logging) - [Prisma in function context](#prisma-in-function-context) - [Cloudflare Workers & Hono environment variables](/docs/examples/middleware/cloudflare-workers-environment-variables) --- ## Cloudflare Workers AI [Workers AI](https://developers.cloudflare.com/workers-ai/) allows you to run machine learning models, on the Cloudflare network, from your own code, triggered by Inngest. To use the `@cloudflare/ai` package, you need access to the `env` object passed to a Workers route handler. This argument is usually abstracted away by a serve handler, but middleware can access arguments passed to the request. Use this along with [mutating function input](/docs/reference/middleware/typescript#mutating-input) to set a new `ai` property that you can use within functions, like in the following example: ```ts interface Env { // If you set another name in wrangler.toml as the value for 'binding', // replace "AI" with the variable name you defined. AI: Ai; } cloudflareMiddleware = new InngestMiddleware({ name: "Inngest: Workers AI", init: () => { return { onFunctionRun: ({ reqArgs }) => { reqArgs as [Request, Env]; ctx.env.AI return { transformInput: () => { return { ctx: { ai } }; }, }; }, }; }, }); ``` ```ts export default inngest.createFunction( { id: "hello-world" }, { event: "demo/event.sent" }, async ({ ai }) => { // `ai` is typed and can be used directly or within a step await ai.run("@cf/meta/llama-2-7b-chat-int8", { prompt: "What is the origin of the phrase Hello, World", }); } ); ``` ## Common actions for every function You likely reuse the same steps across many functions - whether it be fetching user data or sending an email, your app is hopefully full of reusable blocks of code. We could add some middleware to pass these into any Inngest function, automatically wrapping them in `step.run()` and allowing the code inside our function to feel a little bit cleaner. ```ts /** * Pass to a client to provide a set of actions as steps to all functions, or to * a function to provide a set of actions as steps only to that function. */ new Inngest({ id: "my-app", middleware: [ createActionsMiddleware({ getUser(id: string) { return db.user.get(id); }, }), ], }); inngest.createFunction( { id: "user-data-dump" }, { event: "app/data.requested" }, async ({ event, action: { getUser } }) => { // The first parameter is the step's options or ID await getUser("get-user-details", event.data.userId); } ); ``` ```ts /** * Create a middleware that wraps a set of functions in step tooling, allowing * them to be invoked directly instead of using `step.run()`. * * This is useful for providing a set of common actions to a particular function * or to all functions created by a client. */ createActionsMiddleware = (rawActions: T) => { return new InngestMiddleware({ name: "Inngest: Actions", init: () => { return { onFunctionRun: () => { return { transformInput: ({ ctx: { step } }) => { Object.entries( rawActions ).reduce((acc, [key, value]) => { if (typeof value !== "function") { return acc; } ( idOrOptions: StepOptionsOrId, ...args: unknown[] ) => { return step.run(idOrOptions, () => value(...args)); }; return { ...acc, [key]: action, }; }, {} as FilterActions); return { ctx: { action }, }; }, }; }, }; }, }); }; type Actions = Record; /** * Filter out all keys from `T` where the associated value does not match type * `U`. */ type KeysNotOfType = { [P in keyof T]: T[P] extends U ? never : P; }[keyof T]; /** * Given a set of generic objects, extract any top-level functions and * appropriately shim their types. * * We use this type to allow users to spread a set of functions into the * middleware without having to worry about non-function properties. */ type FilterActions> = { [K in keyof Omit any>>]: ( idOrOptions: StepOptionsOrId, ...args: Parameters ) => Promise>>; }; ``` ## Logging The following shows you how you can create a logger middleware and customize it to your needs. It is based on the [built-in logger middleware](/docs/guides/logging) in the SDK, and hope it gives you an idea of what you can do if the built-in logger doesn't meet your needs. ```ts new InngestMiddleware({ name: "Inngest: Logger", init({ client }) { return { onFunctionRun(arg) { arg; { runID: ctx.runId, eventName: ctx.event.name, functionName: arg.fn.name, }; let providedLogger: Logger = client["logger"]; // create a child logger if the provided logger has child logger implementation try { if ("child" in providedLogger) { type ChildLoggerFn = ( metadata: Record ) => Logger; providedLogger = (providedLogger.child as ChildLoggerFn)(metadata) } } catch (err) { console.error('failed to create "childLogger" with error: ', err); // no-op } new ProxyLogger(providedLogger); return { transformInput() { return { ctx: { /** * The passed in logger from the user. * Defaults to a console logger if not provided. */ logger, }, }; }, beforeExecution() { logger.enable(); }, transformOutput({ result: { error } }) { if (error) { logger.error(error); } }, async beforeResponse() { await logger.flush(); }, }; }, }; }, }) ``` --- ## Prisma in function context The following is an example of adding a [Prisma](https://www.prisma.io/?ref=inngest) client to all Inngest functions, allowing them immediate access without needing to create the client themselves. While this example uses Prisma, it serves as a good example of using the [onFunctionRun -> input](/docs/reference/middleware/lifecycle#on-function-run-lifecycle) hook to mutate function input to perform crucial setup for your functions and keep them to just business logic. 💡 Types are inferred from middleware outputs, so your Inngest functions will see an appropriately-typed `prisma` property in their input. ```ts inngest.createFunction( { name: "Example" }, { event: "app/user.loggedin" }, async ({ prisma }) => { await prisma.auditTrail.create(/* ... */); } ); ``` ```ts new InngestMiddleware({ name: "Prisma Middleware", init() { new PrismaClient(); return { onFunctionRun(ctx) { return { transformInput(ctx) { return { // Anything passed via `ctx` will be merged with the function's arguments ctx: { prisma, }, }; }, }; }, }; }, }); ``` Check out [Common actions for every function](/docs/reference/middleware/examples#common-actions-for-every-function) to see how this technique can be used to create steps for all of your unique logic. ## Other examples } href={'/docs/examples/middleware/cloudflare-workers-environment-variables'}> Access environment variables within Inngest functions. # Middleware lifecycle Source: https://www.inngest.com/docs/reference/middleware/lifecycle ## Hook reference The `init()` function can return functions for two separate lifecycles to hook into. 💡 All lifecycle and hook functions can be synchronous or `async` functions - the SDK will always wait until a middleware's function has resolved before continuing to the next one. ### `onFunctionRun` lifecycle Triggered when a function is going to be executed. The input data for the function. Only `event` and `runId` are available at this point. An array of previously-completed step objects. The serialized data for this step if it was successful. The serialized error for this step if it failed. The function that is about to be executed. Arguments passed to the framework's request handler, which are used by the SDK's `serve` handler. Called once the input for the function has been set up. This is where you can modify the input before the function starts. Has the same input as the containing `onFunctionRun()` lifecycle function, but with a complete `ctx` object, including `step` tooling. An object that will be merged with the existing function input to create a new input. An array of modified step data to use in place of the current step data. Called before the function starts to memoize state (running over previously-seen code). Called after the function has finished memoizing state (running over previously-seen code). Called before any step or code executes. Called after any step or code has finished executing. Called after the function has finished executing and before the response is sent back to Inngest. This is where you can modify the output. An object containing the data to be sent back to Inngest in the `data` key, and an original `error` (if any) that threw. If this execution ran a step, will be a step that ran. An object containing a `data` key to overwrite the data that will be sent back to Inngest for this step or function. Called when execution is complete and a final response is returned (success or an error), which will end the run. This function is not guaranteed to be called on every execution. It may be called multiple times if there are many parallel executions or during retries. An object that contains either the successful `data` ending the run or the `error` that has been thrown. Both outputs have already been affected by `transformOutput`. Called after the output has been set and before the response has been sent back to Inngest. Use this to perform any final actions before the request closes. ```ts new InngestMiddleware({ name: "My Middleware", init({ client, fn }) { return { onFunctionRun({ ctx, fn, steps }) { return { transformInput({ ctx, fn, steps }) { // ... return { // All returns are optional ctx: { /* extend fn input */ }, steps: steps.map(({ data }) => { /* transform step data */ }) } }, beforeMemoization() { // ... }, afterMemoization() { // ... }, beforeExecution() { // ... }, afterExecution() { // ... }, transformOutput({ result, step }) { // ... return { // All returns are optional result: { // Transform data before it goes back to Inngest data: transformData(result.data) } } }, finished({ result }) { // ... }, beforeResponse() { // ... }, }; }, }; }, }); ``` --- ### `onSendEvent` lifecycle Triggered when an event is going to be sent via `inngest.send()`, `step.sendEvent()`, or `step.invoke()`. Called before the events are sent to Inngest. This is where you can modify the events before they're sent. Called after events are sent to Inngest. This is where you can perform any final actions and modify the output from `inngest.send()`. ```ts new InngestMiddleware({ name: "My Middleware", init: ({ client, fn }) => { return { onSendEvent() { return { transformInput({ payloads }) { // ... }, transformOutput() { // ... }, }; }, }; }, }); ``` # Inngest client Source: https://www.inngest.com/docs/reference/python/client/overview 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 💡️ 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 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, step: inngest.Step): # 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/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/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/functions/run-priority) for more information. Options to configure how to rate limit function execution ([reference](/docs/reference/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`.
### 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, step: inngest.Step): # 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, step: inngest.Step): # 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/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 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, step: inngest.Step) -> 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: ```sh npx inngest-cli@latest 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 This guide will help you use Pydantic to perform runtime type validation when sending and receiving events. ## 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.Context, step: inngest.StepSync, ) -> None: event = PostUpvotedEvent.from_event(ctx.event) ``` # Testing Source: https://www.inngest.com/docs/reference/python/guides/testing ## 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, step: inngest.Step, ) -> str: message = await 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: - `step.invoke` and `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 `step.invoke` and `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.Context, step: inngest.StepSync, ) -> bool: email_id = step.invoke( "send-email", function=send_email, ) event = 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 `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 ## 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 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 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/blob/main/pkg/inngest/inngest/experimental/encryption_middleware.py) - [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 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 class TransformOutputResult: # Mutations to these fields within middleware will be kept after running # middleware error: typing.Optional[Exception] output: object # Mutations to these fields within middleware will be discarded after # running middleware step: typing.Optional[TransformOutputStepInfo] class TransformOutputStepInfo: id: str op: execution.Opcode opts: typing.Optional[dict[str, object]] ``` 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. # Environment variables Source: https://www.inngest.com/docs/reference/python/overview/env-vars 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/guides/development-with-docker)). 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/guides/development-with-docker). --- ## `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 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 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, step: inngest.Step, ) -> 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, step: inngest.Step, ) -> None: output = await 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 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, step: inngest.Step, ) -> 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, step: inngest.Step, ) -> None: output = 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, step: inngest.Step, ) -> 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, step: inngest.Step, ) -> None: output = 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 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, step: inngest.Step, ) -> None: user_id = ctx.event.data["user_id"] (updated_user, sent_email) = await step.parallel( ( lambda: step.run("update-user", update_user, user_id), lambda: 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, step: inngest.Step, ) -> None: parallel_steps = tuple[typing.Callable[[], typing.Awaitable[bool]]]() for user_id in ctx.event.data["user_ids"]: parallel_steps += tuple( [ functools.partial( step.run, f"get-user-{user_id}", functools.partial(update_user, user_id), ) ] ) updated_users = await step.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 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, step: inngest.Step, ) -> None: # Pass a function to step.run await step.run("my_fn", my_fn) # Args are passed after the function await step.run("my_fn_with_args", my_fn_with_args, 1, "a") # Kwargs require functools.partial await 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 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 ``` # Send event Source: https://www.inngest.com/docs/reference/python/steps/send-event 💡️ 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, step: inngest.Step, ) -> list[str]: return await step.send_event("send", inngest.Event(name="foo")) ``` # Sleep until Source: https://www.inngest.com/docs/reference/python/steps/sleep-until 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, step: inngest.Step, ) -> None: await step.sleep_until( "zzz", datetime.datetime.now() + datetime.timedelta(seconds=2), ) ``` # Sleep Source: https://www.inngest.com/docs/reference/python/steps/sleep 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, step: inngest.Step, ) -> None: await step.sleep("zzz", datetime.timedelta(seconds=2)) ``` # Wait for event Source: https://www.inngest.com/docs/reference/python/steps/wait-for-event 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, step: inngest.Step, ) -> None: res = await step.wait_for_event( "wait", event="app/wait_for_event.fulfill", timeout=datetime.timedelta(seconds=2), ) ``` # REST API Source: https://www.inngest.com/docs/reference/rest-api/index You can view our REST API docs at our API reference portal: [https://api-docs.inngest.com/docs/inngest-api](https://api-docs.inngest.com/docs/inngest-api). # Serve Source: https://www.inngest.com/docs/reference/serve/index The `serve()` API handler is used to serve your application's [functions](/docs/reference/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 {{ title: "v3" }} // or your preferred framework import { importProductImages, sendSignupEmail, summarizeText, } from "./functions"; serve({ client: inngest, functions: [sendSignupEmail, summarizeText, importProductImages], }); ``` ```ts {{ title: "v2" }} // or your preferred framework import { importProductImages, sendSignupEmail, summarizeText, } from "./functions"; serve(inngest, [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/client/create)). An array of Inngest functions defined using `inngest.createFunction()` ([reference](/docs/reference/functions/create)). The Inngest [Signing Key](/docs/platform/signing-keys) for your [selected environment](/docs/platform/environments). We recommend setting the [`INNGEST_SIGNING_KEY`](/docs/sdk/environment-variables#inngest-signing-key) environment variable instead of passing the `signingKey` option. You can find this in [the Inngest dashboard](https://app.inngest.com/env/production/manage/signing-key). 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_HOST`](/docs/sdk/environment-variables#inngest-serve-host). 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_SERVE_HOST`](/docs/sdk/environment-variables#inngest-serve-host). The minimum level to log from the Inngest serve endpoint. Defaults to `"info"`. See also [`INNGEST_LOG_LEVEL`](/docs/sdk/environment-variables#inngest-log-level). The URL used to communicate with Inngest. This can be useful in testing environments when using the Inngest Dev Server. Defaults to: `"https://api.inngest.com/"`. See also [`INNGEST_BASE_URL`](/docs/sdk/environment-variables#inngest-base-url). Override the default [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) implementation. Defaults to the runtime's native Fetch API. The ID to use to represent this application instead of the client's ID. Useful for creating many Inngest endpoints in a single application. We always recommend setting the [`INNGEST_SIGNING_KEY`](/docs/sdk/environment-variables#inngest-signing-key) over using the `signingKey` option. 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 debug page in in **development only**. See [`landingPage`](#landingPage). - `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. # `inngest/function.cancelled` {{ className: "not-prose" }} Source: https://www.inngest.com/docs/reference/system-events/inngest-function-cancelled 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/functions/create#configuration). The cancelled function's [run ID](/docs/reference/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 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/functions/create#configuration). The failed function's [run ID](/docs/reference/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/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) # Testing Source: https://www.inngest.com/docs/reference/testing/index 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@>=3.22.12`. ```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" }, { 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" }, { 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, }; }, }); ``` # Cancel on Source: https://www.inngest.com/docs/reference/typescript/functions/cancel-on Stop the execution of a running function when a specific event is received using `cancelOn`. ```ts inngest.createFunction( { id: "sync-contacts", cancelOn: [ { 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/guides/multi-step-functions#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", cancelOn: [ { event: "app/user.deleted", // ensure the async (future) event's userId matches the trigger userId if: "async.data.userId == event.data.userId", }, ], }, { event: "app/user.created" }, // ... ); ``` 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 name which will be used to cancel 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"` ## Examples ### 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 {{ title: "v3" }} inngest.createFunction( { id: "sync-contacts", cancelOn: [{ event: "app/user.deleted", match: "data.userId", timeout: "1h" }], } // ... ); ``` ```ts {{ title: "v2" }} inngest.createFunction( { name: "Sync contacts", 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. # TypeScript SDK Source: https://www.inngest.com/docs/reference/typescript/index ## Installing ```shell {{ title: "npm" }} npm install inngest ``` ```shell {{ title: "pnpm" }} pnpm add inngest ``` ```shell {{ title: "yarn" }} yarn add inngest ``` ## 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). ## Supported Versions All versions `>=v0.5.0` (released [October 5th 2022](https://github.com/inngest/inngest-js/releases/tag/v0.5.0)) are supported. If you'd like to upgrade, see the [migration guide](/docs/sdk/migration). ## 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 # Creating workflow actions Source: https://www.inngest.com/docs/reference/workflow-kit/actions 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/functions/create#event). See the Inngest Function handler [`step` argument property definition](/docs/reference/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 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/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 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" }, { 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/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 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). To use it, you'll need an application with [Inngest set up](/docs/sdk/overview), ready to [serve Inngest functions](/docs/learn/serving-inngest-functions). ## Source code
Our Workflow Kit is open source and available on Github: [**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 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. */} # Environment Variables Source: https://www.inngest.com/docs/sdk/environment-variables 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_HOST](#inngest-serve-host) - [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()` or `serve()`. 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/guides/development-with-docker). --- ## 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` is `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 will attempt to infer which mode it should be in based on environment variables such as `NODE_ENV`. --- ## INNGEST_ENV Use this to tell Inngest which [Inngest Envionment](/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_LOG_LEVEL The log level to use for the SDK. Can be one of `fatal`, `error`, `warn`, `info`, `debug`, or `silent`. Defaults to `info`. --- ## INNGEST_SERVE_HOST The host used to access this application from Inngest Cloud. 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 `serveHost` 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 `serve()`. --- ## 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 one of `allow`, `force`, 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 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 ``` # Upgrading from Inngest SDK v2 to v3 Source: https://www.inngest.com/docs/sdk/migration Description: How to migrate your code to the latest version of the Inngest TS SDK. This guide walks through migrating your code from v2 to v3 of the Inngest TS SDK. Upgrading from an earlier version? See further down the page: - [Upgrading from v1 to v2](#breaking-changes-in-v2) - [Upgrading from v0 to v1](#migrating-from-inngest-sdk-v0-to-v1) ## Breaking changes in v3 Listed below are all breaking changes made in v3, potentially requiring code changes for you to upgrade. - [Clients and functions now require IDs](#clients-and-functions-require-ids) - [Steps now require IDs](#all-steps-require-ids) - [Refactored serve handlers](#serve-handlers-refactored) - [Removed shorthand function creation](#shorthand-function-creation-removed) - [Refactored environment variables and config](#environment-variables-and-configuration) - [Advanced: Updating custom framework serve handlers](#advanced-updating-custom-framework-serve-handlers) - [Removed `fns` option](#fns-removed) ## Removing the guard rails Aside from some of the breaking changes above, this version also some new features. - **Versioning and state recovery** - Functions can change over time and even mid-run; our new engine will recover and adapt, even for functions running across huge timespans. - **Allow mixing step and async logic** - Top-level `await` alongside steps is now supported within Inngest functions, allowing easier reuse of logic and complex use cases like dynamic imports. - **Sending events returns IDs** - Sending an event now returns the event ID that has created. See [Introducing Inngest TypeScript SDK v3.0](/blog/releasing-ts-sdk-3?ref=migration) to see what these features unlock for the future of the TS SDK. ## A simple example The surface-level changes for v3 and mostly small syntactical changes, which TypeScript should be able to guide you through. Here's a quick view of transitioning a client, function, and serve handler to v3. When migrating, you'll want your ID to stay the same to ensure that in-progress runs switch over smoothly. We export a `slugify()` function you can use to generate an ID from your existing name as we used to do internally. ```ts inngest.createFunction( { id: slugify("Onboarding Example"), name: "Onboarding Example" }, { event: "app/user.created" }, async ({ event, step }) => { // ... } ); ``` This is only needed to ensure function runs started on v2 will transition to v3; new functions can specify any ID. ⚠️ `slugify()` **should only be applied to function IDs, not application IDs**. Changing the application ID will result new app, archiving the existing one.
```ts new Inngest({ id: "My App", }); inngest.createFunction( // NOTE: You can manually slug IDs or import slugify to convert names to IDs automatically. // { id: "onboarding-example", name: "Onboarding example" }, { id: slugify("Onboarding example"), name: "Onboarding example" }, { event: "app/user.created" }, async ({ event, step }) => { await step.run("send-welcome-email", () => sendEmail(event.user.email, "Welcome!") ); await step.waitForEvent( "wait-for-profile-completion", { event: "app/user.profile.completed", timeout: "1d", match: "data.userId", } ); await step.sleep("wait-a-moment", "5m"); if (!profileCompleted) { await step.run("send-profile-reminder", () => sendEmail(event.user.email, "Complete your profile!") ); } } ); export default serve({ client: inngest, functions: [fn], }); ``` ```ts // Clients only previously required a `name`, but we want to be // explicit that this is used to identify your application and manage // concepts such as deployments. new Inngest({ name: "My App" }); inngest.createFunction( // Similarly, functions now require an `id` and `name` is optional. { name: "Onboarding Example" }, { event: "app/user.created" }, async ({ event, step }) => { // `step.run()` stays the same. await step.run("send-welcome-email", () => sendEmail(event.user.email, "Welcome!") ); // The shape of `waitForEvent` has changed; all steps now require // an ID. await step.waitForEvent( "app/user.profile.completed", { timeout: "1d", match: "data.userId", } ); // All steps, even sleeps, require IDs. await step.sleep("5m"); if (!profileCompleted) { await step.run("send-profile-reminder", () => sendEmail(event.user.email, "Complete your profile!") ); } } ); // Serving now uses a single object parameter for better readability. export default serve(inngest, [fn]); ``` If during migration your function ID is not the same, you'll see duplicated functions in your function list. In that case, the recommended approach is to archive the old function using the dashboard. ## Clients and functions require IDs When instantiating a client using `new Inngest()` or creating a function via `inngest.createFunction()`, it's now required to pass an `id` instead of a `name`. We recommend changing the property name and wrapping the value in `slugify()` to ensure you don't redeploy any functions. #### Creating a client ```ts inngest = new Inngest({ id: "My App", }); ``` ```ts inngest = new Inngest({ name: "My App", }); ``` #### Creating a function ```ts inngest.createFunction( { id: "send-welcome-email", name: "Send welcome email" }, { event: "app/user.created" }, async ({ event }) => { // ... } ); ``` ```ts inngest.createFunction( { name: "Send welcome email" }, { event: "app/user.created" }, async ({ event }) => { // ... } ); ``` Previously, only `name` was required, but this implied that the value was safe to change. Internally, we used this name to produce an ID which was used during deployments and executions. ## All steps require IDs When using any `step.*` tool, an ID is now required to ensure that determinism across changes to a function is easier to reason about for the user and the underlying engine. The addition of these IDs allows you to deploy hotfixes and logic changes to long-running functions without fear of errors, failures, or panics. Beforehand, any changes to a function resulted in an irrecoverable error if step definitions changed. With this, changes to a function are smartly applied by default. Every step tool now takes a new option, `StepOptionsOrId`, as its first argument. Either a `string`, indicating the ID for that step, or an object that can also include a friendly `name`. ```ts type StepOptionsOrId = | string | { id: string; name?: string; }; ``` #### `step.run()` This tool shouldn't require any changes. We'd still recommend changing the ID to something that's more obviously an identifier, like `send-welcome-email`, but you should wait for all existing v2 runs to complete before doing so. See [Handling in-progress runs triggered from v2](#handling-in-progress-runs-triggered-from-v2) for more information. #### `step.sendEvent()` ```ts step.sendEvent("broadcast-user-creation", { name: "app/user.created", data: { /* ... */ }, }); ``` ```ts step.sendEvent({ name: "app/user.created", data: { /* ... */ }, }); ``` #### `step.sleep()` ```ts step.sleep("wait-before-poll", "1m"); ``` ```ts step.sleep("1m"); ``` #### `step.sleepUntil()` ```ts step.sleepUntil("wait-for-user-birthday", specialDate); ``` ```ts step.sleepUntil(specialDate); ``` #### `step.waitForEvent()` ```ts step.waitForEvent("wait-for-user-login", { event: " app/user.login", timeout: "1h ", }); ``` ```ts step.waitForEvent("app/user.login", { timeout: "1h ", }); ``` ## Serve handlers refactored Serving functions could become a bit unwieldy with the format we had, so we've slightly altered how you serve your functions to ensure proper discoverability of options and aid in readability when revisiting the code. In v2, `serve()` would always return `any`, to ensure compatibility with any version of any framework. If you're experiencing issues, you can return to this - though we don't recommend it - by using a type assertion such as `serve() as any`. Also see the [Environment variables and config](#environment-variables-and-configuration) section. ```ts export default serve({ client: inngest, functions, // ...options }); ``` ```ts export default serve(inngest, functions, { // ...options }); ``` ## Shorthand function creation removed `inngest.createFunction()` can no longer take a `string` as the first or second arguments; an object is now required to aid in the discoverability of options and configuration. ```ts inngest.createFunction( { id: "send-welcome-email", name: "Send welcome email" }, { event: "app/user.created" }, async () => { // ... } ); ``` ```ts inngest.createFunction( "Send welcome email", "app/user.created", async () => { // ... } ); ``` ## Environment variables and configuration The arrangement of environment variables available has shifted a lot over the course of v2, so in v3 we've streamlined what's available and how they're used. We've refactored some environment variables for setting URLs for communicating with Inngest. - **✅ Added `INNGEST_BASE_URL`** - Sets the URL to communicate with Inngest in one place, e.g. `http://localhost:8288`. - **🛑 Removed `INNGEST_API_BASE_URL`** - Set `INNGEST_BASE_URL` instead. - **🛑 Removed `INNGEST_DEVSERVER_URL`** - Set `INNGEST_BASE_URL` instead. If you were using `INNGEST_DEVSERVER_URL` to test a production build against a local dev server, set `INNGEST_BASE_URL` to your dev server's address instead. We've also added some new environment variables based on config options available when serving Inngest functions. - **✅ Added `INNGEST_SERVE_HOST`** - Sets the `serveHost` serve option, e.g. `https://www.example.com`. - **✅ Added `INNGEST_SERVE_PATH`** - Sets the `servePath` serve option, e.g. `/api/inngest`. - **✅ Added `INNGEST_LOG_LEVEL`** - One of `"fatal" | "error" | "warn" | "info" | "debug" | "silent"`. Setting to `"debug"` will also set `DEBUG=inngest:*`. - **✅ Added `INNGEST_STREAMING`** - One of `"allow" | "force" | "false"`. Check out the [Environment variables](/docs/sdk/environment-variables?ref=migration) page for information on all current environment variables. In this same vein, we've also refactored some configuration options when creating an Inngest client and serving functions. - `new Inngest()` - **✅ Added `baseUrl`** - Sets the URL to communicate with Inngest in one place, e.g. `"http://localhost:8288"`. Synonymous with setting the `INNGEST_BASE_URL` environment variable above. - **🛑 Removed `inngestBaseUrl`** - Set `baseUrl` instead. - `serve()` - **✅ Added `baseUrl`** - Sets the URL to communicate with Inngest in one place, e.g. `"http://localhost:8288"`. Synonymous with setting the `INNGEST_BASE_URL` environment variable above or using `baseUrl` when creating the client. - **🛑 Removed `inngestBaseUrl`** - Set `baseUrl` instead. - **🛑 Removed `landingPage`** - The landing page for the SDK was deprecated in v2. Use the Inngest Dev Server instead via `npx inngest-cli@latest dev`. ## Handling in-progress runs triggered from v2 When upgrading to v3, there may be function runs in progress that were started using v2. For this reason, v3's engine changes are backwards compatible with v2 runs. `step.run()` should require no changes from v2 to v3. To ensure runs are backwards-compatible, make sure to keep the ID the same while in-progress v2 runs complete. ## Advanced: Updating custom framework serve handlers We found that writing custom serve handlers could be a confusing experience, focusing heavily on Inngest concepts. With v3, we've changed these handlers to now focus almost exclusively on shared concepts around how to parse requests and send responses. A handler is now defined by telling Inngest how to access certain pieces of the request and how to send a response. Handlers are also now correctly typed, meaning the output of `serve()` will be a function signature compatible with your framework. See the simple handler below that uses the native `Request` and `Response` objects to see the comparison between v2 and v3. As with custom handlers previously, check out our [custom framework handlers](/docs/learn/serving-inngest-functions#custom-frameworks?ref=migration) section to see how to define your own. ```ts serve = (options: ServeHandlerOptions) => { new InngestCommHandler({ frameworkName, ...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(); }; ``` ```ts serve: ServeHandler = (inngest, fns, opts) => { new InngestCommHandler( name, inngest, fns, { fetch: fetch.bind(globalThis), ...opts, }, (req: Request) => { new URL(req.url, `https://${req.headers.get("host") || ""}`); return { url, register: () => { if (req.method === "PUT") { return { deployId: url.searchParams.get(queryKeys.DeployId) as string, }; } }, run: async () => { if (req.method === "POST") { return { data: (await req.json()) as Record, fnId: url.searchParams.get(queryKeys.FnId) as string, stepId: url.searchParams.get(queryKeys.StepId) as string, signature: req.headers.get(headerKeys.Signature) as string, }; } }, view: () => { if (req.method === "GET") { return { isIntrospection: url.searchParams.has(queryKeys.Introspect), }; } }, }; }, ({ body, status, headers }): Response => { return new Response(body, { status, headers }); } ); return handler.createHandler(); }; ``` ## Function `fns` option removed In v2, providing a `fns` option when creating a function -- an object of functions -- would wrap those passed functions in `step.run()`, meaning you can run code inside your function without the `step.run()` boilerplate. This wasn't a very well advertised feature and had some drawbacks, so we're instead replacing it with some optional middleware. Check out the [Common Actions Middleware Example](/docs/reference/middleware/examples#common-actions-for-every-function?ref=migration) for the code. ```ts new Inngest({ id: "my-app", name: "My App", middleware: [createActionsMiddleware(actions)], }); inngest.createFunction( { name: "Send welcome email" }, { event: "app/user.created" }, async ({ event, action }) => { await action.getUserFromDb(event.data.userId); await action.sendWelcomeEmail(user.email); } ); ``` ```ts new Inngest({ name: "My App" }); inngest.createFunction( { name: "Send welcome email", fns: actions, }, { event: "app/user.created" }, async ({ event, fns }) => { await fns.getUserFromDb(event.data.userId); await fns.sendWelcomeEmail(user.email); } ); ``` --- # Upgrading from Inngest SDK v1 to v2 This guide walks through migrating your code from v1 to v2 of the Inngest TS SDK. ## Breaking changes in v2 Listed below are all breaking changes made in v2, potentially requiring code changes for you to upgrade. - [Better event schemas](#better-event-schemas) - create and maintain your event types with a variety of native tools and third-party libraries - [Clearer event sending](#clearer-event-sending) - we removed some alternate methods of sending events to settle on a common standard - [Removed `tools` parameter](#removed-tools-parameter) - use `step` instead of `tools` for step functions - [Removed ability to `serve()` without a client](#removed-ability-to-serve-without-a-client) - everything is specified with a client, so it makes sense for this to be the same - [Renamed `throttle` to `rateLimit`](#renamed-throttle-to-ratelimit) - the concept didn't quite match the naming ## New features in v2 Aside from some of the breaking features above, this version also adds some new features that aren't breaking changes. - [Middleware](/docs/reference/middleware/overview?ref=migration) - specify functions to run at various points in an Inngest client's lifecycle - **Logging** - use a default console logger or specify your own to log during your workflows ## Better event schemas Typing events is now done using a new `EventSchemas` class to create a guided, consistent, and extensible experience for declaring an event's data. This helps us achieve a few goals: - Reduced duplication (no more `name`!) - Allow many different methods of defining payloads to suit your codebase - Easy to add support for third-party libraries like Zod and TypeBox - Much clearer messaging when an event type doesn't satisfy what's required - Allows the library to infer more data itself, which allows us to add even more powerful type inference ```ts // ❌ Invalid in v2 type Events = { "app/user.created": { name: "app/user.created"; data: { id: string }; }; "app/user.deleted": { name: "app/user.deleted"; data: { id: string }; }; }; new Inngest(); ``` Instead, in v2, we use a new `EventSchemas` class and its methods to show current event typing support clearly. All we have to do is create a `new EventSchemas()` instance and pass it into our `new Inngest()` instance. ```ts // ⬆️ New "EventSchemas" class // ✅ Valid in v2 - `fromRecord()` type Events = { "app/user.created": { data: { id: string }; }; "app/user.deleted": { data: { id: string }; }; }; new Inngest({ schemas: new EventSchemas().fromRecord(), }); ``` Notice we've reduced the duplication of `name` slightly too; a common annoyance we've been seeing for a while! We use `fromRecord()` above to match the current event typing quite closely, but we now have some more options to define events without having to shim, like `fromUnion()`: ```ts // ✅ Valid in v2 - `fromUnion()` type AppUserCreated = { name: "app/user.created"; data: { id: string }; }; type AppUserDeleted = { name: "app/user.deleted"; data: { id: string }; }; new EventSchemas().fromUnion(); ``` This approach also gives us scope to add explicit support for third-party libraries, like Zod: ```ts // ✅ Valid in v2 - `fromZod()` z.object({ id: z.string(), }); new EventSchemas().fromZod({ "app/user.created": { data: userDataSchema }, "app/user.deleted": { data: userDataSchema }, }); ``` Stacking multiple event sources was technically supported in v1, but was a bit shaky. In v2, providing multiple event sources and optionally overriding previous ones is built in: ```ts // ✅ Valid in v2 - stacking new EventSchemas() .fromRecord() .fromUnion() .fromZod(zodEventSchemas); ``` Finally, we've added the ability to pull these built types out of Inngest for creating reusable logic without having to create an Inngest function. Inngest will append relevant fields and context to the events you input, so this is a great type to use for quickly understanding the resulting shape of data. ```ts new Inngest({ name: "My App" }); type Events = GetEvents; ``` For more information, see [Defining Event Payload Types](/docs/reference/client/create#defining-event-payload-types?ref=migration). ## Clearer event sending v1 had two different methods of sending events that shared the same function. This "overload" resulted in autocomplete typing for TypeScript users appear more complex than it needed to be. In addition, using a particular signature meant that you're locked in to sending a particular named event, meaning sending two different events in a batch required refactoring your call. For these reasons, we've removed a couple of the event-sending signatures and settled on a single standard. ```ts // ❌ Invalid in v2 inngest.send("app/user.created", { data: { userId: "123" } }); inngest.send("app/user.created", [ { data: { userId: "123" } }, { data: { userId: "456" } }, ]); // ✅ Valid in v1 and v2 inngest.send({ name: "app/user.created", data: { userId: "123" } }); inngest.send([ { name: "app/user.created", data: { userId: "123" } }, { name: "app/user.created", data: { userId: "456" } }, ]); ``` ## Removed `tools` parameter The `tools` parameter in a function was marked as deprecated in v1 and is now being fully removed in v2. You can swap out `tools` with `step` in every case. ```ts inngest.createFunction( { name: "Example" }, { event: "app/user.created" }, async ({ tools, step }) => { // ❌ Invalid in v2 await tools.run("Foo", () => {}); // ✅ Valid in v1 and v2 await step.run("Foo", () => {}); } ); ``` ## Removed ability to `serve()` without a client In v1, serving Inngest functions could be done without a client via `serve("My App Name", ...)`. This limits our ability to do some clever TypeScript inference in places as we don't have access to the client that the functions have been created with. We're shifting to ensure the client is the place where everything is defined and created, so we're removing the ability to `serve()` with a string name. ```ts // ❌ Invalid in v2 serve("My App", [...fns]); // ✅ Valid in v1 and v2 serve(inngest, [...fns]); ``` As is the case already in v1, the app's name will be the name of the client passed to serve. To preserve the ability to explicitly name a serve handler, you can now pass a `name` option when serving to use the passed string instead of the client's name. ```ts serve(inngest, [...fns], { name: "My Custom App Name", }); ``` ## Renamed `throttle` to `rateLimit` Specifying a rate limit for a function in v1 meant specifying a `throttle` option when creating the function. The term "throttle" was confusing here, as the definition of throttling can change depending on the context, but usually implies that "throttled" events are still eventually used to trigger an event, which was not the case. To be clearer about the functionality of this option, we're renaming it to `rateLimit` instead. ```ts inngest.createFunction( { name: "Example", throttle: { count: 5 }, // ❌ Invalid in v2 rateLimit: { limit: 5 }, // ✅ Valid in v2 }, { event: "app/user.created" }, async ({ tools, step }) => { // ... } ); ``` --- ## Migrating from Inngest SDK v0 to v1 This guide walks through migrating to the Inngest TS SDK v1 from previous versions. ## What's new in v1 - **Step functions and tools are now async** - create your flow however you'd express yourself with JavaScript Promises. - **`inngest.createFunction` for everything** - all functions are now step functions; just use step tools within any function. - **Unified client instantiation and handling of schemas via `new Inngest()`** - removed legacy helpers that required manual types. - **A foundation for continuous improvement:** - Better type inference and schemas - Better error handling - Clearer patterns and tooling - Advanced function configuration ## Replacing function creation helpers Creating any Inngest function now uses `inngest.createFunction()` to create a consistent experience. - All helpers have been removed - `inngest.createScheduledFunction()` has been removed - `inngest.createStepFunction()` has been removed ```ts // ❌ Removed in v1 import { createFunction, createScheduledFunction, createStepFunction, } from "inngest"; // ❌ Removed in v1 inngest.createScheduledFunction(...); inngest.createStepFunction(...); ``` The following is how we would always create functions without the v0 helpers. ```ts // ✅ Valid in v1 // We recommend exporting this from ./src/inngest/client.ts, giving you a // singleton across your entire app. inngest = new Inngest({ name: "My App" }); inngest.createFunction( { name: "Single step" }, { event: "example/single.step" }, async ({ event, step }) => "..." ); inngest.createFunction( { name: "Scheduled" }, { cron: "0 9 * * MON" }, async ({ event, step }) => "..." ); inngest.createFunction( { name: "Step function" }, { event: "example/step.function" }, async ({ event, step }) => "..." ); ``` This helps ensure that important pieces such as type inference of events has a central place to reside. As such, each of the following examples requries an Inngest Client (`new Inngest()`) is used to create the function. ```ts // We recommend exporting your client from a separate file so that it can be // reused across the codebase. inngest = new Inngest({ name: "My App" }); ``` See the specific examples below of how to transition from a helper to the new signatures.
`createFunction()` ```ts // ❌ Removed in v1 createFunction( "Single step", "example/single.step", async ({ event }) => "..." ); ``` ```ts // ✅ Valid in v1 new Inngest({ name: "My App" }); inngest.createFunction( { name: "Single step" }, { event: "example/single.step" }, async ({ event, step }) => "..." ); ```
`createScheduledFunction()` or `inngest.createScheduledFunction()` ```ts // ❌ Removed in v1 createScheduledFunction( // or inngest.createScheduledFunction "Scheduled", "0 9 * * MON", async ({ event }) => "..." ); ``` ```ts // ✅ Valid in v1 new Inngest({ name: "My App" }); inngest.createFunction( { name: "Scheduled" }, { cron: "0 9 * * MON" }, async ({ event, step }) => "..." ); ```
`createStepFunction` or `inngest.createStepFunction` ```ts // ❌ Removed in v1 createStepFunction( "Step function", "example/step.function", ({ event, tools }) => "..." ); ``` ```ts // ✅ Valid in v1 new Inngest({ name: "My App" }); inngest.createFunction( { name: "Step function" }, { event: "example/step.function" }, async ({ event, step }) => "..." ); ```
## Updating to async step functions The signature of a step function is changing. - **`tools` is now `step`** - We renamed this to be easier to reason about billing and make the code more readable. - **Always `async`** - Every Inngest function is now an async function with access to async `step` tooling. - **Steps now return promises** - To align with the async patterns that developers are used to and to enable more flexibility, make sure to `await` steps. Step functions in v0 were synchronous, meaning steps had to run sequentially, one after the other. v1 brings the full power of asynchronous JavaScript to those functions, meaning you can use any and all async tooling at your disposal; `Promise.all()`, `Promise.race()`, loops, etc. ```ts await Promise.all([ step.run("Send email", () => sendEmail(user.email, "Welcome!")), step.run("Send alert to staff", () => sendAlert("New user created!")), ]); ``` Here we look at an example of a step function in v0 and compare it with the new v1. ```ts // ⚠️ v0 step function export default createStepFunction( "Example", "app/user.created", ({ event, tools }) => { tools.run("Get user email", () => getUser(event.userId)); tools.run("Send email", () => sendEmail(user.email, "Welcome!")); tools.run("Send alert to staff", () => sendAlert("New user created!")); } ); ``` ```ts // ✅ v1 step function export default inngest.createFunction( { name: "Example" }, { event: "app/user.created" }, async ({ event, step }) => { // The step must now be awaited! await step.run("Get user email", () => getUser(event.userId)); await step.run("Send email", () => sendEmail(user.email, "Welcome!")); await step.run("Send alert to staff", () => sendAlert("New user created!")); } ); ``` These two examples have the exact same functionality. As above, there are a few key changes that were required. - Using `createFunction()` on the client to create the step function - Awaiting step tooling to ensure they run in order - Using `step` instead of `tools` When translating code to v1, be aware that not awaiting a step tool will mean it happens in the background, in parallel to the tools that follow. Just like a regular JavaScript async function, `await` halts progress, which is sometimes just what you want! Async step functions with v1 of the Inngest TS SDK unlocks a huge `Array`. To explore these further, check out the [multi-step functions](/docs/guides/multi-step-functions?ref=migration) docs. ## Advanced: Updating custom framework serve handlers If you're using a custom serve handler and are creating your own `InngestCommHandler` instance, a `stepId` must be provided when returning arguments for the `run` command. This can be accessed via the query string using the exported `queryKeys.StepId` enum. ```ts run: async () => { if (req.method === "POST") { return { fnId: url.searchParams.get(queryKeys.FnId) as string, // 🆕 stepId is now required stepId: url.searchParams.get(queryKeys.StepId) as string, ``` # 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 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 ``` To get started, install the SDK via `go get`: ```shell go get github.com/inngest/inngestgo ``` To get started, install the SDK via `pip`: ```shell pip install inngest ``` You'll need to do a few things to get set up, which will only take a few minutes. 1. [Set up and serve the Inngest API for your framework](/docs/learn/serving-inngest-functions) 2. [Define and write your functions](/docs/functions) 3. [Trigger functions with events](/docs/events) # Self-hosting Source: https://www.inngest.com/docs/self-hosting Description: Learn how to self-host Inngest. Includes configuration options and instructions for using external services.' # Self-hosting Self-hosting support for Inngest is supported as of the 1.0 release. * [Why self-host Inngest?](#why-self-host-inngest) * [Inngest system architecture](#inngest-system-architecture) * [How to self-host Inngest](#how-to-self-host-inngest) ## Why self-host Inngest? While the easiest way to get started with Inngest is using our hosted platform, including our generous [free tier](/pricing?ref=docs-self-hosting), we understand that developers may want to self-host for a variety of reasons. If security or data privacy are concerns, review our [security documentation](/docs/learn/security?ref=docs-self-hosting) for more information including details about [end-to-end encryption](/docs/features/middleware/encryption-middleware?ref=docs-self-hosting). ## Inngest system architecture To best understand how to self-host Inngest, it's important to understand the system architecture and components. ![Inngest system architecture diagram](/assets/docs/self-hosting/system-architecture-2024-09-23.png) The system is composed of the following services: * **Event API** - Receives events from SDKs via HTTP requests. Authenticates client requests via [Event Keys](/docs/events/creating-an-event-key?ref=docs-self-hosting). The Event API publishes event payloads to an internal event stream. * **Event stream** - Acts as buffer between the _Event API_ and the _Runner_. * **Runner** - Consumes incoming events and performs several actions: * Scheduling of new “function runs” (aka jobs) given the event type, creating initial run state in the _State store_ database. Runs are added to queues given the function's flow control configuration. * Resume functions paused via [`waitForEvent`](/docs/features/inngest-functions/steps-workflows/wait-for-event?ref=docs-self-hosting) with matching expressions. * Cancels running functions with matching [`cancelOn`](/docs/features/inngest-functions/cancellation/cancel-on-events?ref=docs-self-hosting) expressions * Writes ingested events to a database for historical record and future replay. * **Queue** - A multi-tenant aware, multi-tier queue designed for fairness and various [flow control](/docs/guides/flow-control?ref=docs-self-hosting) methods (concurrency, throttling, prioritization, debouncing, rate limiting) and [batching](/docs/guides/batching?ref=docs-self-hosting). * **Executor** - Responsible for executing functions, from initial execution, step execution, writing incremental function run state to the _State store_, and retries after failures. * **State store (database)** - Persists data for pending and ongoing function runs. Data includes initial triggering event(s), step output and step errors. * **Database** - Persists system data and history including Apps, Functions, Events, Function run results. * **API** - GraphQL and REST APIs for programmatic access and management of system resources. * **Dashboard UI** - The UI to manage apps, functions and view function run history. The source code for Inngest and all services is [available on GitHub](https://github.com/inngest/inngest). ## How to self-host Inngest To begin self-hosting Inngest, you only need to install the Inngest CLI. The Inngest CLI is a single binary which includes all Inngest services and can be run in any environment. Alternatively, you download the binary directly from [GitHub releases](https://github.com/inngest/inngest/releases). ```plaintext {{ title: npm" }} npm install -g inngest-cli ``` ```plaintext {{ title: "Docker" }} docker pull inngest/inngest ``` ```plaintext {{ title: "curl" }} curl -sfL https://cli.inngest.com/install.sh ``` Now that you have the CLI installed, you can start the Inngest server using the `inngest start` command. ```plaintext {{ title: "shell" }} inngest start ``` ```plaintext {{ title: "Docker" }} docker run -p 8288:8288 inngest/inngest inngest start ``` This will start the Inngest server on the default port `8288` and use the default configuration, including SQLite for persistence. ## Configuring the server can be done via command line flags, environment variables, or a configuration file. By default, the server will: * Run on `localhost:8288`, including the Event API, API, and Dashboard UI. * Use an in-memory Redis server for the queue and state store. (See [Using external services](#using-external-services) for more information) * Use SQLite for persistence. The default database is located at `./.inngest/main.db`. Queue and state store snapshots are periodically saved to the SQLite database, including prior to shutdown. * Disable app sync polling to check for new functions or updated configurations (see `--poll-interval` flag). To securely configure your server, create your event and signing keys using whatever format that you choose and start the Inngest server using them. You can also pass them via environment variable (see below): ```plaintext inngest start --event-key --signing-key ``` Then you can use these same keys as environment variables when starting your application (`INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY`). [See below](#configuring-inngest-sdks-to-use-self-hosted-server) for an example Node.js startup command. To see all the available options, run `inngest start --help`: ```plaintext $ inngest start --help [Beta] Run Inngest as a single-node service. Usage: inngest start [flags] Examples: inngest start Flags: --config string Path to an Inngest configuration file --event-key strings Event key(s) that will be used by apps to send events to the server. -h, --help Output this help information --host string Inngest server hostname -p, --port string Inngest server port (default "8288") -u, --sdk-url strings App serve URLs to sync (ex. http://localhost:3000/api/inngest) --signing-key string Signing key used to sign and validate data between the server and apps. Persistence Flags: --postgres-uri string [Experimental] PostgreSQL database URI for configuration and history persistence. Defaults to SQLite database. --redis-uri string Redis server URI for external queue and run state. Defaults to self-contained, in-memory Redis server with periodic snapshot backups. --sqlite-dir string Directory for where to write SQLite database. Advanced Flags: --poll-interval int Interval in seconds between polling for updates to apps --queue-workers int Number of executor workers to execute steps from the queue (default 100) --retry-interval int Retry interval in seconds for linear backoff when retrying functions - must be 1 or above --tick int The interval (in milliseconds) at which the executor polls the queue (default 150) Global Flags: --json Output logs as JSON. Set to true if stdout is not a TTY. -l, --log-level string Set the log level. One of: trace, debug, info, warn, error. (default "info") -v, --verbose Enable verbose logging. ```
**Environment variables** Any CLI option can be set via environment variable by converting the flag to uppercase, replacing hyphens with underscores, and prefixing it with `INNGEST_`. For example, `--port 8288` can be set with the `INNGEST_PORT` environment variable.
**Configuration file** (`inngest.yaml`, `inngest.json`, etc.) A configuration file can be specified with the `--config` flag. The file can be in YAML, JSON, TOML, or any other format supported by [Viper](https://github.com/spf13/viper). `urls` is used instead of `sdk-url` to specify your application's Inngest serve endpoints. An example configuration file is shown below: ```yaml {{ title: "inngest.yaml" }} urls: - http://localhost:3000/api/inngest poll-interval: 60 redis-uri: redis://localhost:6379 sqlite-dir: /app/data ``` ```json {{ title: "inngest.json" }} { "urls": [ "http://localhost:3000/api/inngest" ], "poll-interval": 60, "redis-uri": "redis://localhost:6379", "sqlite-dir": "/app/data" } ```
### Configuring Inngest SDKs to use self-hosted server By default, the Inngest SDK will use URLs of the managed Inngest platform. To connect to a self-hosted server, set the [`INNGEST_DEV`](/docs/sdk/environment-variables#inngest-dev) and [`INNGEST_BASE_URL`](/docs/sdk/environment-variables#inngest-base-url) environment variables. As mentioned above, you'll also need to set the `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` environment variables for securely connecting your application to the Inngest server. For example, to connect to a self-hosted server running on `localhost:8288` for a Node.js app, set the following environment variables: ```plaintext INNGEST_EVENT_KEY= \ INNGEST_SIGNING_KEY= \ INNGEST_DEV=0 \ INNGEST_BASE_URL=http://localhost:8288 \ node ./server.js ``` ### Using external services Inngest can be configured to use external services for the queue and state store, and soon, the database **External Redis server** With goal of simplifying the initial setup, the Inngest server will run an in-memory Redis server for the queue and state store. As this is running within the same process as the Inngest server, running your own Redis server can improve performance and reliability of the system. You may choose to run your own Redis server or use a cloud-based Redis service like AWS ElastiCache, Redis Cloud, etc. To use an external Redis server, set the `redis-uri` flag to the Redis server URI. **External Postgres database (experimental)** By default, the Inngest server uses SQLite for persistence. This is convenient for zero-dependency deployments, but does not support scaling beyond a single node. You may choose to run your own Postgres database or use a cloud-based Postgres service like AWS RDS, Neon, Supabase, etc. Postgres support is experimental and should be used with caution. To use an external Postgres database, set the `postgres-uri` flag to your Postgres connection string URI. ## Docker compose example _Coming soon_ ## Roadmap & feature requests Planned features for self-hosting include: * Postgres external database support. * Event key and signing key management via API and UI. * Multi-node Inngest server support. Run standalone services (API, Executor, etc.) instead of all services in a single process. To suggest additional features, please submit feedback on our [public roadmap](https://roadmap.inngest.com/roadmap). Check out [the source code on GitHub](https://github.com/inngest/inngest) to file issues or submit pull requests. # Connect Source: https://www.inngest.com/docs/setup/connect These docs are part of a developer preview for Inngest's `connect` API. Learn more about the [developer preview here](#developer-preview). 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. ## Minimum requirements ### Language - **TypeScript**: SDK `3.34.1` or higher. - **Go**: SDK `0.11.2` or higher. - **Python**: SDK `0.4.21` 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` 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. (Note: Python is not yet supported; [upvote and join the waiting list here](https://roadmap.inngest.com/roadmap?id=2bac8d74-288f-47c7-8afc-3fd1a0e94654)) 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' }, { 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() app, 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) } f := inngestgo.CreateFunction( app, 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 }, ) fmt.Println("Worker: connecting") ws, err := inngestgo.Connect(ctx, inngestgo.ConnectOpts{ InstanceID: inngestgo.Ptr("example-worker"), Apps: []inngestgo.Handler{ app, }, }) if err != nil { fmt.Printf("ERROR: %#v\n", err) os.Exit(1) } defer func(ws connect.WorkerConnection) { <-ctx.Done() err := ws.Close() if err != nil { fmt.Printf("could not close connection: %s\n", err) } }(ws) } ``` ```python import asyncio import inngest from inngest.experimental.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, step: inngest.Step) -> 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 (Coming soon)** - 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. ## 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/dev-server). 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 The `connect` API is currently in developer preview and is not yet recommended for critical production workloads. We recommend deploying to a staging environment first prior to 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 `maxConcurrency` 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. The `maxConcurrency` option is not yet supported. It will be supported in a future release before general availability. ```ts await connect({ apps: [...], maxConcurrency: 100, }) ``` ## 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. See the [developer preview limitations](#limitations) for more details. `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) ## 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: [helloWorld] }], }); 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 ``` ## Self hosted Inngest Self-hosting support for `connect` is in development. Please [contact us](https://app.inngest.com/support) for more info. 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 dynamically re-write the gateway URL that the SDK uses to connect. ```ts await connect({ apps: [...], rewriteGatewayEndpoint: (url) => { // ex. "wss://gw2.connect.inngest.com/v0/connect" // If not running in dev mode, return if (!process.env.INNGEST_DEV) { new URL(url); clusterUrl.host = 'my-cluster-host:8289'; return clusterUrl.toString(); } return url; }, }) ``` {/* TODO: multiple apps in a single worker */} ## Migrating from serve _Guide on migration from `serve` to `connect` coming soon_ ## Developer preview The `connect` API is currently in developer preview. This means that the API is not yet recommended for critical production workloads and is subject to breaking changes. During the developer preview, the `connect` API is available to all Inngest accounts with the following plan-limits: * Free plan: 2 concurrent worker connections * All paid plans: 10 concurrent worker connections * Max apps per connection: 10 Final plan limitations will be announced prior to general availability. Please [contact us](https://app.inngest.com/support) if you need to increase these limits. ### Limitations During the developer preview, there are some limitations to using `connect` to be aware of. Please [contact us](https://app.inngest.com/support) if you'd like clarity on any of the following: * **Lost connections may result in long timeouts before retries** - If a connection is lost in the middle of a step, the step may result in a timeout of the max step duration (2 hours) before the step is retried. Graceful shutdowns, as documented above, handle this correctly, but if a networking issue occurs and the worker cannot re-establish connection, the step may result in a timeout. * **Worker-level maximum concurrency** - This is not yet supported. When completed, 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. * **Reconnection policy is not configurable** - The SDK will attempt to reconnect to Inngest an infinite number of times. We will expose a configurable reconnection policy in the future. * **Rollbacks** - Rollbacks of app versions may not work as expected. Additional functionality and control around rollbacks is coming in a future release. # Streaming Source: https://www.inngest.com/docs/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: - [Next.js on Vercel Edge Functions](/docs/learn/serving-inngest-functions#framework-next-js) - [Remix on Vercel Edge Functions](/docs/learn/serving-inngest-functions#framework-remix) - [Cloudflare Workers](/docs/learn/serving-inngest-functions#framework-cloudflare-workers) ## 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: "allow", }); ``` This can be one of the following values: - `false` - Streaming will never be used. This is the default. - `"allow"` - Streaming will be used if we can confidently detect support for it by verifying that the platform, environment, and serve handler support streaming. ⚠️ We also allow `"force"`, where streaming will be used if the serve handler supports it, but completely overrides the SDK's attempts to verify if the platform supports streaming. This is not recommended, but is an escape hatch if you know that streaming is supported and you're in a restricted environment that has little or no access to the environment. # TypeScript Source: https://www.inngest.com/docs/typescript description = `Learn the Inngest SDK's type safe features with TypeScript` The Inngest SDK leverages the full power of TypeScript, providing you with some awesome benefits when handling events: - 📑 **Autocomplete**
Tab ↹ your way to victory with inferred types for every event. - **Instant feedback**
Understand exactly where your code might error before you even save the file. All of this comes together to provide some awesome type inference based on your actual production data. ## Using types Once your types are generated, there are a few ways we can use them to ensure our functions are protected. ### `new Inngest()` client We can use these when creating a new Inngest client via `new Inngest()`. This comes with powerful inference; we autocomplete your event names when selecting what to react to, without you having to dig for the name and data. ```ts {{ title: "v3" }} type UserSignup = { data: { email: string; name: string; }; }; type Events = { "user/new.signup": UserSignup; }; inngest = new Inngest({ id: "my-app", schemas: new EventSchemas().fromRecord(), }); ``` ```ts {{ title: "v2" }} type UserSignup = { data: { email: string; name: string; }; }; type Events = { "user/new.signup": UserSignup; }; inngest = new Inngest({ name: "My App", schemas: new EventSchemas().fromRecord(), }); ``` ```ts {{ filename: "inngest/sendWelcomeEmail.ts" }} export default inngest.createFunction( { id: "send-welcome-email" }, { event: "user/new.signup" }, async ({ event }) => { // "event" is fully typed to provide typesafety within this function return await email.send("welcome", event.data.email); } ); ``` ### Sending events TypeScript will also enforce your custom events being the right shape - see [Event Format](/docs/reference/events/send) for more details. We recommend putting your `new Inngest()` client and types in a single file, i.e. `/inngest/client.ts` so you can use it anywhere that you send an event. Here's an example of sending an event within a Next.js API handler: ```ts {{ filename: "pages/api/signup.ts" }} export default function handler(req: NextApiRequest, res: NextApiResponse) { createNewUser(req.body.email, req.body.password, req.body.name); // TypeScript will now warn you if types do not match for the event payload // and the user object's properties: await inngest.send({ name: "user/new.signup", data: { email: user.email, name: user.name, } }); res.status(200).json({ success: true }); } ``` ### Using with `waitForEvent` When writing step functions, you can use `waitForEvent` to pause the current function until another event is received or the timeout expires - whichever happens first. When you declare your types using the `Inngest` constructor, `waitForEvent` leverages any types that you have: ```ts {{ title: "v3" }} type UserSignup = { data: { email: string; user_id: string; name: string; }; }; type UserAccountSetupCompleted = { data: { user_id: string; }; }; type Events = { "user/new.signup": UserSignup; "user/account.setup.completed": UserAccountSetupCompleted; }; inngest = new Inngest({ id: "my-app", schemas: new EventSchemas().fromRecord(), }); ``` ```ts {{ title: "v2" }} type UserSignup = { data: { email: string; user_id: string; name: string; }; }; type UserAccountSetupCompleted = { data: { user_id: string; }; }; type Events = { "user/new.signup": UserSignup; "user/account.setup.completed": UserAccountSetupCompleted; }; inngest = new Inngest({ name: "My App", schemas: new EventSchemas().fromRecord(), }); ``` ```ts {{ title: "v3" }} export default inngest.createFunction( { id: "onboarding-drip-campaign" }, { event: "user/new.signup" }, async ({ event, step }) => { await step.run("send-welcome-email", async () => { // "event" will be fully typed provide typesafety within this function return await email.send("welcome", event.data.email); }); // We wait up to 2 days for the user to set up their account await step.waitForEvent( "wait-for-setup-complete", { event: "user/account.setup.completed", timeout: "2d", // ⬇️ This matches both events using the same property // Since both events types are registered above, this is match is typesafe match: "data.user_id", } ); if (!accountSetupCompleted) { await step.run("send-setup-account-guide", async () => { return await email.send("account_setup_guide", event.data.email); }); } } ); ``` ```ts {{ title: "v2" }} export default inngest.createFunction( { id: "Onboarding drip campaign" }, { event: "user/new.signup" }, async ({ event, step }) => { await step.run("Send welcome email", async () => { // "event" will be fully typed provide typesafety within this function return await email.send("welcome", event.data.email); }); // We wait up to 2 days for the user to set up their account await step.waitForEvent( "user/account.setup.completed", { timeout: "2d", // ⬇️ This matches both events using the same property // Since both events types are registered above, this is match is typesafe match: "data.user_id", } ); if (!accountSetupCompleted) { await step.run("Send setup account guide", async () => { return await email.send("account_setup_guide", event.data.email); }); } } ); ``` ## Helpers The TS SDK exports some helper types to allow you to access the type of particular Inngest internals outside of an Inngest function. ### GetEvents Get a record of all available events given an Inngest client. It's recommended to use this instead of directly reusing your own event types, as Inngest will add extra properties and internal events such as `ts` and `inngest/function.failed`. ```ts type Events = GetEvents; ``` By default, the returned events do not include internal events prefixed with `inngest/`, such as `inngest/function.finished`. To include these events in , pass a second `true` generic: ```ts type Events = GetEvents; ``` ### GetFunctionInput Get the argument passed to Inngest functions given an Inngest client and, optionally, an event trigger. Useful for building function factories or other such abstractions. ```ts type InputArg = GetFunctionInput; type InputArgWithTrigger = GetFunctionInput; ``` ### GetStepTools Get the `step` object passed to an Inngest function given an Inngest client and, optionally, an event trigger. Is a small shim over the top of `GetFunctionInput<...>["step"]`. ```ts type StepTools = GetStepTools; type StepToolsWithTrigger = GetStepTools; ``` ### Inngest.Any / InngestFunction.Any Some exported classes have an `Any` type within their namespace that represents any instance of that class without inference or generics. This is useful for typing lists of functions or factories that create Inngest primitives. ```ts []; ``` # Usage Limits Source: https://www.inngest.com/docs/usage-limits/inngest We have put some limits on the service to make sure we provide you a good default to start with, while also keeping it a good experience for all other users using Inngest. 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. ## 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) 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](https://www.inngest.com/pricing) 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 maxmimum event payload size is dependent on your billing plan. The default on the Free Tier is `256KB` and is upgradable to `3MB`. 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 := inngestgo.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) ``` [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 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