From a05739a546ee41b6234f4ce8565838eac2c5a8db Mon Sep 17 00:00:00 2001
From: Charly Poly <1252066+charlypoly@users.noreply.github.com>
Date: Wed, 11 Dec 2024 16:10:11 +0100
Subject: [PATCH] docs: add Python and Go to most docs pages (#1014)
* docs: add Python and Go to most docs pages
* docs: Python and Go examples
* docs: Flow Control code snippets in Go and Python
* docs: Inngest Tour for Python and Go
* docs: serving inngest functions in Go and Python
* docs: Python & Go DX
* docs: guides in Go and Python
* feat(docs): persist language selection across pages
* docs: Python & Go coverage fixes
* feat(docs): make `` compatible with `` persisted storage
* fix(docs): broken link
---
package.json | 2 +-
.../ai-agent-network-state-routing.mdx | 4 +-
pages/docs/agent-kit/ai-agents-tools.mdx | 10 +-
pages/docs/agent-kit/overview.mdx | 4 +-
pages/docs/apps/index.mdx | 96 +++-
pages/docs/dev-server.mdx | 28 +-
pages/docs/events/index.mdx | 251 ++++++++++-
.../features/events-triggers/event-format.mdx | 38 +-
.../inngest-functions/cancellation.mdx | 32 ++
.../cancellation/cancel-on-events.mdx | 89 +++-
.../cancellation/cancel-on-timeouts.mdx | 6 +-
.../error-retries/failure-handlers.mdx | 4 +-
.../error-retries/inngest-errors.mdx | 279 +++++++++++-
.../error-retries/retries.mdx | 17 +
.../steps-workflows/sleeps.mdx | 54 +--
.../steps-workflows/step-ai-orchestration.mdx | 6 +-
.../steps-workflows/wait-for-event.mdx | 129 +-----
pages/docs/features/middleware/create.mdx | 34 +-
pages/docs/guides/background-jobs.mdx | 128 +++++-
pages/docs/guides/batching.mdx | 72 ++-
pages/docs/guides/concurrency.mdx | 301 ++++++++++++-
pages/docs/guides/debounce.mdx | 41 +-
pages/docs/guides/delayed-functions.mdx | 135 +++++-
pages/docs/guides/fan-out-jobs.mdx | 250 ++++++++++-
pages/docs/guides/handling-idempotency.mdx | 30 +-
.../guides/invoking-functions-directly.mdx | 129 +++++-
pages/docs/guides/multi-step-functions.mdx | 419 +++++++++++++++++-
pages/docs/guides/priority.mdx | 29 ++
pages/docs/guides/rate-limiting.mdx | 37 +-
pages/docs/guides/scheduled-functions.mdx | 177 +++++++-
.../guides/sending-events-from-functions.mdx | 197 +++++++-
pages/docs/guides/throttling.mdx | 39 +-
pages/docs/guides/working-with-loops.mdx | 356 ++++++++++++++-
pages/docs/learn/inngest-functions.mdx | 192 +++++++-
pages/docs/learn/inngest-steps.mdx | 392 ++++++++++++++++
.../docs/learn/serving-inngest-functions.mdx | 143 +++++-
pages/docs/platform/environments.mdx | 22 +-
pages/docs/sdk/overview.mdx | 45 +-
pages/docs/usage-limits/inngest.mdx | 20 +-
pnpm-lock.yaml | 54 ++-
shared/Docs/Code.tsx | 71 ++-
41 files changed, 4066 insertions(+), 296 deletions(-)
diff --git a/package.json b/package.json
index 30dc5de07..80a6747ad 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
"@algolia/autocomplete-preset-algolia": "1.7.4",
"@babel/core": "7.20.12",
"@fullstory/browser": "1.6.2",
- "@headlessui/react": "1.7.13",
+ "@headlessui/react": "1.7.19",
"@heroicons/react": "2.0.18",
"@mdx-js/loader": "2.2.1",
"@mdx-js/react": "2.2.1",
diff --git a/pages/docs/agent-kit/ai-agent-network-state-routing.mdx b/pages/docs/agent-kit/ai-agent-network-state-routing.mdx
index 534961b4b..aea79bfab 100644
--- a/pages/docs/agent-kit/ai-agent-network-state-routing.mdx
+++ b/pages/docs/agent-kit/ai-agent-network-state-routing.mdx
@@ -1,6 +1,6 @@
-import { Callout, GuideSelector, GuideSection, CodeGroup } from "src/shared/Docs/mdx";
+import { Callout, GuideSelector, GuideSection, CodeGroup, VersionBadge } from "src/shared/Docs/mdx";
-# Networks, state, and routing
+# Networks, state, and routing
Use Networks to create complex workflows with one or more agents.
diff --git a/pages/docs/agent-kit/ai-agents-tools.mdx b/pages/docs/agent-kit/ai-agents-tools.mdx
index 06eed349f..74bec8ae8 100644
--- a/pages/docs/agent-kit/ai-agents-tools.mdx
+++ b/pages/docs/agent-kit/ai-agents-tools.mdx
@@ -1,6 +1,6 @@
-import { Callout, GuideSelector, GuideSection, CodeGroup } from "src/shared/Docs/mdx";
+import { Callout, GuideSelector, GuideSection, CodeGroup, VersionBadge } from "src/shared/Docs/mdx";
-# Agents and Agent Tools
+# Agents and Agent Tools
You can think of an Agent as a wrapper over a single model, with instructions and tools. Calling `.run` on an Agent
@@ -20,7 +20,7 @@ Agents are the core of AgentKit. An Agent is used to call a single model with a
Here’s a simple agent, which makes a single model call using system prompts and user input:
```jsx
-import { Agent, agenticOpenai as openai } from "@inngest/agent-kit";
+import { Agent, agenticOpenai as openai, createAgent } from "@inngest/agent-kit";
const agent = createAgent({
name: "Code writer",
@@ -73,7 +73,7 @@ You can define an agent's system prompt as a string or as an async callback whic
Here's an example:
```ts
-import { Agent, Network, agenticOpenai as openai } from "@inngest/agent-kit";
+import { Agent, Network, agenticOpenai as openai, createAgent } from "@inngest/agent-kit";
const systemPrompt =
"You are an expert TypeScript programmer. Given a set of asks, think step-by-step to plan clean, " +
@@ -126,7 +126,7 @@ In AgentKit, you also define a `handler` function which is called when the tool
A more complex agent used in a network defines a description, lifecycle hooks, tools, and a dynamic set of instructions based off of network state:
```ts
-import { Agent, Network, agenticOpenai as openai } from "@inngest/agent-kit";
+import { Agent, Network, agenticOpenai as openai, createAgent } from "@inngest/agent-kit";
const systemPrompt =
"You are an expert TypeScript programmer. Given a set of asks, think step-by-step to plan clean, " +
diff --git a/pages/docs/agent-kit/overview.mdx b/pages/docs/agent-kit/overview.mdx
index df4224630..8bdc91869 100644
--- a/pages/docs/agent-kit/overview.mdx
+++ b/pages/docs/agent-kit/overview.mdx
@@ -1,6 +1,6 @@
-import { Callout, GuideSelector, GuideSection, CodeGroup } from "src/shared/Docs/mdx";
+import { Callout, GuideSelector, GuideSection, CodeGroup, VersionBadge } from "src/shared/Docs/mdx";
-# AgentKit overview
+# AgentKit overview
This page introduces the APIs and concepts to AgentKit. AgentKit is in early access, and is improving
diff --git a/pages/docs/apps/index.mdx b/pages/docs/apps/index.mdx
index bcf43fea7..dde2f89a4 100644
--- a/pages/docs/apps/index.mdx
+++ b/pages/docs/apps/index.mdx
@@ -24,13 +24,13 @@ The diagram below shows how each environment can have multiple apps which can ha
## Apps in SDK
-Each [`serve()` API handler](/docs/reference/serve) 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](/docs/reference/client/create).
+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
+```ts {{ title: "Node.js" }}
import { Inngest } from "inngest";
import { serve } from "inngest/next"; // or your preferred framework
import { sendSignupEmail } from "./functions";
@@ -42,6 +42,96 @@ serve({
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)
+}
+```
+
diff --git a/pages/docs/dev-server.mdx b/pages/docs/dev-server.mdx
index 9005375a1..144ab55aa 100644
--- a/pages/docs/dev-server.mdx
+++ b/pages/docs/dev-server.mdx
@@ -60,7 +60,10 @@ There are different ways that you can send events to the dev server when testing
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
+
+```ts {{ title: "Node.js" }}
+import { Inngest } from "inngest";
+
const inngest = new Inngest({ id: "my-app" });
await inngest.send({
name: "user.avatar.uploaded",
@@ -68,6 +71,29 @@ await inngest.send({
});
```
+```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
diff --git a/pages/docs/events/index.mdx b/pages/docs/events/index.mdx
index 731ba531f..118ed003b 100644
--- a/pages/docs/events/index.mdx
+++ b/pages/docs/events/index.mdx
@@ -1,4 +1,4 @@
-import { CodeGroup, Callout } from "src/shared/Docs/mdx";
+import { CodeGroup, Callout, GuideSelector, GuideSection } from "src/shared/Docs/mdx";
export const description = 'Learn how to send events with the Inngest SDK, set the Event Key, and send events from other languages via HTTP.';
@@ -8,6 +8,16 @@ 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' }}
import { Inngest } from "inngest";
@@ -41,6 +51,74 @@ await inngest.send({
+
+
+
+
+```python {{ filename: 'src/inngest/client.py' }}
+import inngest
+
+inngest_client = inngest.Inngest(app_id="acme-storefront-app")
+```
+
+Now with this client, you can send events from anywhere in your app. You can send a single event, or [multiple events at once](#sending-multiple-events-at-once).
+
+```python {{ filename: 'src/api/checkout/route.py' }}
+import inngest
+from src.inngest.client import inngest_client
+
+# This sends an event to Inngest.
+await inngest_client.send(
+ inngest.Event(
+ name="storefront/cart.checkout.completed",
+ data={
+ "cartId": "ed12c8bde",
+ "itemIds": ["9f08sdh84", "sdf098487", "0fnun498n"],
+ "account": {
+ "id": 123,
+ "email": "test@example.com",
+ },
+ },
+ )
+)
+```
+
+
+
+👉 `send()` is meant to be called asynchronously using `await`. For synchronous code, [use the `send_sync()` method instead](/docs/reference/python/client/send).
+
+
+
+
+
+
+
+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.
@@ -61,12 +139,23 @@ You can learn [how to create an Event Key here](/docs/events/creating-an-event-k
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' }}
import { Inngest } from "inngest";
// NOTE - It is not recommended to hard-code your Event Key in your code.
const inngest = 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...")
+```
+
@@ -76,8 +165,6 @@ Event keys are _not_ required in local development with the [Inngest Dev Server]
## Event payload format
-{/* TODO - Most of the detail and additional information not captured here should be moved to a generic reference */}
-
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).
@@ -87,6 +174,8 @@ Explore all events properties in the [Event payload format guide](/docs/features
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: { ... } },
@@ -152,6 +241,119 @@ const { ids } = await inngest.send([
*/
```
+
+
+
+
+```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.
@@ -223,6 +425,8 @@ Often, you may need to prevent duplicate events from being processed by Inngest.
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.
@@ -235,6 +439,45 @@ await inngest.send({
}
});
```
+
+
+
+
+
+```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).
@@ -250,6 +493,8 @@ For example, for two events like `storefront/item.imported` and `storefront/item
+
+
## Further reading
* [Creating an Event Key](/docs/events/creating-an-event-key)
diff --git a/pages/docs/features/events-triggers/event-format.mdx b/pages/docs/features/events-triggers/event-format.mdx
index c11dcf552..a0e668d6e 100644
--- a/pages/docs/features/events-triggers/event-format.mdx
+++ b/pages/docs/features/events-triggers/event-format.mdx
@@ -17,7 +17,7 @@ The event payload is a JSON object that must contain a `name` and `data` propert
* `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"`
-```ts {{ title: "Basic JSON event example" }}
+```json {{ title: "Basic JSON event example" }}
{
"name": "billing/invoice.paid",
"data": {
@@ -45,6 +45,42 @@ type EventPayload = {
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
diff --git a/pages/docs/features/inngest-functions/cancellation.mdx b/pages/docs/features/inngest-functions/cancellation.mdx
index 829ff8bb2..2ef94d26e 100644
--- a/pages/docs/features/inngest-functions/cancellation.mdx
+++ b/pages/docs/features/inngest-functions/cancellation.mdx
@@ -89,6 +89,38 @@ async def send_reminder_push() -> None:
pass
```
+```go {{ title: "main.go" }}
+package main
+
+import (
+ "context"
+ "github.com/inngest/inngestgo"
+ "github.com/inngest/inngestgo/step"
+)
+
+func main() {
+ f := inngestgo.CreateFunction(
+ inngestgo.FunctionOpts{
+ ID: "schedule-reminder",
+ Name: "Schedule reminder",
+ Cancel: []inngestgo.Cancel{
+ {
+ Event: "tasks/deleted",
+ IfExp: "event.data.id == async.data.id",
+ },
+ },
+ },
+ // Run on every tasks/reminder.created event.
+ inngestgo.EventTrigger("tasks/reminder.created", nil),
+ ScheduleReminder,
+ )
+}
+
+func ScheduleReminder(ctx context.Context, input inngestgo.Input[ScheduleReminderEvent]) (any, error) {
+ // ...
+}
+```
+
Let's now look at two different cancellations triggered from the Dashboard Bulk Cancellation UI:
diff --git a/pages/docs/features/inngest-functions/cancellation/cancel-on-events.mdx b/pages/docs/features/inngest-functions/cancellation/cancel-on-events.mdx
index ab912a7f5..f4d3dc42e 100644
--- a/pages/docs/features/inngest-functions/cancellation/cancel-on-events.mdx
+++ b/pages/docs/features/inngest-functions/cancellation/cancel-on-events.mdx
@@ -11,8 +11,8 @@ For our example, we'll take a reminder app where a user can schedule to be remin
@@ -101,9 +101,9 @@ Learn more in the full [reference](/docs/reference/typescript/functions/cancel-o
Delaying code to run for days or weeks is easy with `step.sleep_until`, but we need a way to be able to stop the function if the user deletes the reminder while our function is "sleeping."
-When defining a function, you can also specify the `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."
+When defining a function, you can also specify the `cancel` option which allows you to list one or more events that, when sent to Inngest, will cause the sleep to be terminated and function will be marked as "Canceled."
-Here is our schedule reminders function that leverages `cancelOn`:
+Here is our schedule reminders function that leverages `cancel`:
```py {{ title: "inngest/schedule_reminder.py" }}
@inngest_client.create_function(
@@ -174,6 +174,89 @@ Here is an example of these two events which will be matched on the `data.remind
+
+
+
+
+
+Delaying code to run for days or weeks is easy with `step.Sleep()`, but we need a way to be able to stop the function if the user deletes the reminder while our function is "sleeping."
+
+When defining a function, you can also specify the `Cancel` option which allows you to list one or more events that, when sent to Inngest, will cause the sleep to be terminated and function will be marked as "Canceled."
+
+Here is our schedule reminders function that leverages `Cancel`:
+
+```go {{ title: "main.go" }}
+package main
+
+import (
+ "context"
+ "github.com/inngest/inngestgo"
+ "github.com/inngest/inngestgo/step"
+)
+
+func main() {
+ f := inngestgo.CreateFunction(
+ inngestgo.FunctionOpts{
+ ID: "schedule-reminder",
+ Name: "Schedule reminder",
+ Cancel: []inngestgo.Cancel{
+ {
+ Event: "tasks/reminder.deleted",
+ IfExp: "event.data.id == async.data.id",
+ },
+ },
+ },
+ // Run on every tasks/reminder.created event.
+ inngestgo.EventTrigger("tasks/reminder.created", nil),
+ ScheduleReminder,
+ )
+}
+
+func ScheduleReminder(ctx context.Context, input inngestgo.Input[ScheduleReminderEvent]) (any, error) {
+ // ...
+}
+```
+
+Let's break down how this works:
+
+1. Whenever the function is triggered, a cancellation listener is created which waits for an `"tasks/reminder.deleted"` event to be received.
+2. The `if` statement tells Inngest that both the triggering event (`"tasks/reminder.created"`) and the cancellation event (`"tasks/reminder.deleted"`) have the same exact value for `data.reminderId` in each event payload. This makes sure that an event does not cancel a different reminder.
+
+For more information on writing events, read our guide [on writing expressions](/docs/guides/writing-expressions).
+
+Here is an example of these two events which will be matched on the `data.reminderId` field:
+
+
+
+
+```json
+{
+ "name": "tasks/reminder.created",
+ "data": {
+ "userId": "user_123",
+ "reminderId": "reminder_0987654321",
+ "reminderBody": "Pick up Jane from the airport"
+ }
+}
+```
+
+
+
+
+```json
+{
+ "name": "tasks/reminder.deleted",
+ "data": {
+ "userId": "user_123",
+ "reminderId": "reminder_0987654321",
+ }
+}
+```
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/docs/features/inngest-functions/cancellation/cancel-on-timeouts.mdx b/pages/docs/features/inngest-functions/cancellation/cancel-on-timeouts.mdx
index a0b9bdaa5..3ee0105f8 100644
--- a/pages/docs/features/inngest-functions/cancellation/cancel-on-timeouts.mdx
+++ b/pages/docs/features/inngest-functions/cancellation/cancel-on-timeouts.mdx
@@ -1,15 +1,15 @@
-import { CodeGroup, Row, Col, GuideSelector, GuideSection } from "src/shared/Docs/mdx";
+import { CodeGroup, Row, Col, GuideSelector, GuideSection, Callout, VersionBadge } from "src/shared/Docs/mdx";
export const description = 'Learn how to cancel long running functions with events.'
-# Cancel on timeouts
+# Cancel on timeouts
It's possible to force runs to cancel if they take too long to start, or if the runs execute for too long. The `timeouts` configuration property allows you to automatically cancel functions based off of two timeout properties:
- `timeouts.start`, which controls how long a function can stay unstarted in the queue
- `timeouts.finish`, which controls how long a function can execute once started
-In the following examples, we'll explore how to configure the timeout property and how this works.
+In the following examples, we'll explore how to configure the timeout property and how this works.
If your function exhausts all of its retries, it will be marked as "Failed." You can handle this circumstance by providing an [`onFailure`](/docs/reference/functions/handling-failures) handler when defining your function.
diff --git a/pages/docs/features/inngest-functions/error-retries/inngest-errors.mdx b/pages/docs/features/inngest-functions/error-retries/inngest-errors.mdx
index ec0a27073..03c1e0029 100644
--- a/pages/docs/features/inngest-functions/error-retries/inngest-errors.mdx
+++ b/pages/docs/features/inngest-functions/error-retries/inngest-errors.mdx
@@ -1,11 +1,24 @@
-import { Callout, CodeGroup, Properties, Property, Row, Col, VersionBadge } from "src/shared/Docs/mdx";
+import { Callout, CodeGroup, Properties, Property, Row, Col, VersionBadge, GuideSelector, GuideSection } from "src/shared/Docs/mdx";
+
+export const hidePageSidebar = true;
+
# Inngest Errors
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
@@ -30,8 +43,73 @@ export default inngest.createFunction(
}
);
```
+
+
+
+
+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
-## `NonRetriableError`
+
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.
@@ -75,7 +153,74 @@ new NonRetriableError(message: string, options?: { cause?: Error }): NonRetriabl
-## `RetryAfterError`
+
+
+
+
+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.
@@ -128,6 +273,99 @@ new RetryAfterError(
+
+
+
+
+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.
@@ -196,6 +434,37 @@ inngest.createFunction(
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.
@@ -218,6 +487,7 @@ inngest.createFunction(
);
```
+
## 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.
@@ -251,3 +521,6 @@ When calling functions that return Promises, await the Promise to ensure that th
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.
+
+
+
\ No newline at end of file
diff --git a/pages/docs/features/inngest-functions/error-retries/retries.mdx b/pages/docs/features/inngest-functions/error-retries/retries.mdx
index 9bf17cd80..79a051239 100644
--- a/pages/docs/features/inngest-functions/error-retries/retries.mdx
+++ b/pages/docs/features/inngest-functions/error-retries/retries.mdx
@@ -272,4 +272,21 @@ inngestgo.CreateFunction(
}
)
```
+```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)
+```
diff --git a/pages/docs/features/inngest-functions/steps-workflows/sleeps.mdx b/pages/docs/features/inngest-functions/steps-workflows/sleeps.mdx
index 937accc1c..f30f5593f 100644
--- a/pages/docs/features/inngest-functions/steps-workflows/sleeps.mdx
+++ b/pages/docs/features/inngest-functions/steps-workflows/sleeps.mdx
@@ -17,8 +17,8 @@ A Function paused by a sleeping Step doesn't affect your account capacity; i.e.
@@ -29,28 +29,17 @@ A Function paused by a sleeping Step doesn't affect your account capacity; i.e.
Use `step.sleep()` to pause the execution of your function for a specific amount of time.
-
- ```ts {{ title: "v3" }}
- 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
- }
- );
- ```
- ```ts {{ title: "v2" }}
- export default inngest.createFunction(
- { name: "Send delayed email" },
- { event: "app/user.signup" },
- async ({ event, step }) => {
- await step.sleep("2d");
- // Do something else
- }
- );
- ```
-
+
+```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)
@@ -104,8 +93,8 @@ Check out the [`step.sleep()` Python reference.](/docs/reference/python/steps/sl
Use `step.sleepUntil()` to pause the execution of your function until a specific date time.
-
-```ts {{ title: "v3" }}
+
+```ts
export default inngest.createFunction(
{ id: "send-scheduled-reminder" },
{ event: "app/reminder.scheduled" },
@@ -116,18 +105,7 @@ export default inngest.createFunction(
}
);
```
-```ts {{ title: "v2" }}
-export default inngest.createFunction(
- { name: "Send scheduled reminder" },
- { event: "app/reminder.scheduled" },
- async ({ event, step }) => {
- const date = new Date(event.data.remind_at)
- await step.sleepUntil(date);
- // Do something else
- }
-);
-```
-
+
Check out the [`step.sleepUntil()` TypeScript reference.](/docs/reference/functions/step-sleep-until)
diff --git a/pages/docs/features/inngest-functions/steps-workflows/step-ai-orchestration.mdx b/pages/docs/features/inngest-functions/steps-workflows/step-ai-orchestration.mdx
index 3330e3509..8a4db1e3f 100644
--- a/pages/docs/features/inngest-functions/steps-workflows/step-ai-orchestration.mdx
+++ b/pages/docs/features/inngest-functions/steps-workflows/step-ai-orchestration.mdx
@@ -1,6 +1,6 @@
-import { Callout, GuideSelector, GuideSection, CodeGroup } from "src/shared/Docs/mdx";
+import { Callout, GuideSelector, GuideSection, CodeGroup, VersionBadge } from "src/shared/Docs/mdx";
-# AI Inference
+# AI Inference
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:
@@ -38,7 +38,7 @@ Here's an example of a single model call:
```ts {{ title: "TypeScript" }}
-import { Agent, agenticOpenai as openai } from "@inngest/agent-kit";
+import { Agent, agenticOpenai as openai, createAgent } from "@inngest/agent-kit";
export default inngest.createFunction(
{ id: "summarize-contents" },
{ event: "app/ticket.created" },
diff --git a/pages/docs/features/inngest-functions/steps-workflows/wait-for-event.mdx b/pages/docs/features/inngest-functions/steps-workflows/wait-for-event.mdx
index 1663ec5b1..008c4da50 100644
--- a/pages/docs/features/inngest-functions/steps-workflows/wait-for-event.mdx
+++ b/pages/docs/features/inngest-functions/steps-workflows/wait-for-event.mdx
@@ -14,8 +14,8 @@ This is a useful pattern to react to specific user actions (for example, impleme
@@ -24,8 +24,8 @@ This is a useful pattern to react to specific user actions (for example, impleme
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 {{ title: "v3" }}
+
+```ts
export default inngest.createFunction(
{ id: "send-onboarding-nudge-email" },
{ event: "app/account.created" },
@@ -42,23 +42,7 @@ export default inngest.createFunction(
}
);
```
-```ts {{ title: "v2" }}
-export default inngest.createFunction(
- { name: "Send onboarding nudge email" },
- { event: "app/account.created" },
- async ({ event, step }) => {
- const onboardingCompleted = await step.waitForEvent(
- "app/onboarding.completed",
- {
- timeout: "3d",
- match: "data.userId",
- }
- );
- // Do something else
- }
-);
-```
-
+
Check out the [`step.waitForEvent()` TypeScript reference.](/docs/reference/functions/step-wait-for-event)
@@ -72,8 +56,8 @@ To add a simple time based delay to your code, use [`step.sleep()`](/docs/refere
Below is an example of an Inngest function that creates an Intercom or Customer.io-like drip email campaign, customized based on
-
-```ts {{ title: "v3" }}
+
+```ts
export default inngest.createFunction(
{ id: "onboarding-email-drip-campaign" },
{ event: "app/account.created" },
@@ -108,44 +92,6 @@ export default inngest.createFunction(
}
);
```
-```ts {{ title: "v2" }}
-export default inngest.createFunction(
- { name: "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
- const onboardingCompleted = await step.waitForEvent(
- "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`
@@ -153,8 +99,8 @@ For more complex functions, you may want to match the event payload against some
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 {{ title: "v3" }}
+
+```ts
export default inngest.createFunction(
{ id: "generate-blog-post-with-ai" },
{ event: "ai/post.generator.requested" },
@@ -208,63 +154,6 @@ export default inngest.createFunction(
}
);
```
-```ts {{ title: "v2" }}
-export default inngest.createFunction(
- { name: "Generate blog post with AI" },
- { event: "ai/post.generator.requested" },
- async ({ event, step }) => {
- // Generate a number of suggestions for topics with OpenAI
- const generatedTopics = await step.run("Generate topic ideas", async () => {
- const completion = 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
- const topicSelected = await step.waitForEvent("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 () => {
- const completion = 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]);
- });
- }
- }
-);
-
-```
-
-
-
diff --git a/pages/docs/features/middleware/create.mdx b/pages/docs/features/middleware/create.mdx
index fbd34ebcd..b4282acd2 100644
--- a/pages/docs/features/middleware/create.mdx
+++ b/pages/docs/features/middleware/create.mdx
@@ -24,8 +24,7 @@ Creating middleware means defining the lifecycles and subsequent hooks in those
**`new InngestMiddleware(options): InngestMiddleware`**
-
- ```ts {{ title: "v3" }}
+ ```ts
// Create a new middleware
const myMiddleware = new InngestMiddleware({
name: "My Middleware",
@@ -40,23 +39,7 @@ Creating middleware means defining the lifecycles and subsequent hooks in those
middleware: [myMiddleware],
});
```
- ```ts {{ title: "v2" }}
- // Create a new middleware
- const myMiddleware = new InngestMiddleware({
- name: "My Middleware",
- init: () => {
- return {};
- },
- });
-
- // Register it on the client
- const inngest = new Inngest({
- name: "My App",
- middleware: [myMiddleware],
- });
- ```
-
-
+
@@ -287,9 +270,7 @@ It's common for middleware to require additional customization or options from d
});
};
```
-
-
- ```ts {{ title: "v3" }}
+ ```ts
import { createMyMiddleware } from "./middleware/myMiddleware";
export const inngest = new Inngest({
@@ -297,15 +278,6 @@ It's common for middleware to require additional customization or options from d
middleware: [createMyMiddleware("app/user.created")],
});
```
- ```ts {{ title: "v2" }}
- import { createMyMiddleware } from "./middleware/myMiddleware";
-
- export const inngest = new Inngest({
- name: "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.
diff --git a/pages/docs/guides/background-jobs.mdx b/pages/docs/guides/background-jobs.mdx
index e67957052..a1dbcd364 100644
--- a/pages/docs/guides/background-jobs.mdx
+++ b/pages/docs/guides/background-jobs.mdx
@@ -1,4 +1,4 @@
-import { Callout, CodeGroup, Row, Col } from "src/shared/Docs/mdx";
+import { Callout, CodeGroup, Row, Col, GuideSelector, GuideSection } from "src/shared/Docs/mdx";
export const description = `Define background jobs in just a few lines of code.`
@@ -13,6 +13,13 @@ By running background tasks in Inngest:
## 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.
@@ -23,6 +30,7 @@ The example below shows a background job that uses an event (here called `app/us
### 1. Create a function that runs in the background
+
Let's walk through the code step by step:
@@ -74,12 +82,130 @@ await inngest.send({
});
```
+
+
+
+Let's walk through the code step by step:
+1. We [create a new Inngest function](https://pkg.go.dev/github.com/inngest/inngestgo#CreateFunction), which will run in the background any time the `app/user.created` event is sent to Inngest.
+2. We send an email reliably using the [`step.Run()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Run) method. Every [Inngest step](/docs/steps) is automatically retried upon failure.
+3. We pause the execution of the function for 4 hours using [`step.Sleep()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Sleep). The function will be resumed automatically, across server restarts or serverless functions. You don't have to worry about scale, memory leaks, connections, or restarts.
+4. We resume execution and perform other tasks.
+
+```go
+import (
+ "time"
+ "github.com/inngest/inngest-go"
+ "github.com/inngest/inngest-go/step"
+)
+
+inngestgo.CreateFunction(
+ inngest.FunctionOpts{
+ ID: "send-signup-email",
+ },
+ inngest.TriggerEvent("app/user.created"),
+ func(ctx *inngest.Context) error {
+ _, err := step.Run("send-the-user-a-signup-email", func(ctx *inngest.StepContext) (any, error) {
+ return nil, sesclient.SendEmail(&ses.SendEmailInput{
+ To: ctx.Event.Data["user_email"].(string),
+ Subject: "Welcome to Inngest!",
+ Message: "...",
+ })
+ })
+ if err != nil {
+ return err, nil
+ }
+
+ step.Sleep("wait-for-the-future", 4 * time.Hour)
+
+ _, err = step.Run("do-some-work-in-the-future", func(ctx *inngest.StepContext) error {
+ // Code here runs in the future automatically.
+ return nil, nil
+ })
+ return err, nil
+ },
+)
+```
+
+### 2. Trigger the function
+
+Your `sendSignUpEmail` function will be triggered whenever Inngest receives an event called `app/user.created`. is received. You send this event to Inngest like so:
+
+```go
+_, err := inngestgo.Send(context.Background(), inngestgo.Event{
+ Name: "app/user.created", // This matches the event used in `createFunction`
+ Data: map[string]interface{}{
+ "email": "test@example.com",
+ // any data you want to send
+ },
+})
+```
+
+
+
+
+Let's walk through the code step by step:
+1. We [create a new Inngest function](/docs/reference/python/functions/create), which will run in the background any time the `app/user.created` event is sent to Inngest.
+2. We send an email reliably using the [`step.run()`](/docs/reference/python/steps/run) method. Every [Inngest step](/docs/steps) is automatically retried upon failure.
+3. We pause the execution of the function until a specific date using [`step.sleep_until()`](/docs/reference/python/steps/sleep-until). The function will be resumed automatically, across server restarts or serverless functions. You don't have to worry about scale, memory leaks, connections, or restarts.
+4. We resume execution and perform other tasks.
+
+```python
+import inngest
+
+inngest_client = inngest.Inngest(
+ app_id="my-app",
+)
+
+@inngest_client.create_function(
+ fn_id="send-signup-email",
+ trigger=inngest.TriggerEvent(event="app/user.created")
+)
+async def send_signup_email(ctx: inngest.Context, step: inngest.Step):
+ async def send_email():
+ await sesclient.send_email(
+ to=ctx.event.data["user_email"],
+ subject="Welcome to Inngest!",
+ message="..."
+ )
+
+ await step.run("send-the-user-a-signup-email", send_email)
+
+ await step.sleep_until("wait-for-the-future", "2023-02-01T16:30:00")
+
+ async def future_work():
+ # Code here runs in the future automatically
+ pass
+
+ await step.run("do-some-work-in-the-future", future_work)
+```
+
+
+### 2. Trigger the function
+
+Your `sendSignUpEmail` function will be triggered whenever Inngest receives an event called `app/user.created`. is received. You send this event to Inngest like so:
+
+```python
+from src.inngest.client import inngest_client
+
+await inngest_client.send(
+ name="app/user.created", # This matches the event used in `create_function`
+ data={
+ "email": "test@example.com",
+ # any data you want to send
+ }
+)
+```
+
+
+
When you send an event to Inngest, it automatically finds any functions that are triggered by the event ID and automatically runs those functions in the background. The entire JSON object you pass in to `inngest.send()` will be available to your functions.
💡 Tip: You can create many functions which listen to the same event, and all of them will run in the background. Learn more about this pattern in our ["Fan out" guide](/docs/guides/fan-out-jobs).
+
+
## Further reading
More information on background jobs:
diff --git a/pages/docs/guides/batching.mdx b/pages/docs/guides/batching.mdx
index ce1b0e229..2eb0458d1 100644
--- a/pages/docs/guides/batching.mdx
+++ b/pages/docs/guides/batching.mdx
@@ -13,8 +13,8 @@ Batching allows a function to process multiple events in a single run. This is u
## How to configure batching
{/* NOTE - This should be moved to an example and we can make this more succinct */}
-
-```ts {{ title: "record-api-calls.ts"}}
+
+```ts {{ title: "TypeScript"}}
inngest.createFunction(
{
id: "record-api-calls",
@@ -43,6 +43,74 @@ inngest.createFunction(
}
);
```
+
+```go {{ title: "Go" }}
+inngestgo.CreateFunction(
+ &inngestgo.FunctionOpts{
+ ID: "record-api-calls",
+ BatchEvents: &inngest.EventBatchConfig{
+ MaxSize: 100,
+ Timeout: "5s",
+ Key: "event.data.user_id", // Optional: batch events by user ID
+ },
+ },
+ inngestgo.EventTrigger("log/api.call"),
+ func(ctx context.Context, events []*inngestgo.Event, step inngestgo.StepFunction) (any, error) {
+ // NOTE: Use the events argument, which is an array of event payloads
+ attrs := make([]interface{}, len(events))
+ for i, evt := range events {
+ attrs[i] = map[string]interface{}{
+ "user_id": evt.Data["user_id"],
+ "endpoint": evt.Data["endpoint"],
+ "timestamp": toDateTime(evt.Ts),
+ }
+ }
+
+ var result []interface{}
+ _, err := step.Run(ctx, "record-data-to-db", func(ctx context.Context) (interface{}, error) {
+ return nil, db.BulkWrite(attrs)
+ })
+ if err != nil {
+ return err, nil
+ }
+
+ return nil, map[string]interface{}{
+ "success": true,
+ "recorded": len(result),
+ }
+ },
+)
+```
+
+```py {{ title: "Python" }}
+@inngest_client.create_function(
+ fn_id="record-api-calls",
+ trigger=inngest.TriggerEvent(event="log/api.call"),
+ batch_events=inngest.Batch(
+ max_size=100,
+ timeout=datetime.timedelta(seconds=5),
+ key="event.data.user_id" # Optional: batch events by user ID
+ ),
+)
+async def record_api_calls(ctx: inngest.Context, step: inngest.Step):
+ # NOTE: Use the events from ctx, which is an array of event payloads
+ attrs = [
+ {
+ "user_id": evt.data.user_id,
+ "endpoint": evt.data.endpoint,
+ "timestamp": to_datetime(evt.ts)
+ }
+ for evt in ctx.events
+ ]
+
+ async def record_data():
+ return await db.bulk_write(attrs)
+
+ result = await step.run("record-data-to-db", record_data)
+
+ return {"success": True, "recorded": len(result)}
+```
+
### Configuration reference
diff --git a/pages/docs/guides/concurrency.mdx b/pages/docs/guides/concurrency.mdx
index bdbcc8fa3..d69e9dd08 100644
--- a/pages/docs/guides/concurrency.mdx
+++ b/pages/docs/guides/concurrency.mdx
@@ -16,7 +16,9 @@ Use cases include:
Inngest lets you provide fine-grained concurrency across all functions in a simple, configurable manner. You can control each function's concurrency limits within your function definition. Here are two examples:
-```js
+
+
+```ts {{ title: "TypeScript" }}
// Example 1: a simple concurrency definition limiting this function to 10 steps at once.
inngest.createFunction(
{
@@ -61,6 +63,107 @@ inngest.createFunction(
```
+```go {{ title: "Go" }}
+// Example 1: a simple concurrency definition limiting this function to 10 steps at once.
+inngest.CreateFunction(
+ &inngestgo.FunctionOpts{
+ Name: "another-function",
+ Concurrency: []inngest.Concurrency{
+ {
+ Limit: 10,
+ }
+ },
+ },
+ inngestgo.EventTrigger("ai/summary.requested", nil),
+ func(ctx context.Context, input inngestgo.Input) (any, error) {
+ // Function implementation here
+ return nil, nil
+ },
+)
+
+// Example 2: A complete, complex example with two virtual concurrency queues.
+inngestgo.CreateFunction(
+ &inngestgo.FunctionOpts{
+ Name: "unique-function-id",
+ Concurrency: []inngest.Concurrency{
+ {
+ // Use an account-level concurrency limit for this function, using the
+ // "openai" key as a virtual queue. Any other function which
+ // runs using the same "openai" key counts towards this limit.
+ Scope: "account",
+ Key: `"openai"`,
+ // 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,
+ },
+ },
+ },
+ inngestgo.EventTrigger("ai/summary.requested", nil),
+ func(ctx context.Context, input inngestgo.Input) (any, error) {
+ // Function implementation here
+ return nil, nil
+ },
+)
+
+```
+
+
+```py {{ title: "Python" }}
+# Example 1: a simple concurrency definition limiting this function to 10 steps at once.
+@inngest.create_function(
+ fn_id="another-function",
+ concurrency=[
+ inngest.Concurrency(
+ limit=10,
+ )
+ ]
+)
+async def first_function(event, step):
+ # Function implementation here
+ pass
+
+# Example 2: A complete, complex example with two virtual concurrency queues.
+@inngest.create_function(
+ fn_id="unique-function-id",
+ concurrency=[
+ inngest.Concurrency(
+ # Use an account-level concurrency limit for this function, using the
+ # "openai" key as a virtual queue. Any other function which
+ # runs using the same "openai" key counts towards this limit.
+ scope="account",
+ key='"openai"',
+ # If there are 10 functions running with the "openai" key, this function's
+ # runs will wait for capacity before executing.
+ limit=10,
+ ),
+ inngest.Concurrency(
+ # Create another virtual concurrency queue for this function only. This
+ # limits all accounts to a single execution for this function, based off
+ # of the `event.data.account_id` field.
+ # "fn" is the default scope, so we could omit this field.
+ scope="fn",
+ key="event.data.account_id",
+ limit=1,
+ ),
+ ],
+)
+async def handle_ai_summary(event, step):
+ # Function implementation here
+ pass
+```
+
+
+
+
In the first example, the function is constrained to `10` executing steps at once.
In the second example, we define **two concurrency constraints that create two virtual queues to manage capacity**. Runs will be limited if they hit either of the virtual queue's limits. For example:
@@ -94,7 +197,10 @@ To control concurrency on individual steps, extract the step into a new function
You may write two functions that define different levels for an 'account' scoped concurrency limit. For example, function A may limit the "ai" capacity to 5, while function B limits the "ai" capacity to 50:
-```js
+
+
+
+```ts {{ title: "TypeScript" }}
inngest.createFunction(
{
id: "func-a",
@@ -124,7 +230,77 @@ inngest.createFunction(
);
```
+```go {{ title: "Go" }}
+inngestgo.CreateFunction(
+ &inngestgo.FunctionConfig{
+ Name: "func-a",
+ Concurrency: []inngest.Concurrency{
+ {
+ Scope: "account",
+ Key: `"openai"`,
+ Limit: 5,
+ }
+ },
+ },
+ inngestgo.EventTrigger("ai/summary.requested"),
+ func(ctx context.Context, event *inngestgo.Event, step inngestgo.StepFunction) (any, error) {
+ return nil, nil
+ },
+)
+
+inngestgo.CreateFunction(
+ &inngestgo.FunctionConfig{
+ Name: "func-b",
+ Concurrency: []inngest.Concurrency{
+ {
+ Scope: "account",
+ Key: `"openai"`,
+ Limit: 50,
+ }
+ },
+ },
+ inngestgo.EventTrigger("ai/summary.requested"),
+ func(ctx context.Context, event *inngestgo.Event, step inngestgo.StepFunction) (any, error) {
+ return nil, nil
+ },
+)
+
+```
+
+
+```py {{ title: "Python" }}
+@inngest_client.create_function(
+ fn_id="func-a",
+ trigger=inngest.TriggerEvent(event="ai/summary.requested"),
+ concurrency=[
+ inngest.Concurrency(
+ scope="account",
+ key='"openai"',
+ limit=5
+ )
+ ]
+)
+async def func_a(ctx: inngest.Context, step: inngest.Step):
+ pass
+
+@inngest_client.create_function(
+ fn_id="func-b",
+ trigger=inngest.TriggerEvent(event="ai/summary.requested"),
+ concurrency=[
+ inngest.Concurrency(
+ scope="account",
+ key='"openai"',
+ limit=50
+ )
+ ]
+)
+async def func_b(ctx: inngest.Context, step: inngest.Step):
+ pass
+```
+
+
+
This works in Inngest and is *not* a conflict. Instead, function A is limited any time there are 5 or more functions running in the 'openai' queue. Function B, however, is limited when there are 50 or more items in the queue. This means that function B has more capacity than function A, though both are limited and compete on the same virtual queue.
Because functions are FIFO, function runs are more likely to be worked on the older their jobs get (as the backlog grows). If function A's jobs stay in the backlog longer than function B's jobs, it's likely that their jobs will be worked on as soon as capacity is free. That said, function B will almost always have capacity before function A and may block function A's work.
@@ -133,7 +309,7 @@ Because functions are FIFO, function runs are more likely to be worked on the ol
## Limitations
-- Concurrency limits the number of steps executing at a single time. It does not _yet_ perform rate limiting over a given period of time. This is scheduled for a Q2 '24 release.
+- Concurrency limits the number of steps executing at a single time. It does not _yet_ perform rate limiting over a given period of time.
- Functions can specify up to 2 concurrency constraints at once
- The maximum concurrency limit is defined by your account's plan
- Ordering amongst the same function is guaranteed (with the exception of retries)
@@ -173,7 +349,9 @@ Because functions are FIFO, function runs are more likely to be worked on the ol
Here, we use the Resend SDK to send an email. Resend's rate limit is 10 requests per second so we set a lower concurrency as our function is simple and may execute multiple times per second. Here we use a limit of `4` to keep the throughput a bit slower than necessary:
-```ts
+
+
+```ts {{ title: "TypeScript" }}
export const send = inngest.createFunction(
{
name: "Email: Pending invoice",
@@ -198,14 +376,73 @@ export const send = inngest.createFunction(
);
```
+
+```go {{ title: "Go" }}
+inngest.CreateFunction(
+ &inngestgo.FunctionOpts{
+ Name: "Email: Pending invoice",
+ ID: "email-pending-invoice",
+ Concurrency: []inngest.Concurrency{
+ {
+ Limit: 4, // Resend's rate limit is 10 req/s
+ },
+ },
+ },
+ inngestgo.EventTrigger("billing/invoice.pending", nil),
+ func(ctx context.Context, input inngestgo.Input) (any, error) {
+ _, err := input.Step.Run(ctx, "send-email", func(ctx context.Context) (any, error) {
+ return resend.Emails.Send(&resend.SendEmailRequest{
+ From: "hello@myco.com",
+ To: input.Event.User.Email,
+ Subject: fmt.Sprintf("Invoice pending for %s", input.Event.Data.InvoicePeriod),
+ Text: "Dear user, ...",
+ })
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return map[string]string{"message": "success"}, nil
+ },
+)
+```
+
+
+
+```python {{ title: "Python" }}
+@inngest.create_function(
+ fn_id="email-pending-invoice",
+ name="Email: Pending invoice",
+ concurrency=[
+ inngest.Concurrency(
+ limit=4, # Resend's rate limit is 10 req/s
+ )
+ ]
+)
+async def send(event, step):
+ async with step.run("send-email") as _:
+ params: resend.Emails.SendParams = {
+ "from": "Acme ",
+ "to": ["delivered@resend.dev"],
+ "subject": "hello world",
+ "html": "it works!",
+ }
+ await resend.emails.send(params)
+
+ return {"message": "success"}
+```
+
+
+
### Restricting parallel import jobs for a customer id
In this hypothetical system, customers can upload `.csv` files which each need to be processed and imported. We want to limit each customer to only one import job at a time so no two jobs are writing to a customer's data at a given time. We do this by setting a `limit: 1` and a concurrency `key` to the `customerId` which is included in every single event payload.
Inngest ensures that the concurrency (`1`) applies to each unique value for `event.data.customerId`. This allows different customers to have functions running at the same exact time, but no given customer can have two functions running at once!
+
-```ts
+```ts {{ title: "TypeScript" }}
export const send = inngest.createFunction(
{
name: "Process customer csv import",
@@ -227,6 +464,60 @@ export const send = inngest.createFunction(
);
```
+```go {{ title: "Go" }}
+inngestgo.CreateFunction(
+ &inngestgo.FunctionConfig{
+ Name: "Process customer csv import",
+ ID: "process-customer-csv-import",
+ Concurrency: []inngest.Concurrency{
+ {
+ Limit: 1,
+ Key: `event.data.customerId`, // You can use any piece of data from the event payload
+ },
+ },
+ },
+ inngestgo.EventTrigger("csv/file.uploaded"),
+ func(ctx context.Context, event *inngestgo.Event, step inngestgo.StepFunction) (any, error) {
+ _, err := step.Run(ctx, "process-file", func(ctx context.Context) (any, error) {
+ file, err := bucket.Fetch(event.Data.FileURI)
+ if err != nil {
+ return nil, err
+ }
+ // ...
+ return nil, nil
+ })
+ if err != nil {
+ return err, nil
+ }
+
+ return nil, nil
+ },
+)
+```
+
+```py {{ title: "Python" }}
+@inngest_client.create_function(
+ fn_id="process-customer-csv-import",
+ name="Process customer csv import",
+ trigger=inngest.TriggerEvent(event="csv/file.uploaded"),
+ concurrency=[
+ inngest.Concurrency(
+ limit=1,
+ key="event.data.customerId" # You can use any piece of data from the event payload
+ )
+ ]
+)
+async def process_csv_import(ctx: inngest.Context, step: inngest.Step):
+ async def process_file():
+ file = await bucket.fetch(ctx.event.data.file_uri)
+ # ...
+
+ await step.run("process-file", process_file)
+ return {"message": "success"}
+```
+
+
+
## Tips
* Configure [start timeouts](/docs/features/inngest-functions/cancellation/cancel-on-timeouts) to prevent large backlogs with concurrency
diff --git a/pages/docs/guides/debounce.mdx b/pages/docs/guides/debounce.mdx
index 207abc9d7..36a35aaaa 100644
--- a/pages/docs/guides/debounce.mdx
+++ b/pages/docs/guides/debounce.mdx
@@ -12,8 +12,8 @@ Debounce delays function execution until a series of events are no longer receiv
## How to configure debounce
-
-```ts
+
+```ts {{ title: "TypeScript" }}
export default inngest.createFunction(
{
id: "handle-webhook",
@@ -32,6 +32,43 @@ export default inngest.createFunction(
}
);
```
+```go {{ title: "Go" }}
+inngestgo.CreateFunction(
+ &inngestgo.FunctionOpts{
+ ID: "handle-webhook",
+ Debounce: &inngestgo.Debounce{
+ Key: "event.data.account_id",
+ Period: "5m",
+ Timeout: "10m",
+ },
+ },
+ inngestgo.EventTrigger("intercom/company.updated", nil),
+ func(ctx context.Context, input inngestgo.Input) (any, error) {
+ // This function will only be scheduled 5 minutes after events are no longer received with the same
+ // `event.data.account_id` field.
+ //
+ // `event` will be the last event in the series received.
+ return nil, nil
+ },
+)
+```
+```py {{ title: "Python" }}
+@inngest.create_function(
+ fn_id="handle-webhook",
+ debounce=inngest.Debounce(
+ key="event.data.account_id",
+ period=datetime.timedelta(minutes=5),
+ timeout=datetime.timedelta(minutes=10)
+ ),
+ trigger=inngest.Trigger(event="intercom/company.updated")
+)
+async def handle_webhook(ctx: inngest.Context):
+ // This function will only be scheduled 5 minutes after events are no longer received with the same
+ // `event.data.account_id` field.
+ //
+ // `event` will be the last event in the series received.
+ pass
+```
### Configuration reference
diff --git a/pages/docs/guides/delayed-functions.mdx b/pages/docs/guides/delayed-functions.mdx
index 63dc0244c..cc71e2e89 100644
--- a/pages/docs/guides/delayed-functions.mdx
+++ b/pages/docs/guides/delayed-functions.mdx
@@ -1,4 +1,4 @@
-import { CodeGroup } from "src/shared/Docs/mdx";
+import { GuideSelector, GuideSection } from "src/shared/Docs/mdx";
# Delayed Functions
@@ -17,9 +17,18 @@ You can easily enqueue jobs in the future with Inngest. Inngest offers two ways
## Delaying jobs
-You can delay jobs using the [`step.sleep()`](/docs/reference/functions/step-sleep) utility:
+
+
+
+
+
+You can delay jobs using the [`step.sleep()`](/docs/reference/functions/step-sleep) method:
-
```ts
import { Inngest } from "inngest";
@@ -36,16 +45,16 @@ export const fn = inngest.createFunction(
}
);
```
-
+
For more information on `step.sleep()` read [the reference](/docs/reference/functions/step-sleep).
## Running at specific times
-You can run jobs at a specific time using the [`step.sleepUntil()`](/docs/reference/functions/step-sleep-until) utility:
+You can run jobs at a specific time using the [`step.sleepUntil()`](/docs/reference/functions/step-sleep-until) method:
+
-
```ts
import { Inngest } from "inngest";
@@ -67,10 +76,122 @@ export const fn = inngest.createFunction(
}
);
```
-
+
+
For more information on `step.sleepUntil()` [read the reference](/docs/reference/functions/step-sleep-until).
+
+
+
+
+You can delay jobs using the [`step.Sleep()`](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Sleep) method:
+
+```go
+import (
+ "time"
+ "github.com/inngest/inngest-go"
+ "github.com/inngest/inngest-go/step"
+)
+
+inngestgo.CreateFunction(
+ inngest.FunctionOpts{
+ ID: "send-signup-email",
+ },
+ inngest.TriggerEvent("app/user.created"),
+ func(ctx *inngest.Context) error {
+ // business logic
+
+ step.Sleep("wait-for-the-future", 4 * time.Hour)
+
+ _, err = step.Run("do-some-work-in-the-future", func(ctx *inngest.StepContext) (any, error) {
+ // Code here runs in the future automatically.
+ return nil, nil
+ })
+ return err, nil
+ },
+)
+```
+
+
+For more information on `step.sleep()` read [the reference](https://pkg.go.dev/github.com/inngest/inngestgo@v0.7.4/step#Sleep).
+
+
+
+
+
+
+You can delay jobs using the [`step.sleep()`](http://localhost:3001/docs/reference/python/steps/sleep) method:
+
+```python
+import inngest
+from src.inngest.client import inngest_client
+from datetime import timedelta
+
+@inngest_client.create_function(
+ fn_id="send-signup-email",
+ trigger=inngest.TriggerEvent(event="app/user.created")
+)
+async def send_signup_email(ctx: inngest.Context, step: inngest.Step):
+
+ await step.sleep("wait-for-the-future", timedelta(hours=4))
+
+ async def future_work():
+ # Code here runs in the future automatically
+ pass
+
+ await step.run("do-some-work-in-the-future", future_work)
+```
+
+
+For more information on `step.sleep()` read [the reference](/docs/reference/functions/step-sleep).
+
+
+## Running at specific times
+
+You can run jobs at a specific time using the [`step.sleep_until()`](/docs/reference/python/steps/sleep-until) method:
+
+
+```python
+import inngest
+from src.inngest.client import inngest_client
+
+inngest_client = inngest.Inngest(
+ app_id="my-app",
+)
+
+@inngest_client.create_function(
+ fn_id="send-signup-email",
+ trigger=inngest.TriggerEvent(event="app/user.created")
+)
+async def send_signup_email(ctx: inngest.Context, step: inngest.Step):
+ async def send_email():
+ await sesclient.send_email(
+ to=ctx.event.data["user_email"],
+ subject="Welcome to Inngest!",
+ message="..."
+ )
+
+ await step.run("send-the-user-a-signup-email", send_email)
+
+ await step.sleep_until("wait-for-the-future", "2023-02-01T16:30:00")
+
+ async def future_work():
+ # Code here runs in the future automatically
+ pass
+
+ await step.run("do-some-work-in-the-future", future_work)
+```
+
+
+
+For more information on `step.sleep_until()` [read the reference](/docs/reference/python/steps/sleep-until).
+
+
+
+
+
+
## How it works
{/* TODO - Revisit this section after we write a How Inngest Works explainer */}
diff --git a/pages/docs/guides/fan-out-jobs.mdx b/pages/docs/guides/fan-out-jobs.mdx
index ef0ba81b6..f40dcbd70 100644
--- a/pages/docs/guides/fan-out-jobs.mdx
+++ b/pages/docs/guides/fan-out-jobs.mdx
@@ -1,4 +1,4 @@
-import { ImageTheme } from "src/shared/Docs/mdx";
+import { ImageTheme, GuideSelector, GuideSection, CodeGroup } from "src/shared/Docs/mdx";
export const description = 'How to use the fan-out pattern with Inngest to trigger multiple functions from a single event.'
@@ -28,6 +28,15 @@ The fan-out pattern is also useful in distributed systems where a single event i
## How to fan-out to multiple functions
+
+
+
+
Since Inngest is powered by events, implementing fan-out is as straightforward as defining multiple functions that use the same event trigger. Let's take the above example of user signup and implement it in Inngest.
First, set up a `/signup` route handler to send an event to Inngest when a user signs up:
@@ -98,6 +107,245 @@ Other benefits of fan-out include:
* **New features or refactors**: As each function is independent, you can add new functions or refactor existing ones without having to edit unrelated code.
* **Trigger functions in different codebases**: If you have multiple codebases, even using different programming languages (for example [Python](/docs/reference/python) or [Go](https://pkg.go.dev/github.com/inngest/inngestgo)), you can trigger functions in both codebases from a single event.
+
+
+
+Since Inngest is powered by events, implementing fan-out is as straightforward as defining multiple functions that use the same event trigger. Let's take the above example of user signup and implement it in Inngest.
+
+First, set up a `/signup` route handler to send an event to Inngest when a user signs up:
+
+```go {{ filename: "main.go" }}
+func main() {
+ // Initialize your HTTP server
+ mux := http.NewServeMux()
+
+ // Handle signup route
+ mux.HandleFunc("/signup", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // Parse request body - in a real app you'd validate the input
+ var user struct {
+ Email string `json:"email"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ // Send event to Inngest
+ _, err := inngestgo.Send(r.Context(), inngestgo.Event{
+ Name: "app/user.signup",
+ Data: map[string]interface{}{
+ "user": map[string]interface{}{
+ "email": user.email,
+ },
+ },
+ })
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ })
+
+ // Start the server
+ log.Fatal(http.ListenAndServe(":8080", mux))
+}
+
+```
+
+Now, with this event, any function using `"app/user.signup"` as its event trigger will be automatically invoked.
+
+Next, define two functions: `sendWelcomeEmail` and `startStripeTrial`. As you can see below, both functions use the same event trigger, but perform different work.
+
+```go {{ filename: "inngest/functions.go" }}
+import (
+ "github.com/inngest/inngest-go"
+ "github.com/inngest/inngest-go/step"
+)
+
+func sendWelcomeEmail() *inngest.Function {
+ return inngestgo.CreateFunction(
+ inngest.FunctionOpts{
+ ID: "send-welcome-email",
+ },
+ inngest.TriggerEvent("app/user.signup"),
+ func(ctx *inngest.Context) error {
+ _, err := step.Run("send-email", func(ctx *inngest.StepContext) (any, error) {
+ return sendEmail(&SendEmailInput{
+ Email: ctx.Event.Data["user"].(map[string]interface{})["email"].(string),
+ Template: "welcome",
+ })
+ })
+ return err, nil
+ },
+ )
+}
+
+func startStripeTrial() *inngest.Function {
+ return inngestgo.CreateFunction(
+ inngest.FunctionOpts{
+ ID: "start-stripe-trial",
+ },
+ inngest.TriggerEvent("app/user.signup"),
+ func(ctx *inngest.Context) (any, error) {
+ customer, err := step.Run("create-customer", func(ctx *inngest.StepContext) (any, error) {
+ return nil, stripe.Customers.Create(&stripe.CustomerParams{
+ Email: ctx.Event.Data["user"].(map[string]interface{})["email"].(string),
+ })
+ })
+ if err != nil {
+ return err, nil
+ }
+
+ _, err = step.Run("create-subscription", func(ctx *inngest.StepContext) (any, error) {
+ return nil, stripe.Subscriptions.Create(&stripe.SubscriptionParams{
+ Customer: customer.ID,
+ Items: []*stripe.SubscriptionItemsParams{{Price: "price_1MowQULkdIwHu7ixraBm864M"}},
+ TrialPeriodDays: 14,
+ })
+ })
+ return err, nil
+ },
+ )
+}
+```
+
+You've now successfully implemented fan-out in our application. Each function will run independently and in parallel. If one function fails, the others will not be disrupted.
+
+Other benefits of fan-out include:
+
+* **Bulk Replay**: If a third-party API goes down for a period of time (for example, your email provider), you can use [Replay](/docs/platform/replay) to selectively re-run all functions that failed, without having to re-run all sign-up flow functions.
+* **Testing**: Each function can be tested in isolation, without having to run the entire sign-up flow.
+* **New features or refactors**: As each function is independent, you can add new functions or refactor existing ones without having to edit unrelated code.
+* **Trigger functions in different codebases**: If you have multiple codebases, even using different programming languages (for example [TypeScript](/docs/reference/typescript) or [Python](/docs/reference/python)), you can trigger functions in both codebases from a single event.
+
+
+
+
+Since Inngest is powered by events, implementing fan-out is as straightforward as defining multiple functions that use the same event trigger. Let's take the above example of user signup and implement it in Inngest.
+
+First, set up a `/signup` route handler to send an event to Inngest when a user signs up:
+
+
+
+```ts {{ title: "Flask route" }}
+from flask import Flask, request, redirect
+from src.inngest.client import inngest_client
+
+app = Flask(__name__)
+
+@app.route('/signup', methods=['POST'])
+async def signup():
+ // NOTE - this code is simplified for the example:
+ data = await request.get_json()
+ email = data['email']
+ password = data['password']
+
+ user = await create_user(email=email, password=password)
+ await create_session(user.id)
+
+ // Send an event to Inngest
+ await inngest_client.send(
+ name="app/user.signup",
+ data={
+ "user": {
+ "id": user.id,
+ "email": user.email
+ }
+ }
+ )
+
+ return redirect('https://myapp.com/dashboard')
+```
+
+```ts {{ title: "FastAPI route" }}
+from fastapi import FastAPI, Request, Response
+from fastapi.responses import RedirectResponse
+from src.inngest.client import inngest_client
+
+app = FastAPI()
+
+@app.post("/signup")
+async def signup(request: Request):
+ # NOTE - this code is simplified for the example:
+ data = await request.json()
+ email = data['email']
+ password = data['password']
+
+ user = await create_user(email=email, password=password)
+ await create_session(user.id)
+
+ # Send an event to Inngest
+ await inngest_client.send(
+ name="app/user.signup",
+ data={
+ "user": {
+ "id": user.id,
+ "email": user.email
+ }
+ }
+ )
+
+ return RedirectResponse(url="https://myapp.com/dashboard")
+```
+
+
+
+Now, with this event, any function using `"app/user.signup"` as its event trigger will be automatically invoked.
+
+Next, define two functions: `sendWelcomeEmail` and `startStripeTrial`. As you can see below, both functions use the same event trigger, but perform different work.
+
+```py {{ filename: "inngest/functions.py" }}
+@inngest_client.create_function(
+ fn_id="send-welcome-email",
+ trigger=inngest.TriggerEvent(event="app/user.signup"),
+)
+async def send_welcome_email(
+ ctx: inngest.Context,
+ step: inngest.Step,
+) -> None:
+ await step.run("send-email", lambda: send_email(
+ email=ctx.event.data["user"]["email"],
+ template="welcome"
+ ))
+
+@inngest_client.create_function(
+ fn_id="start-stripe-trial",
+ trigger=inngest.TriggerEvent(event="app/user.signup"),
+)
+async def start_stripe_trial(
+ ctx: inngest.Context,
+ step: inngest.Step,
+) -> None:
+ customer = await step.run("create-customer", lambda: stripe.Customer.create(
+ email=ctx.event.data["user"]["email"]
+ ))
+
+ await step.run("create-subscription", lambda: stripe.Subscription.create(
+ customer=customer.id,
+ items=[{"price": "price_1MowQULkdIwHu7ixraBm864M"}],
+ trial_period_days=14
+ ))
+```
+
+You've now successfully implemented fan-out in our application. Each function will run independently and in parallel. If one function fails, the others will not be disrupted.
+
+Other benefits of fan-out include:
+
+* **Bulk Replay**: If a third-party API goes down for a period of time (for example, your email provider), you can use [Replay](/docs/platform/replay) to selectively re-run all functions that failed, without having to re-run all sign-up flow functions.
+* **Testing**: Each function can be tested in isolation, without having to run the entire sign-up flow.
+* **New features or refactors**: As each function is independent, you can add new functions or refactor existing ones without having to edit unrelated code.
+* **Trigger functions in different codebases**: If you have multiple codebases, even using different programming languages (for example [TypeScript](/docs/reference/typescript) or [Go](https://pkg.go.dev/github.com/inngest/inngestgo)), you can trigger functions in both codebases from a single event.
+
+
+
+
+
## Further reading
* [Sending events](/docs/events)
diff --git a/pages/docs/guides/handling-idempotency.mdx b/pages/docs/guides/handling-idempotency.mdx
index 795089686..078a0ca75 100644
--- a/pages/docs/guides/handling-idempotency.mdx
+++ b/pages/docs/guides/handling-idempotency.mdx
@@ -1,4 +1,4 @@
-import { Callout, Row, Col } from "src/shared/Docs/mdx";
+import { Callout, Row, Col, CodeGroup } from "src/shared/Docs/mdx";
# Handling idempotency
@@ -29,6 +29,9 @@ Each event that is received by Inngest will trigger any functions with that matc
To prevent an event from being handled twice, you can set a unique event `id` when [sending the event](/docs/reference/events/send#inngest-send-event-payload-event-payload-promise). This `id` acts as an idempotency key **over a 24 hour period** and Inngest will check to see if that event has already been received before triggering another function.
+
+
+
```ts
const cartId = 'CGo5Q5ekAxilN92d27asEoDO';
await inngest.send({
@@ -41,6 +44,31 @@ await inngest.send({
})
```
+
+```go {{ title: "Go" }}
+cart_id := "CGo5Q5ekAxilN92d27asEoDO"
+await inngest.Send(context.Background(), inngest.Event{
+ ID: fmt.Sprintf("checkout-completed-%s", cart_id), // <-- This is the idempotency key
+ Name: "cart/checkout.completed",
+ Data: map[string]any{"email": "taylor@example.com", "cart_id": cart_id},
+})
+```
+
+
+```python {{ title: "Python" }}
+cart_id = 'CGo5Q5ekAxilN92d27asEoDO'
+await inngest.send({
+ id: f'checkout-completed-{cart_id}', // <-- This is the idempotency key
+ name: 'cart/checkout.completed',
+ data: {
+ email: 'taylor@example.com',
+ cart_id: cart_id
+ }
+})
+```
+
+
+
| Event ID | Timestamp | Function |
| -------- | --------- | -------- |
| `checkout-completed-CGo5Q5ekAxilN92d27asEoDO` | 08:00:00.000 | ✅ Functions are triggered |
diff --git a/pages/docs/guides/invoking-functions-directly.mdx b/pages/docs/guides/invoking-functions-directly.mdx
index 7cf476f26..f80a761b8 100644
--- a/pages/docs/guides/invoking-functions-directly.mdx
+++ b/pages/docs/guides/invoking-functions-directly.mdx
@@ -1,4 +1,4 @@
-import { Callout } from "src/shared/Docs/mdx";
+import { Callout, GuideSelector, GuideSection } from "src/shared/Docs/mdx";
# Invoking functions directly
@@ -8,6 +8,19 @@ Inngest's `step.invoke()` function provides a powerful tool for calling function
- 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?
@@ -17,8 +30,6 @@ distinct from the invoker's, you can provide a tailored configuration for each f
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.
-## Invoking another function
-
```ts
// Some function we'll call
const computeSquare = inngest.createFunction(
@@ -105,6 +116,118 @@ const mainFunction = inngest.createFunction(
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.
diff --git a/pages/docs/guides/multi-step-functions.mdx b/pages/docs/guides/multi-step-functions.mdx
index a73511327..c5fc12407 100644
--- a/pages/docs/guides/multi-step-functions.mdx
+++ b/pages/docs/guides/multi-step-functions.mdx
@@ -1,5 +1,7 @@
-import { CodeGroup, Callout } from "shared/Docs/mdx";
+import { CodeGroup, Callout, GuideSelector, GuideSection } from "src/shared/Docs/mdx";
+
export const description = `Build reliable workflows with event coordination and conditional execution using Inngest's multi-step functions.`
+export const hidePageSidebar = true;
# Multi-Step Functions
@@ -17,8 +19,20 @@ Creating functions that utilize multiple steps enable you to:
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:
@@ -131,8 +145,312 @@ That's it! We've now written a multi-step function that will send a welcome emai
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.
@@ -248,6 +566,105 @@ If code within a loop is not encapsulated within a step, it will re-run multiple
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
diff --git a/pages/docs/guides/priority.mdx b/pages/docs/guides/priority.mdx
index ad5d0be9c..50c9694dd 100644
--- a/pages/docs/guides/priority.mdx
+++ b/pages/docs/guides/priority.mdx
@@ -30,6 +30,35 @@ export default inngest.createFunction(
}
);
```
+
+```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):
+ # This function will be prioritized based on the account type
+```
+
### Configuration reference
diff --git a/pages/docs/guides/rate-limiting.mdx b/pages/docs/guides/rate-limiting.mdx
index a93bea647..18e6dcabb 100644
--- a/pages/docs/guides/rate-limiting.mdx
+++ b/pages/docs/guides/rate-limiting.mdx
@@ -12,8 +12,8 @@ Rate limiting is a _hard limit_ on how many function runs can start within a tim
## How to configure rate limiting
-
-```ts
+
+```ts {{ title: "TypeScript" }}
export default inngest.createFunction(
{
id: "synchronize-data",
@@ -30,6 +30,39 @@ export default inngest.createFunction(
}
);
```
+
+```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):
+ # This function will be rate limited to 1 run per 4 hours for a given event payload with matching company_id
+```
+
### Configuration reference
diff --git a/pages/docs/guides/scheduled-functions.mdx b/pages/docs/guides/scheduled-functions.mdx
index 8d52f0b08..99a1432bd 100644
--- a/pages/docs/guides/scheduled-functions.mdx
+++ b/pages/docs/guides/scheduled-functions.mdx
@@ -1,12 +1,21 @@
-import { CodeGroup, Callout } from "src/shared/Docs/mdx";
+import { CodeGroup, Callout, GuideSection, GuideSelector } from "src/shared/Docs/mdx";
# Crons (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
import { Inngest } from "inngest";
@@ -70,10 +79,170 @@ export const sendWeeklyDigest = inngest.createFunction(
}
);
```
-
+
+
+
+
+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"},
+ inngestgo.CronTrigger("TZ=Europe/Paris 0 12 * * 5", nil),
+ func(ctx context.Context, input inngestgo.Input) (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.
+👉 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.
diff --git a/pages/docs/guides/sending-events-from-functions.mdx b/pages/docs/guides/sending-events-from-functions.mdx
index 6f6df125e..d3af10048 100644
--- a/pages/docs/guides/sending-events-from-functions.mdx
+++ b/pages/docs/guides/sending-events-from-functions.mdx
@@ -1,4 +1,4 @@
-import { Callout } from "src/shared/Docs/mdx";
+import { Callout, GuideSelector, GuideSection } from "src/shared/Docs/mdx";
export const description = 'How to send events from within functions to trigger other functions to run in parallel'
@@ -12,10 +12,19 @@ In some workflows or pipeline functions, you may want to broadcast events from w
* 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 with `step.invoke()`](/docs/guides/invoking-functions-directly) instead.
+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.
@@ -89,6 +98,190 @@ Each of these functions will run in parallel and individually retry on error, re
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:
diff --git a/pages/docs/guides/throttling.mdx b/pages/docs/guides/throttling.mdx
index 9e545548e..be87503e0 100644
--- a/pages/docs/guides/throttling.mdx
+++ b/pages/docs/guides/throttling.mdx
@@ -11,8 +11,8 @@ Throttling allows you to specify how many function runs can start within a time
## How to configure throttling
-
-```ts
+
+```ts {{ title: "TypeScript" }}
inngest.createFunction(
{
id: "unique-function-id",
@@ -28,6 +28,41 @@ inngest.createFunction(
}
);
```
+
+```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):
+ # This function will be throttled to 1 run per 5 seconds for a given event payload with matching user_id
+```
+
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.
diff --git a/pages/docs/guides/working-with-loops.mdx b/pages/docs/guides/working-with-loops.mdx
index 48abd4ba8..413de3df5 100644
--- a/pages/docs/guides/working-with-loops.mdx
+++ b/pages/docs/guides/working-with-loops.mdx
@@ -1,4 +1,4 @@
-import { CodeGroup, Callout } from "shared/Docs/mdx";
+import { CodeGroup, Callout, GuideSection, GuideSelector } from "shared/Docs/mdx";
export const description = 'Implement loops in your Inngest functions and avoid common pitfalls.';
# Working with Loops in Inngest
@@ -7,8 +7,19 @@ In Inngest each step in your function is executed as a separate HTTP request. T
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
@@ -87,8 +98,250 @@ inngest.createFunction(
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
@@ -132,6 +385,107 @@ Note that in the example above `getShopifySession` is deterministic across multi
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 {
+ var page *shopify.ProductsResponse
+ if _, err := step.Run(fmt.Sprintf("fetch-products-%v", cursor), func() error {
+ var err error
+ page, err = shopify.Product.All(&shopify.ProductListOptions{
+ Session: session,
+ SinceID: cursor,
+ })
+ return err
+ }); 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):
diff --git a/pages/docs/learn/inngest-functions.mdx b/pages/docs/learn/inngest-functions.mdx
index e128b17e9..f7144b443 100644
--- a/pages/docs/learn/inngest-functions.mdx
+++ b/pages/docs/learn/inngest-functions.mdx
@@ -1,6 +1,7 @@
import IconConcurrency from 'src/shared/Icons/FlowControl/Concurrency';
import { ResourceGrid, Resource } from 'src/shared/Docs/Resources';
import { EnvelopeIcon } from "@heroicons/react/24/outline";
+import { GuideSelector, GuideSection } from "shared/Docs/mdx";
export const description = 'Learn what Inngest functions are and of what they are capable.';
@@ -12,8 +13,18 @@ This page covers components of an Inngest function, as well as introduces differ
## Anatomy of an Inngest function
+
+
Let's have a look at the following Inngest function:
+
+
```ts
import { inngest } from "./client";
@@ -37,8 +48,87 @@ export default inngest.createFunction(
);
```
-The above code can be explained as:
+
+
+
+
+```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.
@@ -47,6 +137,8 @@ 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.
@@ -90,6 +182,102 @@ There are several step methods available at your disposal, for example, `step.ru
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:
+
+
+
+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:
+
+
+
+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
@@ -125,3 +313,5 @@ This is useful when you need to break down complex workflows into simpler, manag
- ["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.
+
+
\ No newline at end of file
diff --git a/pages/docs/learn/inngest-steps.mdx b/pages/docs/learn/inngest-steps.mdx
index 7b525380f..c129f70b4 100644
--- a/pages/docs/learn/inngest-steps.mdx
+++ b/pages/docs/learn/inngest-steps.mdx
@@ -2,6 +2,7 @@ import { Callout } from "shared/Docs/mdx";
import IconConcurrency from 'src/shared/Icons/FlowControl/Concurrency';
export const description = 'Learn about Inngest steps and their methods.';
+import { GuideSelector, GuideSection } from "shared/Docs/mdx";
# Inngest Steps
@@ -23,6 +24,16 @@ If you'd like to learn more about how Inngest steps are executed, check the ["Ho
## Anatomy of an Inngest Step
+
+
+
+
The first argument of every Inngest step method is an `id`. Each step is treated as a discrete task which can be individually retried, debugged, or recovered. Inngest uses the ID to memoize step state across function versions.
```typescript
@@ -63,6 +74,8 @@ export default inngest.createFunction(
{ id: "import-product-images" },
{ event: "shop/product.imported" },
async ({ event, step }) => {
+ // Here goes the business logic
+ // By wrapping code in steps, it will be retried automatically on failure
const uploadedImageURLs = await step.run("copy-images-to-s3", async () => {
return copyAllImagesToS3(event.data.imageURLs);
});
@@ -173,6 +186,385 @@ export default inngest.createFunction(
);
```
+
+
+
+
+The first argument of every Inngest step method is an `id`. Each step is treated as a discrete task which can be individually retried, debugged, or recovered. Inngest uses the ID to memoize step state across function versions.
+
+```go
+import (
+ "github.com/inngest/inngestgo"
+ "github.com/inngest/inngestgo/step"
+)
+
+inngestgo.CreateFunction(
+ // config
+ &inngestgo.FunctionOpts{
+ ID: "import-product-images",
+ },
+ // trigger (event or cron)
+ inngestgo.EventTrigger("shop/product.imported", nil),
+ // handler function
+ func(ctx context.Context, input inngestgo.Input) (any, error) {
+ // Here goes the business logic
+ // By wrapping code in steps, it will be retried automatically on failure
+ s3Urls, err := step.Run("copy-images-to-s3", func() ([]string, error) {
+ return copyAllImagesToS3(input.Event.Data["imageURLs"].([]string))
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return nil, nil
+ },
+)
+```
+
+The ID is also used to identify the function in the Inngest system.
+
+Inngest's SDK also records a counter for each unique step ID. The counter increases every time the same step is called. This allows you to run the same step in a loop, without changing the ID.
+
+
+Please note that each step is executed as **a separate HTTP request**. To ensure efficient and correct execution, place any non-deterministic logic (such as DB calls or API calls) within a `step.run()` call.
+
+
+## Available Step Methods
+
+### step.Run()
{{anchor: false}}
+
+This method executes a defined piece of code.
+Code within `step.Run()` is automatically retried if it throws an error. When `step.Run()` finishes successfully, the response is saved in the function run state and the step will not re-run.
+
+Use it to run synchronous or asynchronous code as a retriable step in your function.
+
+```go
+import (
+ "github.com/inngest/inngestgo"
+ "github.com/inngest/inngestgo/step"
+)
+
+inngestgo.CreateFunction(
+ &inngestgo.FunctionOpts{
+ ID: "import-product-images",
+ },
+ inngestgo.EventTrigger("shop/product.imported", nil),
+ func(ctx context.Context, input inngestgo.Input) (any, error) {
+ // Here goes the business logic
+ // By wrapping code in steps, it will be retried automatically on failure
+ s3Urls, err := step.Run("copy-images-to-s3", func() ([]string, error) {
+ return copyAllImagesToS3(input.Event.Data["imageURLs"].([]string))
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return nil, nil
+ },
+)
+```
+
+
+`step.Run()` acts as a code-level transaction. The entire step must succeed to complete.
+
+
+### step.Sleep()
{{anchor: false}}
+
+This method pauses execution for a specified duration. Inngest handles the scheduling for you. Use it to add delays or to wait for a specific amount of time before proceeding. At maximum, functions can sleep for a year (seven days for the [free tier plans](/pricing)).
+
+```go
+import (
+ "github.com/inngest/inngestgo"
+ "github.com/inngest/inngestgo/step"
+)
+
+inngestgo.CreateFunction(
+ &inngestgo.FunctionOpts{
+ ID: "send-delayed-email",
+ },
+ inngestgo.EventTrigger("app/user.signup", nil),
+ // handler function
+ func(ctx context.Context, input inngestgo.Input) (any, error) {
+ step.Sleep("wait-a-couple-of-days", 2 * time.Day)
+ },
+)
+```
+
+
+### step.WaitForEvent()
{{anchor: false}}
+
+This method pauses the execution until a specific event is received.
+
+```go
+import (
+ "github.com/inngest/inngestgo"
+ "github.com/inngest/inngestgo/errors"
+ "github.com/inngest/inngestgo/step"
+)
+
+inngestgo.CreateFunction(
+ &inngestgo.FunctionOpts{
+ ID: "send-delayed-email",
+ },
+ inngestgo.EventTrigger("app/user.signup", nil),
+ // handler function
+ func(ctx context.Context, input inngestgo.Input) (any, error) {
+ // Sample from the event stream for new events. The function will stop
+ // running and automatically resume when a matching event is found, or if
+ // the timeout is reached.
+ fn, err := step.WaitForEvent[FunctionCreatedEvent](
+ ctx,
+ "wait-for-activity",
+ step.WaitForEventOpts{
+ Name: "Wait for a function to be created",
+ Event: "api/function.created",
+ Timeout: time.Hour * 72,
+ // Match events where the user_id is the same in the async sampled event.
+ If: inngestgo.StrPtr("event.data.user_id == async.data.user_id"),
+ },
+ )
+ if err == step.ErrEventNotReceived {
+ // A function wasn't created within 3 days. Send a follow-up email.
+ _, _ = step.Run(ctx, "follow-up-email", func(ctx context.Context) (any, error) {
+ // ...
+ return true, nil
+ })
+ return nil, nil
+ }
+ },
+)
+```
+
+### step.Invoke()
{{anchor: false}}
+
+This method is used to asynchronously call another Inngest function ([written in any language SDK](/blog/cross-language-support-with-new-sdks)) and handle the result. Invoking other functions allows you to easily re-use functionality and compose them to create more complex workflows or map-reduce type jobs.
+
+This method comes with its own configuration, which enables defining specific settings like concurrency limits.
+
+```go
+import (
+ "github.com/inngest/inngestgo"
+ "github.com/inngest/inngestgo/errors"
+ "github.com/inngest/inngestgo/step"
+)
+
+inngestgo.CreateFunction(
+ &inngestgo.FunctionOpts{
+ ID: "send-delayed-email",
+ },
+ inngestgo.EventTrigger("app/user.signup", nil),
+ // handler function
+ func(ctx context.Context, input inngestgo.Input) (any, error) {
+ // Invoke another function and wait for its result
+ result, err := step.Invoke[any](
+ ctx,
+ "invoke-email-function",
+ step.InvokeOpts{
+ FunctionID: "send-welcome-email",
+ // Pass data to the invoked function
+ Data: map[string]any{
+ "user_id": input.Event.Data["user_id"],
+ "email": input.Event.Data["email"],
+ },
+ // Optional: Set a concurrency limit
+ Concurrency: step.ConcurrencyOpts{
+ Limit: 5,
+ Key: "user-{{event.data.user_id}}",
+ },
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+ },
+)
+```
+
+
+
+
+
+
+The first argument of every Inngest step method is an `id`. Each step is treated as a discrete task which can be individually retried, debugged, or recovered. Inngest uses the ID to memoize step state across function versions.
+
+```python
+import inngest
+from src.inngest.client import inngest_client
+
+@inngest_client.create_function(
+ fn_id="import-product-images",
+ event="shop/product.imported"
+)
+async def import_product_images(ctx: inngest.Context, step: inngest.Step):
+ uploaded_image_urls = await step.run(
+ # step ID
+ "copy-images-to-s3",
+ # other arguments, in this case: a handler
+ lambda: copy_all_images_to_s3(ctx.event.data["image_urls"])
+ )
+```
+
+The ID is also used to identify the function in the Inngest system.
+
+Inngest's SDK also records a counter for each unique step ID. The counter increases every time the same step is called. This allows you to run the same step in a loop, without changing the ID.
+
+
+Please note that each step is executed as **a separate HTTP request**. To ensure efficient and correct execution, place any non-deterministic logic (such as DB calls or API calls) within a `step.run()` call.
+
+
+## Available Step Methods
+
+### step.run()
{{anchor: false}}
+
+This method executes a defined piece of code.
+Code within `step.run()` is automatically retried if it throws an error. When `step.run()` finishes successfully, the response is saved in the function run state and the step will not re-run.
+
+Use it to run synchronous or asynchronous code as a retriable step in your function.
+
+```python
+import inngest
+from src.inngest.client import inngest_client
+
+@inngest_client.create_function(
+ fn_id="import-product-images",
+ event="shop/product.imported"
+)
+async def import_product_images(ctx: inngest.Context, step: inngest.Step):
+ # Here goes the business logic
+ # By wrapping code in steps, it will be retried automatically on failure
+ uploaded_image_urls = await step.run(
+ # step ID
+ "copy-images-to-s3",
+ # other arguments, in this case: a handler
+ lambda: copy_all_images_to_s3(ctx.event.data["image_urls"])
+ )
+```
+
+
+`step.run()` acts as a code-level transaction. The entire step must succeed to complete.
+
+
+### step.sleep()
{{anchor: false}}
+
+This method pauses execution for a specified duration. Inngest handles the scheduling for you. Use it to add delays or to wait for a specific amount of time before proceeding. At maximum, functions can sleep for a year (seven days for the [free tier plans](/pricing)).
+
+```python
+import inngest
+from src.inngest.client import inngest_client
+
+@inngest_client.create_function(
+ fn_id="send-delayed-email",
+ trigger=inngest.Trigger(event="app/user.signup")
+)
+async def send_delayed_email(ctx: inngest.Context, step: inngest.Step):
+ await step.sleep("wait-a-couple-of-days", datetime.timedelta(days=2))
+ # Do something else
+```
+
+### step.sleep_until()
{{anchor: false}}
+
+This method pauses execution until a specific date time. Any date time string in the format accepted by the Date object, for example `YYYY-MM-DD` or `YYYY-MM-DDHH:mm:ss`. At maximum, functions can sleep for a year (seven days for the [free tier plans](/pricing)).
+
+```python
+import inngest
+from src.inngest.client import inngest_client
+from datetime import datetime
+
+@inngest_client.create_function(
+ fn_id="send-scheduled-reminder",
+ trigger=inngest.Trigger(event="app/reminder.scheduled")
+)
+async def send_scheduled_reminder(ctx: inngest.Context, step: inngest.Step):
+ date = datetime.fromisoformat(ctx.event.data["remind_at"])
+ await step.sleep_until("wait-for-the-date", date)
+ # Do something else
+```
+
+### step.wait_for_event()
{{anchor: false}}
+
+This method pauses the execution until a specific event is received.
+
+```python
+import inngest
+from src.inngest.client import inngest_client
+
+@inngest_client.create_function(
+ fn_id="send-onboarding-nudge-email",
+ trigger=inngest.Trigger(event="app/account.created")
+)
+async def send_onboarding_nudge_email(ctx: inngest.Context, step: inngest.Step):
+ onboarding_completed = await step.wait_for_event(
+ "wait-for-onboarding-completion",
+ event="app/wait_for_event.fulfill",
+ if_exp="event.data.user_id == async.data.user_id",
+ timeout=datetime.timedelta(days=1),
+ );
+ # Do something else
+```
+
+### step.invoke()
{{anchor: false}}
+
+This method is used to asynchronously call another Inngest function ([written in any language SDK](/blog/cross-language-support-with-new-sdks)) and handle the result. Invoking other functions allows you to easily re-use functionality and compose them to create more complex workflows or map-reduce type jobs.
+
+This method comes with its own configuration, which enables defining specific settings like concurrency limits.
+
+```python
+import inngest
+from src.inngest.client import inngest_client
+
+@inngest_client.create_function(
+ fn_id="fn-1",
+ trigger=inngest.TriggerEvent(event="app/fn-1"),
+)
+async def fn_1(
+ ctx: inngest.Context,
+ step: inngest.Step,
+) -> None:
+ return "Hello!"
+
+@inngest_client.create_function(
+ fn_id="fn-2",
+ trigger=inngest.TriggerEvent(event="app/fn-2"),
+)
+async def fn_2(
+ ctx: inngest.Context,
+ step: inngest.Step,
+) -> None:
+ output = await step.invoke(
+ "invoke",
+ function=fn_1,
+ )
+
+ # Prints "Hello!"
+ print(output)
+```
+
+### step.send_event()
{{anchor: false}}
+
+This method sends events to Inngest to invoke functions with a matching event. Use `send_event()` when you want to trigger other functions, but you do not need to return the result. It is useful for example in [fan-out functions](/docs/guides/fan-out-jobs).
+
+```python
+import inngest
+from src.inngest.client import inngest_client
+
+@inngest_client.create_function(
+ fn_id="my_function",
+ trigger=inngest.TriggerEvent(event="app/my_function"),
+)
+async def fn(
+ ctx: inngest.Context,
+ step: inngest.Step,
+) -> list[str]:
+ return await step.send_event("send", inngest.Event(name="foo"))
+
+```
+
+
+
+
+
+
## Further reading
- [Quick Start](/docs/getting-started/nextjs-quick-start?ref=docs-inngest-steps): learn how to build complex workflows.
diff --git a/pages/docs/learn/serving-inngest-functions.mdx b/pages/docs/learn/serving-inngest-functions.mdx
index 98859734c..c742cb970 100644
--- a/pages/docs/learn/serving-inngest-functions.mdx
+++ b/pages/docs/learn/serving-inngest-functions.mdx
@@ -1,6 +1,7 @@
-import { Callout, CodeGroup, VersionBadge } from "shared/Docs/mdx";
+import { Callout, CodeGroup, VersionBadge, GuideSelector, GuideSection } from "shared/Docs/mdx";
export const description = `Serve the Inngest API as an HTTP endpoint in your application.`
+export const hidePageSidebar = true;
# Serving Inngest Functions
@@ -10,9 +11,19 @@ This page covers how to serve the Inngest API in your project.
## Exposing 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 {{ title: "./api/inngest.ts" }}
// All serve handlers have the same arguments:
serve({
client: inngest, // a client created with new Inngest()
@@ -21,12 +32,9 @@ serve({
});
```
-### Signing key
-
-You'll need to assign your [signing key](/docs/platform/signing-keys) to an [`INNGEST_SIGNING_KEY`](/docs/sdk/environment-variables#inngest-signing-key) environment variable in your hosting
-provider or `.env` file locally, which lets the SDK securely communicate with Inngest. If you can't
-provide this as a signing key, you can pass it in to `serve` when setting up your framework. [Read
-the reference for more information](/docs/sdk/reference/serve#reference).
+
+ You will find the complete list of supported frameworks below.
+
## Supported Platforms
@@ -869,6 +877,125 @@ const fn = inngest.createFunction(
export default serve({ client: inngest, functions: [fn] });
```
+
+
+
+
+
+Inngest enables you to create a HTTP handler for your functions. This handler will be used to serve your functions over HTTP (compatible with `net/http`).
+
+```go {{ title: "Go (HTTP)" }}
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/inngest/inngestgo"
+ "github.com/inngest/inngestgo/step"
+)
+
+func main() {
+ h := inngestgo.NewHandler("core", inngestgo.HandlerOpts{})
+ f := inngestgo.CreateFunction(
+ inngestgo.FunctionOpts{
+ ID: "account-created",
+ Name: "Account creation flow",
+ },
+ // Run on every api/account.created event.
+ inngestgo.EventTrigger("api/account.created", nil),
+ AccountCreated,
+ )
+ h.Register(f)
+ http.ListenAndServe(":8080", h)
+}
+```
+
+
+
+
+
+You expose your functions to Inngest through this HTTP endpoint.
+Inngest provides integrations with Flask and FastAPI.
+
+
+
+```python {{ title: "Python (Flask)" }}
+import logging
+import inngest
+from src.flask import app
+import inngest.flask
+
+logger = logging.getLogger(f"{app.logger.name}.inngest")
+logger.setLevel(logging.DEBUG)
+
+inngest_client = inngest.Inngest(app_id="flask_example", logger=logger)
+
+@inngest_client.create_function(
+ fn_id="hello-world",
+ trigger=inngest.TriggerEvent(event="say-hello"),
+)
+def hello(
+ ctx: inngest.Context,
+ step: inngest.StepSync,
+) -> str:
+
+inngest.flask.serve(
+ app,
+ inngest_client,
+ [hello],
+)
+
+app.run(port=8000)
+```
+
+```python {{ title: "Python (FastAPI)" }}
+import logging
+import inngest
+import fastapi
+import inngest.fast_api
+
+logger = logging.getLogger("uvicorn.inngest")
+logger.setLevel(logging.DEBUG)
+
+inngest_client = inngest.Inngest(app_id="fast_api_example", logger=logger)
+
+@inngest_client.create_function(
+ fn_id="hello-world",
+ trigger=inngest.TriggerEvent(event="say-hello"),
+)
+async def hello(
+ ctx: inngest.Context,
+ step: inngest.Step,
+) -> str:
+ return "Hello world!"
+
+app = fastapi.FastAPI()
+
+inngest.fast_api.serve(
+ app,
+ inngest_client,
+ [hello],
+)
+```
+
+
+
+
+
+
+
+### Signing key
+
+You'll need to assign your [signing key](/docs/platform/signing-keys) to an [`INNGEST_SIGNING_KEY`](/docs/sdk/environment-variables#inngest-signing-key) environment variable in your hosting
+provider or `.env` file locally, which lets the SDK securely communicate with Inngest. If you can't
+provide this as a signing key, you can pass it in to `serve` when setting up your framework. [Read
+the reference for more information](/docs/sdk/reference/serve#reference).
+
+
+
## Reference
For more information about the `serve` handler, read the [the reference guide](/docs/reference/serve), which includes:
diff --git a/pages/docs/platform/environments.mdx b/pages/docs/platform/environments.mdx
index 042419462..eb048b737 100644
--- a/pages/docs/platform/environments.mdx
+++ b/pages/docs/platform/environments.mdx
@@ -36,7 +36,7 @@ It can be helpful to visualize the typical Inngest developer workflow using Bran
As Branch Environments are created on-demand, all of your Branch Environments share the same Event Keys and Signing Key. This enables you to use the same environment variables in each of your application's deployment preview environments and set the environment dynamically using the `env` option with the `Inngest` client:
-```ts
+```ts {{ title: "TypeScript" }}
const inngest = new Inngest({
id: "my-app",
env: process.env.BRANCH,
@@ -46,6 +46,15 @@ const inngest = new Inngest({
// Pass the client to the serve handler to complete the setup
serve({ client: inngest, functions: [myFirstFunction, mySecondFunction] });
```
+
+```python {{ title: "Python" }}
+import inngest
+
+inngest_client = inngest.Inngest(
+ app_id="flask_example",
+ env=os.getenv("BRANCH"),
+)
+```
### Automatically Supported Platforms
@@ -61,12 +70,21 @@ You can always override this using [`INNGEST_ENV`](/docs/sdk/environment-variabl
Some platforms only pass an environment variable at build time. This means you'll have to explicitly set `env` to the platform's specific environment variable. For example, here's how you would set it on Netlify:
-```ts
+```ts {{ title: "TypeScript" }}
const inngest = new Inngest({
id: "my-app",
env: process.env.BRANCH,
});
```
+
+```python {{ title: "Python" }}
+import inngest
+
+inngest_client = inngest.Inngest(
+ app_id="flask_example",
+ env=os.getenv("BRANCH"),
+)
+```
- **Netlify** - `BRANCH` ([docs](https://docs.netlify.com/configure-builds/environment-variables/#git-metadata))
diff --git a/pages/docs/sdk/overview.mdx b/pages/docs/sdk/overview.mdx
index a2dfff412..03ea00c12 100644
--- a/pages/docs/sdk/overview.mdx
+++ b/pages/docs/sdk/overview.mdx
@@ -1,4 +1,4 @@
-import { CodeGroup } from "shared/Docs/mdx";
+import { CodeGroup, GuideSelector, GuideSection } from "shared/Docs/mdx";
# Installing the SDK
@@ -9,8 +9,20 @@ The Inngest SDK allows you to write reliable, durable functions in your existing
- 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:
@@ -28,8 +40,37 @@ bun add inngest
```
+
+
+
+
+
+To get started, install the SDK via `go get`:
+
+```shell
+go get github.com/inngest/inngest-go
+```
+
+
+
+
+
+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)
+3. [Trigger functions with events](/docs/events)
\ No newline at end of file
diff --git a/pages/docs/usage-limits/inngest.mdx b/pages/docs/usage-limits/inngest.mdx
index a20695e9a..08625434e 100644
--- a/pages/docs/usage-limits/inngest.mdx
+++ b/pages/docs/usage-limits/inngest.mdx
@@ -63,7 +63,7 @@ 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: "v3" }}
+```ts {{ title: "TypeScript" }}
// this `events` list will need to be <= 5000
const events = [{name: "", data: {}}, ...];
@@ -71,13 +71,21 @@ await step.sendEvent("send-example-events", events);
// or
await inngest.send(events);
```
-```ts {{ title: "v2" }}
+
+```go {{ title: "Go" }}
// this `events` list will need to be <= 5000
-const events = [{name: "", data: {}}, ...];
+events := []inngestgo.Event{{Name: "", Data: {}}}
-await step.sendEvent(events);
-// or
-await inngest.send(events);
+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)
```
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5bc9ad571..68eb95529 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,8 +23,8 @@ dependencies:
specifier: 1.6.2
version: 1.6.2
'@headlessui/react':
- specifier: 1.7.13
- version: 1.7.13(react-dom@18.2.0)(react@18.2.0)
+ specifier: 1.7.19
+ version: 1.7.19(react-dom@18.2.0)(react@18.2.0)
'@heroicons/react':
specifier: 2.0.18
version: 2.0.18(react@18.2.0)
@@ -2080,13 +2080,14 @@ packages:
graphql: 16.7.1
dev: false
- /@headlessui/react@1.7.13(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-9n+EQKRtD9266xIHXdY5MfiXPDfYwl7zBM7KOx2Ae3Gdgxy8QML1FkCMjq6AsOf0l6N9uvI4HcFtuFlenaldKg==}
+ /@headlessui/react@1.7.19(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==}
engines: {node: '>=10'}
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
dependencies:
+ '@tanstack/react-virtual': 3.10.9(react-dom@18.2.0)(react@18.2.0)
client-only: 0.0.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -2372,7 +2373,7 @@ packages:
'@motionone/easing': 10.15.1
'@motionone/types': 10.15.1
'@motionone/utils': 10.15.1
- tslib: 2.4.1
+ tslib: 2.8.1
dev: false
/@motionone/dom@10.15.3:
@@ -2390,7 +2391,7 @@ packages:
resolution: {integrity: sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==}
dependencies:
'@motionone/utils': 10.15.1
- tslib: 2.4.1
+ tslib: 2.8.1
dev: false
/@motionone/generators@10.15.1:
@@ -2398,7 +2399,7 @@ packages:
dependencies:
'@motionone/types': 10.15.1
'@motionone/utils': 10.15.1
- tslib: 2.4.1
+ tslib: 2.8.1
dev: false
/@motionone/types@10.15.1:
@@ -2410,7 +2411,7 @@ packages:
dependencies:
'@motionone/types': 10.15.1
hey-listen: 1.0.8
- tslib: 2.4.1
+ tslib: 2.8.1
dev: false
/@mrmlnc/readdir-enhanced@2.2.1:
@@ -2637,7 +2638,7 @@ packages:
open: 8.4.0
picocolors: 1.0.0
tiny-glob: 0.2.9
- tslib: 2.4.1
+ tslib: 2.8.1
dev: true
/@pmmmwh/react-refresh-webpack-plugin@0.5.10(react-refresh@0.11.0)(webpack@4.46.0):
@@ -4313,7 +4314,7 @@ packages:
/@swc/helpers@0.4.14:
resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==}
dependencies:
- tslib: 2.4.1
+ tslib: 2.8.1
dev: false
/@tailwindcss/typography@0.5.8(tailwindcss@3.4.1):
@@ -4328,6 +4329,21 @@ packages:
tailwindcss: 3.4.1
dev: true
+ /@tanstack/react-virtual@3.10.9(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ '@tanstack/virtual-core': 3.10.9
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
+ /@tanstack/virtual-core@3.10.9:
+ resolution: {integrity: sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==}
+ dev: false
+
/@testing-library/dom@8.11.3:
resolution: {integrity: sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==}
engines: {node: '>=12'}
@@ -6261,7 +6277,7 @@ packages:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
dependencies:
pascal-case: 3.1.2
- tslib: 2.4.1
+ tslib: 2.8.1
dev: true
/camelcase-css@2.0.1:
@@ -7322,7 +7338,7 @@ packages:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
dependencies:
no-case: 3.0.4
- tslib: 2.4.1
+ tslib: 2.8.1
dev: true
/dotenv-expand@5.1.0:
@@ -8005,6 +8021,7 @@ packages:
/eslint@8.46.0:
resolution: {integrity: sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
@@ -8810,7 +8827,7 @@ packages:
resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==}
engines: {node: '>= 4.0'}
os: [darwin]
- deprecated: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2
+ deprecated: Upgrade to fsevents v2 to mitigate potential security issues
requiresBuild: true
dependencies:
bindings: 1.5.0
@@ -10734,7 +10751,7 @@ packages:
/lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
dependencies:
- tslib: 2.4.1
+ tslib: 2.8.1
dev: true
/lowlight@1.20.0:
@@ -12137,7 +12154,7 @@ packages:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
dependencies:
lower-case: 2.0.2
- tslib: 2.4.1
+ tslib: 2.8.1
dev: true
/node-dir@0.1.17:
@@ -12616,7 +12633,7 @@ packages:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
dependencies:
dot-case: 3.0.4
- tslib: 2.4.1
+ tslib: 2.8.1
dev: true
/parent-module@1.0.1:
@@ -12704,7 +12721,7 @@ packages:
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
dependencies:
no-case: 3.0.4
- tslib: 2.4.1
+ tslib: 2.8.1
dev: true
/pascalcase@0.1.1:
@@ -15455,6 +15472,9 @@ packages:
/tslib@2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
+ /tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
/tsutils@3.21.0(typescript@5.1.3):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
diff --git a/shared/Docs/Code.tsx b/shared/Docs/Code.tsx
index 176271bb7..ac161e0d7 100644
--- a/shared/Docs/Code.tsx
+++ b/shared/Docs/Code.tsx
@@ -15,6 +15,7 @@ import clsx from "clsx";
import create from "zustand";
import { Tag } from "./Tag";
+import { useLocalStorage } from "react-use";
const languageNames = {
js: "JavaScript",
@@ -23,6 +24,7 @@ const languageNames = {
typescript: "TypeScript",
php: "PHP",
python: "Python",
+ py: "Python",
ruby: "Ruby",
go: "Go",
};
@@ -312,6 +314,8 @@ export function CodeGroup({
let languages = Children.map(children, (child) =>
getPanelTitle(child.props)
);
+ const [currentLanguage] = useLocalStorage("currentLanguage", null);
+ const mountRef = useRef(false);
let tabGroupProps = useTabGroupProps(languages);
let hasTabs = forceTabs || Children.count(children) > 1;
let Container: typeof Tab["Group"] | "div" = hasTabs ? Tab.Group : "div";
@@ -320,6 +324,30 @@ export function CodeGroup({
? { selectedIndex: tabGroupProps.selectedIndex }
: {};
+ // ensure to select the current language if set in local storage
+ useEffect(() => {
+ if (mountRef.current) {
+ return;
+ }
+ mountRef.current = true;
+ const childrenList: React.ReactElement[] = Children.toArray(
+ children
+ ) as React.ReactElement[];
+ if (
+ tabGroupProps &&
+ currentLanguage &&
+ childrenList.find(
+ (child) => child.props?.title?.toLowerCase() === currentLanguage
+ )
+ ) {
+ tabGroupProps.onChange(
+ childrenList.findIndex(
+ (child) => child.props?.title?.toLowerCase() === currentLanguage
+ )
+ );
+ }
+ }, []);
+
return (
(options[0].key);
+ const [defaultSelected, setDefaultSelected] = useState(
+ options[0].key
+ );
+ // infer the default selected from the url or local storage
useEffect(() => {
+ if (mountRef.current) {
+ return;
+ }
+ mountRef.current = true;
+
const urlSelected = Array.isArray(router.query[searchParamKey])
? router.query[searchParamKey][0]
: router.query[searchParamKey];
- const isValidOption = options.find((o) => o.key === urlSelected);
- if (isValidOption && Boolean(urlSelected) && urlSelected !== selected) {
+ if (
+ options.find((o) => o.key === urlSelected) &&
+ Boolean(urlSelected) &&
+ urlSelected !== selected
+ ) {
setSelected(urlSelected);
+ setDefaultSelected(urlSelected);
+ } else if (
+ !urlSelected &&
+ // if no url param, fallback to local storage
+ currentLanguage &&
+ options.find((o) => o.key === currentLanguage)
+ ) {
+ setSelected(currentLanguage);
+ setDefaultSelected(currentLanguage);
}
- }, [router, selected]);
+ }, []);
const onChange = (newSelectedIndex) => {
const newSelectedKey = options[newSelectedIndex].key;
+ setCurrentLanguage(newSelectedKey);
setSelected(newSelectedKey);
const url = new URL(router.asPath, window.location.origin);
url.searchParams.set(searchParamKey, newSelectedKey);
@@ -405,11 +461,16 @@ export function GuideSelector({
return (
-
+ o.key === defaultSelected)}
+ selectedIndex={options.findIndex((o) => o.key === selected)}
+ >
{options.map((option, idx) => (