# 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

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)")

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:

🎉 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:
**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
```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:

### 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.

## 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:

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:

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.

1. In the modal window, specify the name of the webhook (for example, “Resend”):

You will now see your webhook page with the webhook URL at the top:

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.

Your Inngest webhook is set up!
1. Go back to the top of the page and copy your webhook URL.

1. Now navigate to the [webhooks](https://resend.com/webhooks) page in the Resend dashboard. Click on the **Add webhook** button:

1. In the modal, paste the Inngest webhook URL and choose which events you want to listen to -- tick all of them for now.

Your webhook will be created but there will be no webhook events yet.

1. To see your webhook in action, [send a test email](https://resend.com/emails) and see the webhook events recorded:

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:

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**.

You'll now see the event in the Inngest Dev Server's **Stream** tab alongside any functions that it triggered.

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.

## 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.”

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:

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.

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!! */}

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.

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:

## 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.

And a few seconds later in the Inngest cloud dashboard:

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:

### 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
###
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
We can now spot that the `downgrade-account-billing-plan` failed.
Let's expand this step to look at the retries and errors.


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.

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.

# 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

## Function metrics
Navigating to a Function displays the Function metrics page, composed of 7 charts:

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 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.

### Total runs throughput

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_ 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.

*/}
### Backlog

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.

*/}
## 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:

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 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:

# 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.

## 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.

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.

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."

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.

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.

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.

## 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 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.

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!

## 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
// 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(
{ 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.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(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(
"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: 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({ 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.

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.

## 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