diff --git a/docs.json b/docs.json index 7892add36..e87ed6789 100644 --- a/docs.json +++ b/docs.json @@ -1254,7 +1254,7 @@ "tab": "Workflow", "groups": [ { - "group": "Workflow", + "group": "Getting Started", "pages": [ "workflow/getstarted", { @@ -1286,120 +1286,66 @@ } ] }, + "workflow/basics/how", + "workflow/basics/caveats" + ] + }, + { + "group": "Core Concepts", + "pages": [ { - "group": "Basics", + "group": "Create Workflow Endpoint", "pages": [ - "workflow/basics/how", - { - "group": "Serve Workflow", - "pages": [ - "workflow/basics/serve", - "workflow/basics/serve/advanced" - ] - }, - { - "group": "Workflow Context", - "pages": [ - "workflow/basics/context", - { - "group": "Functions", - "pages": [ - "workflow/basics/context/run", - "workflow/basics/context/sleep", - "workflow/basics/context/sleepUntil", - "workflow/basics/context/call", - "workflow/basics/context/waitForEvent", - "workflow/basics/context/createWebhook", - "workflow/basics/context/waitForWebhook", - "workflow/basics/context/notify", - "workflow/basics/context/invoke", - "workflow/basics/context/api", - "workflow/basics/context/cancel" - ] - } - ] - }, - { - "group": "Workflow Client", - "pages": [ - "workflow/basics/client", - { - "group": "Functions", - "pages": [ - "workflow/basics/client/trigger", - "workflow/basics/client/cancel", - "workflow/basics/client/logs", - { - "group": "client.dlq", - "pages": [ - "workflow/basics/client/dlq/list", - "workflow/basics/client/dlq/restart", - "workflow/basics/client/dlq/resume", - "workflow/basics/client/dlq/delete", - "workflow/basics/client/dlq/callback" - ] - }, - "workflow/basics/client/notify", - "workflow/basics/client/waiters" - ] - } - ] - }, - "workflow/basics/caveats" + "workflow/basics/serve", + "workflow/basics/serve/advanced" ] }, { - "group": "Features", + "group": "Local Development", "pages": [ + "workflow/howto/local-development/development-server", + "workflow/howto/local-development/local-tunnel" + ] + }, + { + "group": "Steps", + "pages": [ + "workflow/steps", + "workflow/steps/run", + "workflow/steps/sleep", + "workflow/steps/sleepUntil", + "workflow/steps/call", + "workflow/steps/waitForEvent", + "workflow/steps/createWebhook", + "workflow/steps/waitForWebhook", + "workflow/steps/notify", + "workflow/steps/invoke", + "workflow/steps/api", + "workflow/steps/cancel" + ] + }, + { + "group": "Workflow Client", + "pages": [ + "workflow/basics/client", { - "group": "Retries", - "pages": [ - "workflow/features/retries", - "workflow/features/retries/prevent-retries" - ] - }, - "workflow/features/parallel-steps", - { - "group": "Failure Function", - "pages": [ - "workflow/features/failure-callback", - "workflow/features/failureFunction/reliability", - "workflow/features/failureFunction/advanced" - ] - }, - { - "group": "Dead Letter Queue", - "pages": [ - "workflow/features/dlq", - "workflow/features/dlq/restart", - "workflow/features/dlq/resume", - "workflow/features/dlq/delete", - "workflow/features/dlq/callback" - ] - }, - { - "group": "Flow Control", - "pages": [ - "workflow/features/flow-control", - "workflow/features/flow-control/rate-period", - "workflow/features/flow-control/parallelism", - "workflow/features/flow-control/monitor" - ] - }, - { - "group": "Wait For Event", - "pages": [ - "workflow/features/wait-for-event", - "workflow/features/wait", - "workflow/features/notify" - ] - }, - "workflow/features/webhooks", - { - "group": "Invoke", + "group": "Functions", "pages": [ - "workflow/features/invoke", - "workflow/features/invoke/serveMany" + "workflow/basics/client/trigger", + "workflow/basics/client/cancel", + "workflow/basics/client/logs", + { + "group": "client.dlq", + "pages": [ + "workflow/basics/client/dlq/list", + "workflow/basics/client/dlq/restart", + "workflow/basics/client/dlq/resume", + "workflow/basics/client/dlq/delete", + "workflow/basics/client/dlq/callback" + ] + }, + "workflow/basics/client/notify", + "workflow/basics/client/waiters" ] } ] @@ -1407,13 +1353,6 @@ { "group": "How To", "pages": [ - { - "group": "Local Development", - "pages": [ - "workflow/howto/local-development/development-server", - "workflow/howto/local-development/local-tunnel" - ] - }, "workflow/howto/start", "workflow/howto/configure", "workflow/howto/cancel", @@ -1433,25 +1372,86 @@ ] } ] + } + ] + }, + { + "group": "Features", + "pages": [ + { + "group": "Retries", + "pages": [ + "workflow/features/retries", + "workflow/features/retries/prevent-retries" + ] }, + "workflow/features/parallel-steps", { - "group": "Agents", + "group": "Failure Function", "pages": [ - "workflow/agents/overview", - "workflow/agents/getting-started", - "workflow/agents/features", - { - "group": "Patterns", - "pages": [ - "workflow/agents/patterns/prompt-chaining", - "workflow/agents/patterns/evaluator-optimizer", - "workflow/agents/patterns/parallelization", - "workflow/agents/patterns/orchestrator-workers" - ] - }, - "workflow/agents/examples" + "workflow/features/failure-callback", + "workflow/features/failureFunction/reliability", + "workflow/features/failureFunction/advanced" + ] + }, + { + "group": "Dead Letter Queue", + "pages": [ + "workflow/features/dlq", + "workflow/features/dlq/restart", + "workflow/features/dlq/resume", + "workflow/features/dlq/delete", + "workflow/features/dlq/callback" + ] + }, + { + "group": "Flow Control", + "pages": [ + "workflow/features/flow-control", + "workflow/features/flow-control/rate-period", + "workflow/features/flow-control/parallelism", + "workflow/features/flow-control/monitor" + ] + }, + { + "group": "Wait For Event", + "pages": [ + "workflow/features/wait-for-event", + "workflow/features/wait", + "workflow/features/notify" ] }, + "workflow/features/webhooks", + { + "group": "Invoke", + "pages": [ + "workflow/features/invoke", + "workflow/features/invoke/serveMany" + ] + } + ] + }, + { + "group": "Agents", + "pages": [ + "workflow/agents/overview", + "workflow/agents/getting-started", + "workflow/agents/features", + { + "group": "Patterns", + "pages": [ + "workflow/agents/patterns/prompt-chaining", + "workflow/agents/patterns/evaluator-optimizer", + "workflow/agents/patterns/parallelization", + "workflow/agents/patterns/orchestrator-workers" + ] + }, + "workflow/agents/examples" + ] + }, + { + "group": "API Reference", + "pages": [ { "group": "REST API", "openapi": { @@ -1459,17 +1459,22 @@ "directory": "workflow/api-reference" } }, + "workflow/sdk/workflow-js", + "workflow/sdk/workflow-py" + ] + }, + { + "group": "Resources", + "pages": [ { "group": "Integrations", "pages": [ "workflow/integrations/openai", "workflow/integrations/anthropic", - "workflow/integrations/datadog", - "workflow/integrations/prometheus", - "workflow/integrations/resend", "workflow/integrations/aisdk", - "workflow/sdk/workflow-js", - "workflow/sdk/workflow-py" + "workflow/integrations/resend", + "workflow/integrations/datadog", + "workflow/integrations/prometheus" ] }, { @@ -1903,6 +1908,14 @@ ] }, "redirects": [ + { + "source": "/workflow/basics/context", + "destination": "/workflow/steps" + }, + { + "source": "/workflow/basics/context/:slug*", + "destination": "/workflow/steps/:slug*" + }, { "source": "/redis/overall/rediscompatibility", "destination": "/redis/overall/compatibility" diff --git a/llms-full.txt b/llms-full.txt index 90f3a1430..84d9759cd 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -43284,5933 +43284,5950 @@ const result = await client.getWaiters({ }); ``` -# Overview -Source: https://upstash.com/docs/workflow/basics/context +# How Workflow Works +Source: https://upstash.com/docs/workflow/basics/how -A workflow's **context** is an object provided by the route function. +Upstash Workflow is an orchestration layer that allows you to write **multi‑step workflows** which are: -The context object provides: -* **Workflow APIs** – functions for defining workflow steps. -* **Workflow Run Properties** – request payload, request headers, and other metadata. +* **Durable** – steps automatically recover from errors or outages +* **Scalable** – steps run independently and in parallel when possible +* **Cost‑efficient** – idle waiting (delays, sleeps, external calls) does not consume compute resources - - ```typescript api/workflow/route.ts highlight={4-5} - import { serve } from "@upstash/workflow/nextjs"; +Upstash Workflow is built on top of Upstash QStash, our serverless messaging and scheduling solution, to achieve these features. - export const { POST } = serve( - // 👇 the workflow context - async (context) => { - // ... - } - ); - ``` +## The Core Idea - ```python main.py - from fastapi import FastAPI - from upstash_workflow.fastapi import Serve - from upstash_workflow import AsyncWorkflowContext +Traditionally, backend functions are built in one of two ways: either everything is executed inside a single API function—which is difficult to maintain and prone to failures—or the flow is split across multiple APIs connected by a queueing system, which adds significant infrastructure and state‑management overhead. - app = FastAPI() - serve = Serve(app) +These approaches can work, but they often fail to handle production load reliably or become increasingly difficult to maintain over time: - @serve.post("/api/example") - async def example(context: AsyncWorkflowContext[str]) -> None: ... +* **Timeouts** – the whole function runs inside one execution window. A slow API can easily exceed serverless limits (often 10–60 seconds). +* **Temporary issues** – slow or unreliable external services can exceed serverless limits or cause the entire request to fail. +* **Failures** – if a step fails, the whole request fails. You either restart everything or you must write custom retry logic. +* **Rate limits** – calling external APIs in bulk requires careful concurrency control, which is difficult to implement manually. +* **Complexity** – to address these issues, teams often build custom queues, schedulers, or state trackers, adding unnecessary infrastructure overhead. - ``` - +*** -## Context Object Properties +## How Upstash Workflow Solves This - - The request payload passed to the workflow run via `trigger()` call. - +Upstash Workflow takes a different approach: +instead of treating your entire function as one continuous execution, **it splits your logic into multiple steps in a workflow endpoint**, each managed and retried by the orchestration engine. - - The request headers passed to the workflow run via `trigger()` call. - +* Each step is executed in its own **HTTP call** to your application. +* After a step finishes, its result is **stored in durable state** inside Upstash Workflow. +* On the next execution, Workflow **skips completed steps** and **resumes exactly where it left off by restoring the previous step results**. +* If a step fails, it is retried automatically based on your retry configuration. - - The unique identifier of the current workflow run. - +This means you no longer need custom queues, retry logic, or manual state management. You just define your workflow once, and the orchestration layer ensures that **every step runs once, in order, with full reliability.** - - The public URL of the workflow endpoint. - + - - The URL used for workflow failure callback. +*** - If a failure function is defined, this is the same as the workflow's `url`. - +## Extended Features - - The environment variables available to the workflow. - +Upstash Workflow extends the basic step model with additional primitives: - - The QStash client instance used by the workflow endpoint. - +* **Parallel Steps** + Define multiple steps (e.g. inside a `Promise.all()`). The engine detects independent work and runs steps concurrently as separate HTTP executions. - - The labels attached to the current workflow run, if set in [client.trigger](/docs/workflow/basics/client/trigger). - Defaults to an empty array when no label was set. - +* **Delays / Sleep** + `context.sleep` and `context.sleepUntil` allow pausing a workflow for hours, days, or even months. No compute is held during the wait time; execution resumes when the delay has expired. - - Deprecated. Use `labels` instead. When a run has multiple labels, this only - returns the first one. - +* **External Event Handling** + `context.waitForEvent` pauses execution until you notify the workflow externally (e.g. via webhook or user action). State is persisted until the event arrives. -## Context Object Functions +* **External Calls** + Use `context.call` to have Upstash perform slow or unreliable HTTP calls. Instead of blocking your function, the call is handled by Upstash. When it completes, the workflow resumes with the response. -You can use the functions exposed by context object to define workflow steps. +*** -* [context.run](/docs/workflow/basics/context/run) -* [context.sleep](/docs/workflow/basics/context/sleep) -* [context.sleepUntil](/docs/workflow/basics/context/sleepUntil) -* [context.waitForEvent](/docs/workflow/basics/context/waitForEvent) -* [context.createWebhook](/docs/workflow/basics/context/createWebhook) -* [context.waitForWebhook](/docs/workflow/basics/context/waitForWebhook) -* [context.notify](/docs/workflow/basics/context/notify) -* [context.invoke](/docs/workflow/basics/context/invoke) -* [context.call](/docs/workflow/basics/context/call) -* [context.cancel](/docs/workflow/basics/context/cancel) -* [context.api](/docs/workflow/basics/context/api) +This architecture makes your serverless functions durable, reliable, and performance‑optimized, even in the face of runtime errors or temporary service outages. -# context.api -Source: https://upstash.com/docs/workflow/basics/context/api +It's quick and easy to get started: follow the [Quickstarts](/docs/workflow/quickstarts/platforms) to define your first workflow in minutes. -In addition to `context.call`, you can also make third‑party requests using the `context.api` namespace. +# Overview +Source: https://upstash.com/docs/workflow/basics/serve -This namespace provides built‑in integrations for **OpenAI**, **Anthropic**, and **Resend**, allowing you to make requests in a **type‑safe** manner. +Use the `serve()` function to define an endpoint that runs a workflow. +It accepts two arguments: + +1. **Route Function**: an async function that receives the workflow context and defines the workflow steps. +2. **Options**: configuration options for the workflow. + ```typescript TypeScript + import { serve } from "@upstash/workflow/nextjs"; -```typescript OpenAI -const { status, body } = await context.api.openai.call("Call OpenAI", { - token: "", - operation: "chat.completions.create", - body: { - model: "gpt-4o", - messages: [ - { - role: "system", - content: "Assistant says 'hello!'", - }, - { role: "user", content: "User shouts back 'hi!'" }, - ], - }, -}); -``` + export const { POST } = serve(async (context) => { + // Route function + }, { + // Options + }); + ``` -```typescript Anthropic -const { status, body } = await context.api.anthropic.call( - "Call Anthropic", - { - token: "", - operation: "messages.create", - body: { - model: "claude-3-5-sonnet-20241022", - max_tokens: 1024, - messages: [ - {"role": "user", "content": "Hello, world"} - ] - }, - } -); -``` + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext -```typescript Resend -const { status, body } = await context.api.resend.call("Call Resend", { - token: "", - body: { - from: "Acme ", - to: ["delivered@resend.dev"], - subject: "Hello World", - html: "

It works!

", - }, - headers: { - "content-type": "application/json", - }, -}); -``` + app = FastAPI() + serve = Serve(app) -
+ @serve.post("/api/example") + async def example(context: AsyncWorkflowContext[str]) -> None: + async def _step1() -> str: + # define a piece of business logic as step 1 + return "step 1 result" -We'll continue adding more integrations over time. If you'd like to see a specific integration, feel free to contribute to the SDK or contact us with your suggestion. + result = await context.run("step-1", _step1) -For detailed guides on usage and configuration, see the [Integrations section](/docs/workflow/integrations/openai). + async def _step2() -> None: + # define another piece of business logic as step 2 + pass -# context.call -Source: https://upstash.com/docs/workflow/basics/context/call + await context.run("step-2", _step2) + ``` + -`context.call()` performs an HTTP request as a workflow step, supporting longer response times up to 12 hours. +## Route Function -The request is executed by **Upstash on your behalf**, so your application does not consume compute resources during the request duration. +The route function defines the execution logic of the workflow. +It is an async function that receives a context object, which is automatically created and passed by Upstash Workflow. -If the endpoint responds with a non‑success status code (anything outside `200–299`), -`context.call()` still returns the response and the workflow continues. -This allows you to inspect the response (via the `status` field) and decide how to handle failure cases in your logic. +The context object provides: +* **Workflow APIs** – functions for defining workflow steps. +* **Workflow Run Properties** – request payload, request headers, and other metadata. -If you want requests to retry automatically, you can explicitly pass a retry configuration. +For a full list of available APIs and properties, see the [Workflow Context](/docs/workflow/basics/context) documentation. -## Arguments + + ```typescript TypeScript highlight={4-9} + import { serve } from "@upstash/workflow/nextjs"; - - The URL of the HTTP endpoint to call. - + export const { POST } = serve( + async (context) => { + // 👇 Access context properties + const { userId } = context.requestPayload; + // 👇 Define a workflow step + await context.run("step-1", async () => {}) + } + ); + ``` - - TThe HTTP method to use (`GET`, `POST`, `PUT`, etc.). Defaults to `GET`. - + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext - - The request body as a string. - + app = FastAPI() + serve = Serve(app) - - A map of headers to include in the request. - + @serve.post("/api/example") + async def example(context: AsyncWorkflowContext[str]) -> None: + async def _step1() -> str: + # define a piece of business logic as step 1 + return "step 1 result" - - Number of retry attempts if the request fails. Defaults to `0` (no retries). - + result = await context.run("step-1", _step1) - - Delay between retries (in milliseconds). By default, uses exponential backoff. You can use mathematical expressions and the special variable `retried` (current retry attempt count starting from 0). Examples: `1000`, `pow(2, retried)`, `max(10, pow(2, retried))`. - + async def _step2() -> None: + # define another piece of business logic as step 2 + pass - - Throttle outbound requests. + await context.run("step-2", _step2) - See [Flow Control](/docs/workflow/features/flow-control) for details. - - - - A logical grouping key that identifies which requests share the same flow control limits. - - - - The maximum number of allowed requests per second. - + ``` + - - The maximum number of concurrent requests allowed. - +## Options - - The time window used to enforce the defined rate limit. Default is `1s`. - - - +Options provide additional configuration for workflow runs. +Most of them are advanced settings and are not required for typical use cases. See [Advanced Options](/docs/workflow/basics/serve/advanced) for more details. - - Maximum time (in seconds) to wait for a response. - If retries are enabled, this timeout applies individually to each attempt. - + - - When using [`serveMany`](/docs/workflow/features/invoke/serveMany#using-serve-manyny), you can call another workflow defined in the same `serveMany` by passing it to this parameter. - + ```typescript TypeScript highlight={5-8} + import { serve } from "@upstash/workflow/nextjs"; -## Response + export const { POST } = serve( + async (context) => { ... }, + // 👇 Workflow options + { + failureFunction: async ({ ... }) => {} + } + ); + ``` - - The HTTP response status code. - + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext - - The response body. + app = FastAPI() + serve = Serve(app) - `context.call()` attempts to parse the body as JSON. - If parsing fails, the raw body string is returned. - + @serve.post("/api/example") + async def example(context: AsyncWorkflowContext[str]) -> None: + async def _step1() -> str: + # define a piece of business logic as step 1 + return "step 1 result" - - The response headers. - + result = await context.run("step-1", _step1) - - In TypeScript, you can declare the expected result type for strong typing: + async def _step2() -> None: + # define another piece of business logic as step 2 + pass - ```typescript - type ResultType = { - field1: string, - field2: number - }; + await context.run("step-2", _step2) - const result = await context.call( ... ); ``` - - -## Usage - - + -```javascript TypeScript -import { serve } from "@upstash/workflow/nextjs"; +# Advanced Options +Source: https://upstash.com/docs/workflow/basics/serve/advanced -export const { POST } = serve<{ topic: string }>(async (context) => { - const { userId, name } = context.requestPayload; +Advanced Options are intended to support edge cases or testing pipelines and are **not required for regular use**. - const { status, headers, body } = await context.call("sync-user-data", { - url: "https://my-third-party-app", // Endpoint URL - method: "POST", - body: JSON.stringify({ - userId, - name - }), - headers: { - authorization: `Bearer ${process.env.OPENAI_API_KEY}`, - }, - } - ); -}); + + Defines a function that executes if the workflow fails after all retries are exhausted. -``` + For details, see [failureFunction](/docs/workflow/features/failure-callback). -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext + + ```typescript TypeScript + export const { POST } = serve( + async (context) => { ... }, + { + failureFunction: async ({ + context, // context during failure + failStatus, // failure status + failResponse, // failure message + failHeaders, // failure headers + failStack // failure stack trace (if available) + }) => { + // handle the failure + } + } + ); + ``` -app = FastAPI() -serve = Serve(app) + ```python Python + async def failure_function( + context, # context during failure + fail_status, # failure status + fail_response, # failure message + fail_headers # failure headers + ): + # handle the failure + pass -@dataclass -class Request: - topic: str + @serve.post("/api/example", failure_function=failure_function) + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` -@serve.post("/api/example") -async def example(context: AsyncWorkflowContext[Request]) -> None: - request: Request = context.request_payload + + - result = await context.call( - "generate-long-essay", - url="https://api.openai.com/v1/chat/completions", - method="POST", - body={ - "model": "gpt-4o", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant writing really long essays that would cause a normal serverless function to timeout.", - }, - {"role": "user", "content": request["topic"]}, - ], - }, - headers={ - "authorization": f"Bearer {os.environ['OPENAI_API_KEY']}", - }, - ) + - status, headers, body = result.status, result.headers, result.body + + This parameter is only available in Python SDK. In Javascript SDK, you can pass this value when triggering the workflow. + -``` + The `failureUrl` option defines an external endpoint that will be called if the workflow fails after all retries are exhausted. - + This option is an advanced alternative to `failureFunction`. + For more details, see [Advanced failureUrl Option](/docs/workflow/features/failureFunction/advanced). - - We provide integrations for **OpenAI, Anthropic, and Resend**, allowing you to call their APIs with strongly typed request bodies using `context.call`. - See [`context.api`](/docs/workflow/basics/context#context-api) for details. - + - - The `context.call()` function can make requests to any public API endpoint. However, it cannot: + ```python Python + @serve.post("/api/example", failureUrl="https:///...") + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` - * Make requests to localhost (unless you set up a local tunnel, [here's how](/docs/workflow/howto/local-development/local-tunnel)) - * Make requests to internal Upstash QStash endpoints. - + + -# context.cancel -Source: https://upstash.com/docs/workflow/basics/context/cancel + -All of the methods covered so far are used to define workflow steps. + + This parameter is only available in Python SDK. In Javascript SDK, you can pass this value when triggering the workflow. + -`context.cancel` is different — it allows you to **explicitly cancel the current workflow run**. + Defines the number of retry attempts if a workflow step fails. + The default value is 3. -```ts -export const { POST } = serve<{ topic: string }>(async (context) => { - const payload = context.requestPayload + For details, see [retry configuration](/docs/workflow/features/retries#configuration). - const result = await context.run("check if canceled", () => { ... }); + - if (result.cancel) { - await context.cancel() // cancel the workflow run - } -}) -``` + ```python Python + @serve.post("/api/example", retries=3) + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` + + -When a workflow run is canceled: + + An array of middleware instances that intercept workflow lifecycle and debug events. -* It is labeled as **canceled** (not failed). -* The configured `failureFunction` **is not triggered**. -* No entries are sent to the **dead-letter queue (DLQ)**. + Middlewares allow you to hook into various stages of workflow execution (before/after steps, run start/completion) + and debug events (errors, warnings, info logs). -# context.createWebhook -Source: https://upstash.com/docs/workflow/basics/context/createWebhook + For details and examples, see [Middlewares](/docs/workflow/howto/middlewares). -`context.createWebhook()` creates a unique webhook that can be called by external services to trigger workflow continuation. + -The webhook URL generated can be called multiple times to resume multiple [`context.waitForWebhook`](/docs/workflow/basics/context/waitForWebhook) steps. + ```typescript TypeScript + import { serve } from "@upstash/workflow/nextjs"; + import { loggingMiddleware } from "@upstash/workflow"; -## Arguments + export const { POST } = serve( + async (context) => { ... }, + { + middlewares: [loggingMiddleware] + } + ); + ``` - - Name of the step. + -## Response + + Enables custom parsing of the initial request payload. - - The unique webhook URL that external services should call to resume the workflow. + Use this option if the incoming payload is not plain JSON or a simple string. + The parser function lets you transform the raw request into a strongly typed + object before workflow execution begins. - Can be called multiple times to resume multiple [`context.waitForWebhook`](/docs/workflow/basics/context/waitForWebhook) steps. - - - - The internal event identifier associated with this webhook. - This is primarily used internally by [`context.waitForWebhook`](/docs/workflow/basics/context/waitForWebhook). - + -## Usage + ```typescript TypeScript + type InitialPayload = { + foo: string; + bar: number; + }; -```typescript highlight={4} -import { serve } from "@upstash/workflow/nextjs"; + // 👇 1: provide initial payload type + export const { POST } = serve( + async (context) => { + // 👇 3: parsing result is available as requestPayload + const payload: InitialPayload = context.requestPayload; + }, + { + // 👇 2: custom parsing for initial payload + initialPayloadParser: (initialPayload) => { + const payload: InitialPayload = parsePayload(initialPayload); + return payload; + }, + } + ); + ``` -export const { POST } = serve(async (context) => { - const webhook = await context.createWebhook("create webhook"); + ```python Python + @dataclass + class InitialPayload: + foo: str + bar: int - console.log(webhook.webhookUrl); // Use this URL with external services -}); -``` + def initial_payload_parser(initial_payload: str) -> InitialPayload: + return parse_payload(initial_payload) -For more complete examples and use cases, see [the page on webhooks](/docs/workflow/features/webhooks). + @serve.post("/api/example", initial_payload_parser=initial_payload_parser) + async def example(context: AsyncWorkflowContext[InitialPayload]) -> None: + payload: InitialPayload = context.request_payload -# context.invoke -Source: https://upstash.com/docs/workflow/basics/context/invoke + ``` -`context.invoke()` triggers another workflow run and pauses until the invoked workflow finishes. + -The calling workflow resumes once the invoked workflow either **succeeds**, **fails**, or is **canceled**. + - - Workflows can only invoke other workflows that were served together in the same `serveMany` route. - For details, see [Invoke other workflows](/docs/workflow/features/invoke). - + + Alternative to `initialPayloadParser`, you can pass a `schema` in the TypeScript SDK. -## Arguments + The schema is used to validate and parse the initial request payload automatically using [Zod](https://zod.dev/). - - The workflow definition to invoke. - Must be a workflow exposed under the same `serveMany`. - + - - The payload to send to the invoked workflow. - This value will be set as `context.requestPayload` in the invoked workflow. - + ```typescript TypeScript - - Optional HTTP headers to forward to the invoked workflow. - This value will be set as `context.headers` in the invoked workflow. - + import { z } from "zod"; - - Override the workflow run ID for the invoked workflow. - Defaults to a new ID if not specified. - + const parameters = z.object({ expression: z.string() }); - - Number of retry attempts configuration of the invoked workflow. - Defaults to `3`. Retries use exponential backoff. - + export const { POST } = serve( + async (context) => { + // context.requestPayload is typed as `{ expression: string }` + const payload = context.requestPayload; + }, + { + schema: parameters, + } + ); + ``` - - Delay between retries of the invoked workflow. + - - Flow control configuration of the invoked workflow. + + Specifies the full endpoint URL of the workflow, including the route path. - See [Flow Control](/docs/workflow/features/flow-control) for details. + By default, Upstash Workflow infers the URL from `request.url` when scheduling the next step. + However, in some environments, `request.url` may resolve to an internal or unreachable address. - - - A logical grouping key that identifies which requests share the same flow control limits. - + Use this option when running behind a proxy, reverse proxy, or local tunnel during development where `request.url` cannot be used directly. - - The maximum number of allowed requests per second. - + - - The maximum number of concurrent requests allowed. - + ```typescript TypeScript + export const { POST } = serve( + async (context) => { ... }, + { + url: "https://.com/api/workflow" + } + ); + ``` - - The time window used to enforce the defined rate limit. Default is `1s`. - - + ```python Python + @serve.post("/api/example", url="https://.com/api/workflow") + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` + -## Response + - - The response body returned by the invoked workflow. - + Similar to `url`, but `baseUrl` only overrides the base portion of the inferred URL rather than replacing the entire path. + This is useful when you want to preserve the route structure while changing only the host or scheme. - - `true` if the invoked workflow completed with failure. - + + If you have multiple workflow endpoints, you can set the `UPSTASH_WORKFLOW_URL` environment variable instead of configuring `baseUrl` on each endpoint. + The `UPSTASH_WORKFLOW_URL` environment variable corresponds directly to this option and configures it globally. + - - `true` if the invoked workflow was canceled before completion. - + -## Usage + ```typescript TypeScript + export const { POST } = serve( + async (context) => { + ... + }, + // options: + { + baseUrl: "" + } + ); + ``` -```ts -const { body, isFailed, isCanceled } = await context.invoke( - "invoke another workflow", - { - workflow: anotherWorkflow, - body: "test", - header: {...}, // headers to pass to anotherWorkflow (optional) - retries, // number of retries (optional, default: 3) - retryDelay, // delay between retries (optional, uses exponential backoff by default) - flowControl, // flow control settings (optional) - workflowRunId // workflowRunId to set (optional) - } -); -``` + ```python Python + @serve.post("/api/example", base_url="") + async def example(context: AsyncWorkflowContext[str]) -> None: ... -# context.notify -Source: https://upstash.com/docs/workflow/basics/context/notify + ``` -`context.notify()` notifies workflows that are waiting for a specific event, passing along an optional payload. + + -It is typically used in combination with [`context.waitForEvent`](/docs/workflow/basics/context#context-waitforevent). + -## Arguments + Use `qstashClient` if you want to provide your own QStash client instead of letting Workflow use the default from environment variables. - - A unique identifier for the step. - + This is useful if you're working with multiple QStash projects in the same app. - - The identifier of the event to notify. - Must match the `eventId` used in `context.waitForEvent`. - + - - Data to deliver to the waiting workflow(s). - This value will be returned in `eventData` from the corresponding `waitForEvent` call. - + ```typescript TypeScript + import { Client } from "@upstash/qstash"; + import { serve } from "@upstash/workflow/nextjs"; - - The workflow run ID to notify. When provided, enables **lookback functionality** - the notification will be stored and delivered even if `notify` is called before `waitForEvent`. + export const { POST } = serve( + async (context) => { ... }, + { + qstashClient: new Client({ token: "" }) + } + ); + ``` + + ```python Python + from qstash import AsyncQStash + + @serve.post("/api/example", qstash_client=AsyncQStash(os.environ["QSTASH_TOKEN"])) + async def example(context: AsyncWorkflowContext[str]) -> None: ... + + ``` + + - This solves race conditions where notifications might be sent before a workflow reaches its wait step. -## Response + -`context.notify()` returns a list of waiters describing the workflows that were notified. + The `Receiver` verifies that every request to your endpoint actually comes from QStash, blocking anyone else from triggering your workflow. - - A list of `NotifyResponse` objects describing each workflow that was waiting on the event. + The `receiver` option allows you to pass a QStash Receiver explicitly. - - - The ID of the notification message delivered to the workflow. - This is unique to every notification. - + By default, Workflow initializes the Receiver automatically using the environment variables `QSTASH_CURRENT_SIGNING_KEY` and `QSTASH_NEXT_SIGNING_KEY`. - - The unique identifier of the workflow run that was notified. - + This is useful if you're working with multiple QStash projects in the same app. - - Unix timestamp (in milliseconds) representing when the workflow was created. - + - - - - URL to call upon notify - - - Unix timestamp for when the wait will time out - - - Headers sent in case of notify - - - URL to call upon timeout - - - Body used in timeout request - - - Headers sent in case of time out - - - - - - + ```typescript TypeScript + import { Receiver } from "@upstash/qstash"; + import { serve } from "@upstash/workflow/nextjs"; -## Usage + export const { POST } = serve( + async (context) => { ... }, + { + receiver: new Receiver({ + currentSigningKey: "", + nextSigningKey: "", + }) + } + ); + ``` -### Basic Notification + ```python Python + from qstash import Receiver -```javascript -import { serve } from "@upstash/workflow/nextjs"; + @serve.post( + "/api/example", + receiver=Receiver( + current_signing_key=os.environ["QSTASH_CURRENT_SIGNING_KEY"], + next_signing_key=os.environ["QSTASH_NEXT_SIGNING_KEY"], + ), + ) + async def example(context: AsyncWorkflowContext[str]) -> None: + ... + ``` -export const { POST } = serve<{ topic: string }>(async (context) => { - const payload = context.requestPayload; + - const { - notifyResponse, // result of notify, which is a list of notified waiters - } = await context.notify("notify step", "my-event-Id", payload); -}); -``` + -### Notification with Lookback + -To prevent race conditions, you can provide a `workflowRunId`. This enables lookback - the notification will be stored and delivered even if sent before the target workflow reaches `waitForEvent`: +By default, Workflow uses `process.env` to read credentials and initialize QStash. +If you're in an environment where `process.env` isn't available, or you want to inject values manually, you can pass them with `env`. -```javascript -import { serve } from "@upstash/workflow/nextjs"; +Inside your workflow, these values are also exposed on `context.env`. -export const { POST } = serve<{ orderId: string }>(async (context) => { - const { orderId } = context.requestPayload; + - // Process payment - await context.run("process-payment", async () => { - return processPayment(orderId); - }); +```typescript TypeScript +import { Receiver } from "@upstash/qstash"; +import { serve } from "@upstash/workflow/nextjs"; - // Notify a specific workflow run with lookback support - const { - notifyResponse, - } = await context.notify( - "notify payment complete", - "payment-processed", - { orderId, status: "success" }, - "wfr_order_processor_123" // Enables lookback for this workflow run - ); -}); +export const { POST } = serve( + async (context) => { + // the env option will be available in the env field of the context: + const env = context.env; + }, + { + env: { + QSTASH_URL: "", + QSTASH_TOKEN: "", + QSTASH_CURRENT_SIGNING_KEY: "", + QSTASH_NEXT_SIGNING_KEY: "", + } + } +); ``` -# context.run -Source: https://upstash.com/docs/workflow/basics/context/run - -`context.run()` executes a piece of custom business logic as a workflow step. - -It returns a `Promise`, so you can decide how steps execute: -* **Sequentially** by awaiting them one by one. -* **In parallel** by awaiting multiple steps together. - -## Arguments +```python Python +@serve.post( + "/api/example", + env={ + "QSTASH_CURRENT_SIGNING_KEY": os.environ["QSTASH_CURRENT_SIGNING_KEY"], + "QSTASH_NEXT_SIGNING_KEY": os.environ["QSTASH_NEXT_SIGNING_KEY"], + }, +) +async def example(context: AsyncWorkflowContext[str]) -> None: + ... +``` - - A unique identifier for the step. - + - - The business logic to run inside this step. -## Response - -Each step can return a JSON-serializable value—anything from simple primitives to complex objects. - -The value is **JSON-serialized** and automatically restored across requests. - - - Avoid returning stateful resources such as database connections or file handles. - - Instead, return plain data (numbers, strings, arrays, objects) so the result can be safely persisted and restored across workflow executions. - - -## Usage - - - ```typescript Serial execution (TypeScript) highlight={6-8, 10-12} - import { serve } from "@upstash/workflow/nextjs"; - - export const { POST } = serve(async (context) => { - const input = context.requestPayload; - - const result1 = await context.run("step-1", async () => { - return someWork(input); - }); - - await context.run("step-2", async () => { - someOtherWork(result1); - }); - }); + - ``` + Enables verbose mode to print detailed logs of workflow execution to the application's `stdout`. - ```typescript Parallel execution (TypeScript) - import { serve } from "@upstash/workflow/nextjs" + Verbose mode is disabled by default. + ```typescript export const { POST } = serve( - async (context) => { - const input = context.requestPayload; - - const promise1 = context.run("step-1", async () => { - return someWork(input); - }); - - const promise2 = context.run("step-2", async () => { - return someOtherWork(input); - }); - - await Promise.all([promise1, promise2]); - }, + async (context) => { ... }, + { + verbose: true + } ); ``` - ```python Serial execution (Python) - from fastapi import FastAPI - from upstash_workflow.fastapi import Serve - from upstash_workflow import AsyncWorkflowContext + - app = FastAPI() - serve = Serve(app) + + Disables anonymous telemetry data collection for this workflow endpoint. Since we don't collect telemetry + in Python SDK, this option is only available in the TypeScript SDK. - @serve.post("/api/example") - async def example(context: AsyncWorkflowContext[str]) -> None: - input = context.request_payload + By default, the Upstash Workflow SDK collects anonymous telemetry data to help improve the service. + The collected data includes: - async def _step1(): - return some_work(input) + * SDK version + * Platform (Vercel, AWS, etc.) + * Runtime version (Node.js, Python, etc.) - result1 = await context.run("step-1", _step1) + Set `disableTelemetry` to `true` to opt out of telemetry for this specific workflow endpoint. - async def _step2(): - return some_other_work(result1) + - await context.run("step-2", _step2) + ```typescript TypeScript + export const { POST } = serve( + async (context) => { ... }, + { + disableTelemetry: true + } + ); + ``` - ``` - + ```python Python + @serve.post("/api/example", disable_telemetry=True) + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` + - - Because results are JSON-serialized, **class instances are restored as plain objects**. - This means instance methods will not be available unless you explicitly rehydrate the object. + + You should also + set [`disableTelemetry` when triggering workflow runs via `client.trigger()`](/docs/workflow/basics/client/trigger#param-disable-telemetry) to fully disable telemetry + + - To fix this, you can recreate the instance using Object.assign() or a custom factory: - ```typescript - export const { POST } = serve( - async (context) => { +# Changelog +Source: https://upstash.com/docs/workflow/changelog - let user = await context.run("step-1", async () => { - // 👇 Return a class instance from step - return new User("John Doe", "john.doe@example.com"); - }); + + We have moved the roadmap and the changelog to [Github Discussions](https://github.com/orgs/upstash/discussions) starting from October 2025.Now you can follow `In Progress` features. You can see that your `Feature Requests` are recorded. You can vote for them and comment your specific use-cases to shape the feature to your needs. + - // 👇 Properties are accessible by default - console.log(user.name) + +* **TypeScript SDK (`workflow-js`):** + * Multiple labels per workflow run are now supported. `label` on [`client.trigger`](/docs/workflow/basics/client/trigger) and `context.invoke` accepts `string | string[]`, and log/DLQ/cancel filters accept an array to match runs that have any of the given labels (OR semantics). Workflow run logs now expose a `labels: string[]` field, and `context.labels: string[]` replaces the now-deprecated `context.label`. + - // 👇 Create a Proper Instance with Object.assign() - user = Object.assign(new User(), user); + +* **TypeScript SDK (`workflow-js`):** + * Added optional `workflowRunId` parameter to `notify` method, enabling **lookback functionality**. When provided, notifications are stored and delivered even if sent before a workflow reaches `waitForEvent`, preventing race conditions. See [notify documentation](/docs/workflow/basics/client/notify) and [wait-for-event guide](/docs/workflow/features/wait-for-event#race-condition-between-wait-and-notify) for details. + - await context.run("greet", async () => { - // 👇 Now instance methods are available as well - console.log(user.greet()); - }); - } - ); - ``` - + +* **TypeScript SDK (`workflow-js`):** + * `Label` feature is added. This will enable our users to label their workflow runs so that + * Logs can be filtered with user given label. + * DLQ can be filtered with user given label. + * `notBefore` parameter is added to `trigger` function that will allow starting a workflow run at a later date + given by the `notBefore` parameter. +* **Console:** + * A major Workflow redesign is landed to improve debugging and monitoring experience workflow runs logs. + * `Flat view` is removed. All the data is moved to single view. This is also to avoid confusing our + users and made over all experience simpler. + -# context.sleep -Source: https://upstash.com/docs/workflow/basics/context/sleep + +* **TypeScript SDK (`workflow-js`):** + * Added `retryDelay` option to dynamicaly program the retry duration. It can be configured on + [trigger](/docs/workflow/basics/client#trigger-workflow) , [context.call](/docs/workflow/basics/context#context-call) + or [serve](/docs/workflow/basics/serve#retrydelay) + * Added ability to detect if a given url is a workflow or not. Starting with `0.2.17` trigger made via the sdk can fail (instead of hanging), + if there is no workflow serve on the given url. +* **Console:** + * Local mode is added to enable our users to use the console with their local development envrionment and the locally deployed workflows. + See [docs](/docs/workflow/howto/local-development#development-server-recommended) for details. + -`context.sleep()` pauses workflow execution for a specified duration. + +* **TypeScript SDK (`workflow-js`):** + * Restart/Resume for DLQ is added to allow more options to handle failed runs. See [here](/docs/workflow/howto/failures#manually-handling-failed-workflow-runs) + * Added `WorkflowNonRetryableError` to fail a workflow without causing any retries. See [here](/docs/workflow/basics/context#error-handling-and-retries) + * For additional bug fixes, see the full changelog [here](https://github.com/upstash/workflow-js/compare/v0.2.14...v0.2.16). + -When a workflow is paused, the current request completes and a new one is automatically scheduled to resume after the delay. -This ensures no compute resources are consumed during the sleep period. + +* **TypeScript SDK (`workflow-js`):** + * Added `useFailureFunction` and `failureFunction` to `client.trigger`. See [here](https://github.com/upstash/workflow-js/pull/107). + * Added batch triggering support to `client.trigger`. See [here](https://github.com/upstash/workflow-js/pull/110). + * For additional bug fixes, see the release notes [here](https://github.com/upstash/workflow-js/releases/tag/v0.2.14). +* **Python SDK (`workflow-py`):**   + * Failure function is implemented. This feature enables to act on a failure of a workflow on the code. See docs [here](/docs/workflow/howto/failures#using-a-failurefunction-recommended) + * For other bug fixes, see the full changelog [here](https://github.com/upstash/workflow-py/compare/v0.1.0...v0.1.1). +* **Console:** + * A major redesign is coming next month to improve Workflow usability. +* **Workflow Server:**   + * An issue causing Workflows not usable with `CloudFront` is fixed. + -Always `await` a `sleep` step to properly pause execution. + +* **TypeScript SDK (`workflow-js`):** + * Added a `workflow` parameter to `context.call`, enabling type-safe workflow calls. See [here](https://github.com/upstash/workflow-js/pull/75). + * Enabled passing `context.call` settings when defining an Agent. See [here](https://github.com/upstash/workflow-js/pull/90). + * Added `delay` support to `client.trigger`. See [here](https://github.com/upstash/workflow-js/pull/100). + * Introduced `period` and improved `rate` support in flow control. See [here](https://github.com/upstash/workflow-js/pull/101). + Previously, `period` was fixed at 1 second. For example, `rate: 3 period: 1d` throttles publishes to 3 per day. + * For additional bug fixes, see the release notes [here](https://github.com/upstash/workflow-js/releases/tag/v0.2.13). +* **Workflow Server:** + * Added support for custom `period` in flow control, allowing users to set a period of up to 1 week. + Previously, `period` was fixed at 1 second. For example, `rate: 3 period: 1d` throttles publishes to 3 per day. + * Implemented **Workflow Resume** and **Restart** features (SDK and Console support in progress): + * **Resume** allows users to retry a workflow run from the point it stopped. + * **Restart** allows users to retry a workflow run from the beginning. +* **Console:** + * A major redesign is coming to improve Workflow usability. + -## Arguments + +* **Python SDK (`workflow-py`):**   + * Minor bug fixes. + See the full changelog [here](https://github.com/upstash/workflow-py/compare/v0.1.0...v0.1.1). +* **Workflow Server:** + * Prevented intermediate Workflow calls from failing due to request/message quota limits. + * Fixed handling of `RUN_STARTED` so that it correctly returns unfinished Workflow Runs as documented. + Previously, some Workflow Runs could be skipped if internal state was logged after `RUN_STARTED`. + * Applied several performance optimizations. + - - A unique identifier for the step. - + +* **TypeScript SDK (`workflow-js`):** + * Added `onError` support to `serve` by the community. See [here](https://github.com/upstash/workflow-js/pull/79). + * Enabled support for all fetch-compatible models in Agents. See [more details here](https://github.com/upstash/workflow-js/pull/77). + * For additional bug fixes, see the full changelog [here](https://github.com/upstash/workflow-js/compare/v0.2.11...v0.2.12). + - - The duration to pause workflow execution. + +* **TypeScript SDK (`workflow-js`):** + * Fixed a Unicode issue in `context.call` where binary responses from endpoints could break. See [here](https://github.com/upstash/workflow-js/pull/71). + * Introduced `WorkflowTool`, allowing Workflow Agents to define multi-step workflows as a tool. See [here](/docs/workflow/agents/features#tools). + * Added `context.invoke` to call one workflow from another with full type-safety. See the guide [here](/docs/workflow/features/invoke). + * Introduced flow control parameters to limit the rate or concurrency of workflow runs. Learn more [here](/docs/workflow/features/flow-control). + * For additional bug fixes, see the full changelog [here](https://github.com/upstash/workflow-js/compare/v0.2.3...v0.2.6). +* **Workflow Server:** + * Added RateLimit and Parallelism controls to manage the frequency and concurrency of workflow runs. Learn more [here](/docs/workflow/features/flow-control). + - * **Human-readable string format:** + +* **TypeScript SDK (`workflow-js`):** + * Added the Agents API to workflows. You can now create AI agents to run workflows on your own infrastructure with all the benefits of workflows: reduced environment costs, fault tolerance, and scalability. Learn more about agents [here](/docs/workflow/agents/overview). + * For other bug fixes, see the full changelog [here](https://github.com/upstash/workflow-js/compare/v0.2.3...v0.2.6). +* **Python SDK (workflow-py):** + * Released [`workflow-py`](https://github.com/upstash/workflow-py). +* **Local Development Server:** + * The local development server is now available for public use. This server allows you to test your workflows locally. Learn more about the local development server [here](/docs/workflow/howto/local-development#development-server-recommended). +* **Console:** + * Separated Workflow and QStash consoles for a better user experience. + * Separated their DLQ messages as well. +* **Workflow Server:** + * The core team focused on RateLimit and Parallelism features. These features are ready on the server and will be announced next month after the documentation and SDKs are completed. + - | Input | Duration | - |---------|-------------| - | `"10s"` | 10 seconds | - | `"1m"` | 1 minute | - | `"30m"` | 30 minutes | - | `"2h"` | 2 hours | - | `"1d"` | 1 day | - | `"1w"` | 1 week | - | `"1mo"` | 1 month | - | `"1y"` | 1 year | + +* **TypeScript SDK (`workflow-js`):** + * Introduced third-party integrations, starting with Anthropic, Resend, and OpenAI. These integrations are automatically offloaded to workflows, ensuring long-running calls do not consume user environment time. See the related documentation [here](/docs/workflow/basics/context#context-api). + * Added a `timeout` parameter to `context.call`. Learn more in the [documentation](/docs/workflow/basics/context#context-call). + * Improved support for workflows in Express and SvelteKit by adding the `useJSONContent` option. + * Resolved loop detection issues on Cloudflare and Render. + * Full changelog, including all fixes, is available [here](https://github.com/upstash/workflow-js/compare/v0.2.0...v0.2.3). - * **Numeric format (seconds):** +* **Workflow Server:** + * Added the `WorkflowCreatedAt` filter for Dead Letter Queue (DLQ) and Events. + * Prepared the local development server for public release (coming soon). + * Enhanced `context.SleepUntil` to support float values. + * Increased the event retention period from 10,000 events to up to 14 days. Learn more on the [Pricing page](https://upstash.com/pricing/workflow). + - | Input | Duration | - |---------|---------------| - | `60` | 60 seconds (1 minute) | - | `3600` | 3600 seconds (1 hour) | - | `86400` | 86400 seconds (1 day) | - + +* **Python SDK (workflow-py):** + * Began development of the Python SDK. +* **TypeScript SDK (workflow-js):** + * Added support for string durations (e.g., `1d`, `30s`) in `context.sleep` and `context.waitForEvent`. + * Introduced integrations for [Astro](/docs/workflow/quickstarts/astro) and [Express](/docs/workflow/quickstarts/express). + * Added `client.trigger`, enabling workflows to start and return the workflow run ID. See the [documentation](/docs/workflow/basics/client#trigger-workflow). + * Added a retry option for `context.call`. See the [documentation](/docs/workflow/basics/context#context-call). + * Introduced a lazy fetch feature to support longer and larger workflows on resource-limited platforms. + * Added `context.cancel` to cancel the current workflow. See the [documentation](/docs/workflow/basics/context#context-cancel). + * Full changelog, including fixes, is available [here](https://github.com/upstash/workflow-js/compare/v0.1.2...v0.2.0). +* **Workflow Server:** + * Added bulk cancel functionality for workflow runs. See the [REST API](/docs/workflow/api-reference/runs/bulk-cancel-workflow-runs). + * Introduced content-based deduplication for workflows and retry-until-success functionality. This will allow workflows to be used in areas with unstable network connection. + -## Usage + +* Optimized the console by trimming event bodies, reducing resource usage and enabling efficient querying of events with large payloads. +* Began development on a new architecture to deliver faster event processing on the server. +* Added [Wait Notify](/docs/workflow/features/wait-for-event) feature. + - + +* Bug fixes and internal logging improvements. + -```typescript TypeScript highlight={12-13} -import { serve } from "@upstash/workflow/nextjs"; -import { signIn, sendEmail } from "@/utils/onboarding-utils"; + +* Released [Upstash Workflow](/docs/workflow/getstarted). + -export const { POST } = serve(async (context) => { - const userData = context.requestPayload; +- [AI Generation](https://upstash.com/docs/workflow/examples/allInOne.md) +- [Auth Provider Webhook](https://upstash.com/docs/workflow/examples/authWebhook.md) +- [Custom Retry Logic](https://upstash.com/docs/workflow/examples/customRetry.md) +- [Customer Onboarding](https://upstash.com/docs/workflow/examples/customerOnboarding.md) +- [Dynamic Workflows](https://upstash.com/docs/workflow/examples/dynamicWorkflow.md) +- [E-commerce Order Fulfillment](https://upstash.com/docs/workflow/examples/eCommerceOrderFulfillment.md) +- [Image Processing](https://upstash.com/docs/workflow/examples/imageProcessing.md) +- [Payment Retries](https://upstash.com/docs/workflow/examples/paymentRetry.md) +- [Waiting for Events](https://upstash.com/docs/workflow/examples/waitForEvent.md) - const user = await context.run("sign-in", async () => { - const signedInUser = await signIn(userData); - return signedInUser; - }); +# Overview +Source: https://upstash.com/docs/workflow/features/dlq - // 👇 Wait for one day (in seconds) - await context.sleep("wait-until-welcome-email", "1d"); +The Dead Letter Queue (DLQ) automatically captures failed workflow runs that have exhausted all retry attempts. - await context.run("send-welcome-email", async () => { - return sendEmail(user.name, user.email); - }); -}); -``` +This ensures that no workflow execution is lost and provides multiple options for recovering from failures gracefully. -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext -from onboarding_utils import sign_in, send_email +## How it works? -app = FastAPI() -serve = Serve(app) +When a workflow step fails and exhausts all configured retries, Upstash Workflow automatically moves the failed run to the DLQ. +This happens automatically without any additional configuration required. -@serve.post("/api/onboarding") -async def onboarding(context: AsyncWorkflowContext[User]) -> None: - user_data = context.request_payload + - async def _sign_in(): - return await sign_in(user_data) +The DLQ serves as a safety net, preserving failed workflow runs with their complete execution context. - user = await context.run("sign-in", _sign_in) + + Dead Letter Queue entries have retention period based on your pricing plan: + * **Free**: 3 days + * **Pay-as-you-go**: 1 week + * **Fixed pricing**: Up to 3 months - # 👇 Wait for one day (in seconds) - await context.sleep("wait-until-welcome-email", "1d") + After the retention duration expires, DLQ items are automatically removed and cannot be recovered. + - async def _send_email(): - return await send_email(user.name, user.email) +## Recovery Actions - await context.run("send-welcome-email", _send_email) +Once a workflow run is in the DLQ, you can take the following actions: -``` +* **[Restart](/docs/workflow/features/dlq/restart)** – trigger the workflow from the beginning. +* **[Resume](/docs/workflow/features/dlq/resume)** – continue the workflow from the point of failure. +* **[Re-run Failure Function](/docs/workflow/features/dlq/callback)** – execute the workflow's failure handling logic again. +* **[Delete](/docs/workflow/features/dlq/delete)** – remove the DLQ entry if no action is required. - +You can apply these actions in bulk to multiple DLQ entries. Check the individual action pages for more details. -# context.sleepUntil -Source: https://upstash.com/docs/workflow/basics/context/sleepUntil +# Rerun Failure Function +Source: https://upstash.com/docs/workflow/features/dlq/callback -`context.sleepUntil()` pauses workflow execution until a specific timestamp. +The **Rerun Failure Function** action allows you to retry the failure function that executes when a workflow run enters the Dead Letter Queue (DLQ). -When a workflow is paused, the current request completes and a new one is automatically scheduled to resume at the target time. -This ensures no compute resources are consumed while sleeping. +The failure function is typically a cleanup or notification operation that runs automatically whenever a workflow is moved to the DLQ. -Always await a `sleepUntil` step to properly pause execution. +This feature is particularly helpful for: -## Arguments +* Ensuring that important cleanup operations are executed. +* Guaranteeing that logging or alerting is completed after a workflow failure. +* Recovering from temporary errors in the failure function itself. - - A unique identifier for the step. - +By manually rerunning this function, you can ensure that critical operations—such as cleanup tasks, logging, or alerting—complete successfully even if the main workflow has failed. - - The target time when the workflow should resume. - Accepted formats: - * A **number**: Unix timestamp in seconds - * A **Date object** - * A **string** that can be parsed by `new Date(string)` in JavaScript - + -## Usage +You can perform this action programmatically as well: + ```typescript TypeScript + import { Client } from "@upstash/workflow"; -```typescript TypeScript highlight={11-16} -import { serve } from "@upstash/workflow/nextjs"; -import { signIn, sendEmail } from "@/utils/onboarding-utils"; + const client = new Client({ token: "" }); -export const { POST } = serve(async (context) => { - const userData = context.requestPayload; + await client.dlq.retryFailureFunction({ + dlqId: "dlq-12345", + }); + ``` + - const user = await context.run("sign-in", async () => { - return signIn(userData); - }); + + This action is only available if the failure function itself has failed as well. + If the failure function already succeeded, it cannot be rerun. - // 👇 Calculate the date for one week from now - const oneWeekFromNow = new Date(); - oneWeekFromNow.setDate(oneWeekFromNow.getDate() + 7); + You can view the status of the failure function in the **DLQ** and **Logs** dashboards, which indicate whether it succeeded or failed. + - // 👇 Sleep until the calculated date - await context.sleepUntil("wait-for-one-week", oneWeekFromNow); +# Delete +Source: https://upstash.com/docs/workflow/features/dlq/delete - await context.run("send-welcome-email", async () => { - return sendEmail(user.name, user.email); - }); -}); -``` +The **Delete** action allows you to remove failed workflow runs from the Dead Letter Queue when no recovery action is needed. -```python Python -from fastapi import FastAPI -from datetime import datetime, timedelta -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext -from onboarding_utils import sign_in, send_email +You can delete entries individually, in bulk by IDs, or by using filters such as label, URL, or date range. -app = FastAPI() -serve = Serve(app) + + ```typescript TypeScript + import { Client } from "@upstash/workflow"; -@serve.post("/api/onboarding") -async def onboarding(context: AsyncWorkflowContext[User]) -> None: - user_data = context.request_payload + const client = new Client({ token: "" }); - async def _sign_in(): - return await sign_in(user_data) + // delete a single entry + await client.dlq.delete("dlq-12345"); - user = await context.run("sign-in", _sign_in) + // delete multiple entries + await client.dlq.delete(["dlq-12345", "dlq-67890"]); - # 👇 Calculate the date for one week from now - one_week_from_now = datetime.now() + timedelta(days=7) + // delete by filters + await client.dlq.delete({ label: "my-label" }); - # 👇 Wait until the calculated date - await context.sleep_until("wait-for-one-week", one_week_from_now) + // delete all + await client.dlq.delete({ all: true }); + ``` + - async def _send_email(): - return await send_email(user.name, user.email) +For the full API reference, see [client.dlq.delete](/docs/workflow/basics/client/dlq/delete). - await context.run("send-welcome-email", _send_email) +# Restart +Source: https://upstash.com/docs/workflow/features/dlq/restart -``` +The **Restart** action allows you to re-execute a failed workflow run from the beginning. +All previous step results are discarded, and the workflow executes from scratch using the original configuration and initial payload. - +This approach is ideal when: -# context.waitForEvent -Source: https://upstash.com/docs/workflow/basics/context/waitForEvent +* Previous step results are no longer relevant. +* The failure was caused by corrupted or inconsistent state. +* You need a completely fresh execution with updated or clean data. -`context.waitForEvent` pauses workflow execution until a given event occurs or a timeout is reached. + -Default timeout value is 7 days. +You can perform this action programmatically as well: -## Arguments + + ```typescript TypeScript + import { Client } from "@upstash/workflow"; - - A unique identifier for the step. - + const client = new Client({ token: "" }); - - A unique identifier for the event to wait on. - + await client.dlq.restart({ + dlqId: "dlq-12345", + retries: 3, + }); + ``` + - - The maximum time to wait before continuing execution. +# Resume +Source: https://upstash.com/docs/workflow/features/dlq/resume - * **String format**: Human‑readable duration (e.g., `"10s"`, `"2h"`, `"1d"`). - * **Number format**: Duration in seconds (e.g., `60`, `3600`). +The **Resume** action allows you to continue a failed workflow run from the exact point of failure, preserving all successfully completed steps and their results. - Defaults to `7d` (7 days). - +This approach is ideal when: -## Response +* The workflow has long-running or resource-intensive steps that have already succeeded. +* You want to preserve progress and avoid re-executing successful operations. +* The failure was a temporary issue that can now be resolved. - - The data passed in when the event is triggered via `notify()`. - + - - `true` if execution resumed because the timeout elapsed, - `false` if resumed due to the event being received. - +You can perform this action programmatically as well: -## Usage + + ```typescript TypeScript + import { Client } from "@upstash/workflow"; -```javascript highlight={6-11} -import { serve } from "@upstash/workflow/nextjs"; + const client = new Client({ token: "" }); -export const { POST } = serve<{ topic: string }>(async (context) => { - const request = context.requestPayload; + await client.dlq.resume({ + dlqId: "dlq-12345", + retries: 3, + }); + ``` + - const { - eventData, - timeout, - } = await context.waitForEvent("wait for some event", "my-event-id", { - timeout: "1000s", // 1000 second timeout - }); -}); + + You can modify workflow code as long as changes occur **after** the failed steps. + Changes to steps prior to the failure are not allowed and may break the workflow. -``` + For more details, check out the [Handle workflow route code changes](/docs/workflow/howto/changes) page. + -# context.waitForWebhook -Source: https://upstash.com/docs/workflow/basics/context/waitForWebhook +# Overview +Source: https://upstash.com/docs/workflow/features/failure-callback -`context.waitForWebhook()` pauses workflow execution until the webhook created by `createWebhook` is called or a timeout is reached. +When you define a workflow endpoint, you can attach a failure function to the workflow that allows you to execute custom logic when a workflow run fails after exhausting all retry attempts. -You can call `context.waitForWebhook` with the same `webhook` object multiple times to wait for multiple calls to the same webhook URL. +This feature ensures that you can perform cleanup operations, logging, alerting, or any other custom error handling logic before the failed workflow run is moved to the Dead Letter Queue (DLQ). -## Arguments + - - Name of the step. - +The failure function automatically receives the workflow run context and the reason for the failure, so you can decide how to handle it. - - The webhook object returned by `context.createWebhook()`. + +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; - - - The webhook URL to wait for. - +export const { POST } = serve( + async (context) => { + // Your workflow logic... + }, + { + failureFunction: async ({ + context, + failStatus, + failResponse, + failHeaders, + }) => { - - The internal event identifier. - - - + // 👇 Log error to monitoring system + await logToSentry(...); - - The maximum time to wait before continuing execution. + // 👇 Send alert to team + await sendSlackAlert(...); - Should be passed in Human‑readable duration (e.g., `"10s"`, `"2h"`, `"1d"`). - + // 👇 Perform cleanup operations + await cleanupWorkflowResources(...); + }, + } +``` +); -## Response + -The response varies depending on whether the webhook was called before the timeout: +You cannot create new workflow steps inside the `failureFunction` using `context`. +The `context` provided here is only meant to expose workflow run properties (like URL, payload, and headers). +Think of the failure function as an individual `context.run` step. It executes once with the provided context but cannot define further steps. - - * `false` if the webhook was called successfully - * `true` if execution resumed because the timeout elapsed - + + If you use a custom authorization method to secure your workflow endpoint, add authorization to the `failureFunction` too. + Otherwise, anyone could invoke your failure function with a request. - - The HTTP request object received by the webhook (only present when `timeout` is `false`). + Read more here: [securing your workflow endpoint](/docs/workflow/howto/security). + - Contains the full request details including method, headers, body, and URL. - +## Parameters -## Usage +The `failureFunction` receives an object with the following parameters: -### Basic Example + + The workflow context object containing: -```typescript highlight={8-13} -import { serve } from "@upstash/workflow/nextjs"; + + + The ID of the failed workflow run + -export const { POST } = serve(async (context) => { - // Create webhook - const webhook = await context.createWebhook("create webhook"); + + The publicly accessible workflow endpoint URL + - // Wait for webhook to be called with 30 second timeout - const webhookResponse = await context.waitForWebhook( - "wait for webhook", - webhook, - "30s" - ); - - if (webhookResponse.timeout) { - console.log("Webhook was not called within the timeout period"); - } else { - console.log("Webhook was called successfully"); - console.log("Request body:", webhookResponse.request.body); - console.log("Request headers:", webhookResponse.request.headers); - } -}); -``` + + The original request payload that triggered the workflow + -For more complete examples and use cases, see [the page on webhooks](/docs/workflow/features/webhooks). + + The original request headers + -# How Workflow Works -Source: https://upstash.com/docs/workflow/basics/how + + Environment variables + + + -Upstash Workflow is an orchestration layer that allows you to write **multi‑step workflows** which are: + + The HTTP status code returned by the failed workflow step. + -* **Durable** – steps automatically recover from errors or outages -* **Scalable** – steps run independently and in parallel when possible -* **Cost‑efficient** – idle waiting (delays, sleeps, external calls) does not consume compute resources + + The response body returned by the failed workflow step. + -Upstash Workflow is built on top of Upstash QStash, our serverless messaging and scheduling solution, to achieve these features. + + The response headers returned by the failed workflow step. + -## The Core Idea +# Advanced failureUrl Option +Source: https://upstash.com/docs/workflow/features/failureFunction/advanced -Traditionally, backend functions are built in one of two ways: either everything is executed inside a single API function—which is difficult to maintain and prone to failures—or the flow is split across multiple APIs connected by a queueing system, which adds significant infrastructure and state‑management overhead. +The `failureUrl` is an advanced option that sends failure callback to a different endpoint rather than to the workflow endpoint (failure function). +This approach is useful for handling failures on separate infrastructure. -These approaches can work, but they often fail to handle production load reliably or become increasingly difficult to maintain over time: +You can use either `failureFunction` or `failureUrl`, but not both. These options are mutually exclusive. -* **Timeouts** – the whole function runs inside one execution window. A slow API can easily exceed serverless limits (often 10–60 seconds). -* **Temporary issues** – slow or unreliable external services can exceed serverless limits or cause the entire request to fail. -* **Failures** – if a step fails, the whole request fails. You either restart everything or you must write custom retry logic. -* **Rate limits** – calling external APIs in bulk requires careful concurrency control, which is difficult to implement manually. -* **Complexity** – to address these issues, teams often build custom queues, schedulers, or state trackers, adding unnecessary infrastructure overhead. +For most users, **Failure Function** is the better choice because: +* It runs alongside your workflow and has access to the same context and dependencies +* Failure function requests are automatically retried on failure as well. +* You can manually retry failure function if it fails via DLQ. -*** +If you think this advanced option fits your need, you can configure it by passing `failureUrl` configuration. -## How Upstash Workflow Solves This + + ```typescript + import { Client } from "@upstash/workflow"; -Upstash Workflow takes a different approach: -instead of treating your entire function as one continuous execution, **it splits your logic into multiple steps in a workflow endpoint**, each managed and retried by the orchestration engine. + const client = new Client({ token: "" }) -* Each step is executed in its own **HTTP call** to your application. -* After a step finishes, its result is **stored in durable state** inside Upstash Workflow. -* On the next execution, Workflow **skips completed steps** and **resumes exactly where it left off by restoring the previous step results**. -* If a step fails, it is retried automatically based on your retry configuration. + const { workflowRunId } = await client.trigger({ + url: "https:///workflow" + failureUrl: "https:///workflow-failure" + }) + ``` -This means you no longer need custom queues, retry logic, or manual state management. You just define your workflow once, and the orchestration layer ensures that **every step runs once, in order, with full reliability.** + ```python Python + @serve.post("/api/example", failure_url="https:///workflow-failure") + async def example(context: AsyncWorkflowContext[str]) -> None: + # Your workflow logic... + pass + ``` + - +# Reliability of Failure Function +Source: https://upstash.com/docs/workflow/features/failureFunction/reliability -*** +The failure function is executed whenever a workflow run fails. -## Extended Features +In some cases, the failure function itself may throw an error. +When this happens, it will be retried according to the workflow run's retry configuration. +If all retry attempts also fail, the failure function execution is marked as failed. -Upstash Workflow extends the basic step model with additional primitives: +You can view and filter workflow runs with failed failure function executions in the DLQ dashboard. -* **Parallel Steps** - Define multiple steps (e.g. inside a `Promise.all()`). The engine detects independent work and runs steps concurrently as separate HTTP executions. + -* **Delays / Sleep** - `context.sleep` and `context.sleepUntil` allow pausing a workflow for hours, days, or even months. No compute is held during the wait time; execution resumes when the delay has expired. +From the DLQ dashboard, you can retry the failure function. -* **External Event Handling** - `context.waitForEvent` pauses execution until you notify the workflow externally (e.g. via webhook or user action). State is persisted until the event arrives. + -* **External Calls** - Use `context.call` to have Upstash perform slow or unreliable HTTP calls. Instead of blocking your function, the call is handled by Upstash. When it completes, the workflow resumes with the response. +You can perform this action programmatically as well: -*** +```ts +import { Client } from "@upstash/workflow"; -This architecture makes your serverless functions durable, reliable, and performance‑optimized, even in the face of runtime errors or temporary service outages. +const client = new Client({ token: "" }); -It's quick and easy to get started: follow the [Quickstarts](/docs/workflow/quickstarts/platforms) to define your first workflow in minutes. +const response = await client.dlq.retryFailureFunction({ + dlqId: "dlq-12345" // The ID of the DLQ message to retry +}); +``` # Overview -Source: https://upstash.com/docs/workflow/basics/serve +Source: https://upstash.com/docs/workflow/features/flow-control -Use the `serve()` function to define an endpoint that runs a workflow. -It accepts two arguments: +Flow Control allows you to limit how many workflow steps are executed by delaying and queuing their delivery. -1. **Route Function**: an async function that receives the workflow context and defines the workflow steps. -2. **Options**: configuration options for the workflow. +This feature helps to: +* Manage resource consumption +* Prevent violations of external API rate limits +* Ensure workflows run within defined system constraints - - ```typescript TypeScript - import { serve } from "@upstash/workflow/nextjs"; +## How Flow Control Works - export const { POST } = serve(async (context) => { - // Route function - }, { - // Options - }); - ``` +When defined limits are exceeded, Flow Control automatically queues and delays step executions instead of rejecting them. +This guarantees that all steps are eventually processed while staying within configured thresholds. - ```python Python - from fastapi import FastAPI - from upstash_workflow.fastapi import Serve - from upstash_workflow import AsyncWorkflowContext +To configure Flow Control, you define a flow control key, a unique identifier used to group related steps under the same rate and parallelism limits. +The steps that has the same flow control key respect the same constraints. - app = FastAPI() - serve = Serve(app) +There are two main parameters to configure: - @serve.post("/api/example") - async def example(context: AsyncWorkflowContext[str]) -> None: - async def _step1() -> str: - # define a piece of business logic as step 1 - return "step 1 result" +* [Rate and Period](/docs/workflow/features/flow-control/rate-period): Maximum number of steps that may start within a time window +* [Parallelism](/docs/workflow/features/flow-control/parallelism): Maximum number of steps allowed to run concurrently - result = await context.run("step-1", _step1) +These parameters can be combined for fine‑grained control. +For example, you can allow up to 10 steps per minute but restrict concurrency +to 5 steps in parallel, ensuring more predictable load patterns. - async def _step2() -> None: - # define another piece of business logic as step 2 - pass +## Example - await context.run("step-2", _step2) - ``` - +Suppose you have the following workflow: -## Route Function +```typescript +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload -The route function defines the execution logic of the workflow. -It is an async function that receives a context object, which is automatically created and passed by Upstash Workflow. + await context.run("step-1", () => { ... }); -The context object provides: -* **Workflow APIs** – functions for defining workflow steps. -* **Workflow Run Properties** – request payload, request headers, and other metadata. + await context.run("step-2", () => { ... }); -For a full list of available APIs and properties, see the [Workflow Context](/docs/workflow/basics/context) documentation. + await context.run("step-3", () => { ... }); +}) +``` - - ```typescript TypeScript highlight={4-9} - import { serve } from "@upstash/workflow/nextjs"; +Now imagine you trigger **N workflow runs** for this workflow with the following configuration: - export const { POST } = serve( - async (context) => { - // 👇 Access context properties - const { userId } = context.requestPayload; - // 👇 Define a workflow step - await context.run("step-1", async () => {}) - } - ); - ``` +```typescript +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "fw_example", + parallelism: 7, + rate: 3, + period: "1m", + } +}) +``` - ```python Python - from fastapi import FastAPI - from upstash_workflow.fastapi import Serve - from upstash_workflow import AsyncWorkflowContext +Without Flow Control, all workflow runs immediately execute their steps as soon as possible. +If the workflow calls an external API in a step, this would likely result in ~N concurrent requests being fired in a very short timeframe, potentially overloading services or breaching API limits. - app = FastAPI() - serve = Serve(app) + - @serve.post("/api/example") - async def example(context: AsyncWorkflowContext[str]) -> None: - async def _step1() -> str: - # define a piece of business logic as step 1 - return "step 1 result" +With the configuration above: +* **Rate:** At most 3 steps per minute can start across all workflow runs. +* **Parallelism:** At most 7 steps can be running at the same time. - result = await context.run("step-1", _step1) +Steps that exceed these limits are automatically queued and executed later. - async def _step2() -> None: - # define another piece of business logic as step 2 - pass + - await context.run("step-2", _step2) +Note that each step above corresponds to a separate workflow run. +Because this workflow is sequential, each workflow run has only one pending step at a time. +In workflows with **parallel branches**, multiple steps from the same workflow run may appear in the schedule simultaneously. - ``` - +Parallelism slots are consumed by running steps. +If no slots are available, new steps enter the **waitlist** until resources free up: -## Options + -Options provide additional configuration for workflow runs. -Most of them are advanced settings and are not required for typical use cases. See [Advanced Options](/docs/workflow/basics/serve/advanced) for more details. + +Upstash Workflow does not support per-step level configuration. Meaning that you can attach a flow-control configuration +to the workflow run and all the steps will inherit to the same limits. +Following the analogy above, you cannot enforce parallelism limit on "green" steps natively. - +The context.call and context.invoke steps are exception this to this rule and accept their own flow control configuration: - ```typescript TypeScript highlight={5-8} - import { serve } from "@upstash/workflow/nextjs"; +* [context.call](/docs/workflow/basics/context/call) – lets you run external HTTP requests under a separate key, so you can throttle third‑party API calls independently of your workflow logic. +* [context.invoke](/docs/workflow/basics/context/invoke) – starts a new workflow run with its own flow control configuration. This allows the invoked workflow to run under different limits than the parent workflow, giving you more precise control. - export const { POST } = serve( - async (context) => { ... }, - // 👇 Workflow options - { - failureFunction: async ({ ... }) => {} - } - ); - ``` +If you want to throttle a specific `context.run` step, the recommended approach is to **extract it into a separate workflow** and call it using `context.invoke()` with its own flow control configuration with a stricter limits. + - ```python Python - from fastapi import FastAPI - from upstash_workflow.fastapi import Serve - from upstash_workflow import AsyncWorkflowContext +## Configuration - app = FastAPI() - serve = Serve(app) +You can configure flow control when starting a workflow run: - @serve.post("/api/example") - async def example(context: AsyncWorkflowContext[str]) -> None: - async def _step1() -> str: - # define a piece of business logic as step 1 - return "step 1 result" +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; - result = await context.run("step-1", _step1) +const client = new Client({ token: "" }) - async def _step2() -> None: - # define another piece of business logic as step 2 - pass +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "user-signup", + parallelism: 1, + rate: 10, + period: 100, + } +}) +``` - await context.run("step-2", _step2) +All steps within a workflow run will adhere to the specified flow control configuration. - ``` - + +Keep in mind that rate/period and parallelism info are kept on each step separately. +If you change the rate/period or parallelism on a new deployment, the old fired ones will not be affected. +They will keep their flow control configuration. -# Advanced Options -Source: https://upstash.com/docs/workflow/basics/serve/advanced +During the period that old steps have not been delivered but there are also steps with new rates, Upstash Workflow will effectively allow the highest rate/period or highest parallelism. Eventually (after the old publishes are delivered), the new rate/period and parallelism will be used. + -Advanced Options are intended to support edge cases or testing pipelines and are **not required for regular use**. +# Monitor & Manage +Source: https://upstash.com/docs/workflow/features/flow-control/monitor - - Defines a function that executes if the workflow fails after all retries are exhausted. +You can monitor wait list size of your flow control key's from the console `FlowControl` tab. The console also allows you to pin, unpin, and reset rate for flow control keys directly. - For details, see [failureFunction](/docs/workflow/features/failure-callback). + - - ```typescript TypeScript - export const { POST } = serve( - async (context) => { ... }, - { - failureFunction: async ({ - context, // context during failure - failStatus, // failure status - failResponse, // failure message - failHeaders, // failure headers - failStack // failure stack trace (if available) - }) => { - // handle the failure - } - } - ); - ``` +Also you can get the same info using the REST API. +* [List All Flow Control Keys](/docs/workflow/api-reference/flow-control/list-flow-control-keys). +* [Single Flow Control Key](/docs/workflow/api-reference/flow-control/get-flow-control-key). +* [Global Parallelism](/docs/workflow/api-reference/flow-control/get-global-parallelism). - ```python Python - async def failure_function( - context, # context during failure - fail_status, # failure status - fail_response, # failure message - fail_headers # failure headers - ): - # handle the failure - pass +## Manage - @serve.post("/api/example", failure_function=failure_function) - async def example(context: AsyncWorkflowContext[str]) -> None: ... - ``` +You can manage flow control keys by pausing/resuming delivery, pinning/unpinning configurations, and resetting rate counts. - - +These operations are available via the SDK, REST API, or directly from the **Flow Control** tab in the [Upstash Console](https://console.upstash.com). - +Since Upstash Workflow uses QStash under the hood, flow control management is done through the QStash client. See the [QStash Flow Control Management API](/docs/qstash/features/flowcontrol#management-api) for full details and code examples. - - This parameter is only available in Python SDK. In Javascript SDK, you can pass this value when triggering the workflow. - +# Parallelism +Source: https://upstash.com/docs/workflow/features/flow-control/parallelism - The `failureUrl` option defines an external endpoint that will be called if the workflow fails after all retries are exhausted. +The parallelism limit controls the maximum number of calls that can be executed concurrently. +Unlike rate limiting (which works per time window), parallelism enforces concurrency control with a token-based system. - This option is an advanced alternative to `failureFunction`. - For more details, see [Advanced failureUrl Option](/docs/workflow/features/failureFunction/advanced). +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; - +const client = new Client({ token: "" }) - ```python Python - @serve.post("/api/example", failureUrl="https:///...") - async def example(context: AsyncWorkflowContext[str]) -> None: ... - ``` +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "user-signup", + parallelism: 10, + } +}) +``` - - +**Example**: +If `parallelism = 3`, at most 3 requests can run concurrently. - +When tokens are available, requests acquire one and start execution: + - - This parameter is only available in Python SDK. In Javascript SDK, you can pass this value when triggering the workflow. - +When all tokens are in use, additional requests are not failed — they’re queued in a **waitlist**: + - Defines the number of retry attempts if a workflow step fails. - The default value is 3. +The step in the waitlist will wait for a step to complete and hand off it's token to a pending request: - For details, see [retry configuration](/docs/workflow/features/retries#configuration). + + Token handoff does not guarantee strict ordering. + A later request in the waitlist may acquire a token before an earlier one. + - + - ```python Python - @serve.post("/api/example", retries=3) - async def example(context: AsyncWorkflowContext[str]) -> None: ... - ``` - - +# Rate and Period +Source: https://upstash.com/docs/workflow/features/flow-control/rate-period - - An array of middleware instances that intercept workflow lifecycle and debug events. +The rate specifies the maximum number of requests allowed in a given period (time window). - Middlewares allow you to hook into various stages of workflow execution (before/after steps, run start/completion) - and debug events (errors, warnings, info logs). +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; - For details and examples, see [Middlewares](/docs/workflow/howto/middlewares). +const client = new Client({ token: "" }) - +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "user-signup", + rate: 10, + period: 100, + } +}) +``` - ```typescript TypeScript - import { serve } from "@upstash/workflow/nextjs"; - import { loggingMiddleware } from "@upstash/workflow"; +**Example**: +If `rate = 2` and `period = 1 minute`, then **a maximum of 2 steps** can be executed per minute. - export const { POST } = serve( - async (context) => { ... }, - { - middlewares: [loggingMiddleware] - } - ); - ``` +The first 2 requests within the minute are executed immediately: - - + - - Enables custom parsing of the initial request payload. +The 3rd request in the same minute is not executed immediately: - Use this option if the incoming payload is not plain JSON or a simple string. - The parser function lets you transform the raw request into a strongly typed - object before workflow execution begins. + - +Instead of rejecting it, Workflow schedules the request in the next available time window: - ```typescript TypeScript - type InitialPayload = { - foo: string; - bar: number; - }; + - // 👇 1: provide initial payload type - export const { POST } = serve( - async (context) => { - // 👇 3: parsing result is available as requestPayload - const payload: InitialPayload = context.requestPayload; - }, - { - // 👇 2: custom parsing for initial payload - initialPayloadParser: (initialPayload) => { - const payload: InitialPayload = parsePayload(initialPayload); - return payload; - }, - } - ); - ``` +Note that step executions may take longer than the defined period. +The rate limit only controls how many steps are **started** within each time window, +it does not limit their execution duration. - ```python Python - @dataclass - class InitialPayload: - foo: str - bar: int +# Overview +Source: https://upstash.com/docs/workflow/features/invoke - def initial_payload_parser(initial_payload: str) -> InitialPayload: - return parse_payload(initial_payload) +You can start another workflow run inside a workflow and await its execution to complete. +This allows to orchestrate multiple workflows together without external synchronization. - @serve.post("/api/example", initial_payload_parser=initial_payload_parser) - async def example(context: AsyncWorkflowContext[InitialPayload]) -> None: - payload: InitialPayload = context.request_payload +When you use `context.invoke`, invoking workflow will wait until the invoked workflow finishes before running the next step. - ``` +```typescript +const { + body, // response from the invoked workflow + isFailed, // whether the invoked workflow was canceled + isCanceled // whether the invoked workflow failed +} = await context.invoke( + "analyze-content", + { + workflow: analyzeContent, + body: "test", + header: {...}, // headers to pass to anotherWorkflow (optional) + retries, // number of retries (optional, default: 3) + flowControl, // flow control settings (optional) + workflowRunId // workflowRunId to set (optional) + } +) +``` - +You can return a response from a workflow, which will be delivered to invoker workflow run. - + - - Alternative to `initialPayloadParser`, you can pass a `schema` in the TypeScript SDK. + + You cannot create an infinite chain of workflow invocations. If you set up an 'invoke loop' where workflows continuously invoke each other, the process will fail once it reaches a depth of 100. + - The schema is used to validate and parse the initial request payload automatically using [Zod](https://zod.dev/). +# Using Serve Many +Source: https://upstash.com/docs/workflow/features/invoke/serveMany - +Normally, workflows are created with `serve()`, which exposes each workflow as its own HTTP endpoint. +If workflows were invoked only by their full URL, it would mean: - ```typescript TypeScript +* You'd have to provide the URL explicitly like a trigger request +* You'd lose type safety for request and response payloads - import { z } from "zod"; +To avoid these issues, Upstash Workflow lets you define workflows as objects and expose them under the same parent path. +This way, you can invoke a workflow simply by passing the object to `context.invoke`, with full type safety and no URLs required. - const parameters = z.object({ expression: z.string() }); + + + Use `createWorkflow()` to define workflows as objects. - export const { POST } = serve( - async (context) => { - // context.requestPayload is typed as `{ expression: string }` - const payload = context.requestPayload; - }, - { - schema: parameters, + It works just like `serve()`—accepting the same arguments—but does **not** expose the workflow directly as an HTTP endpoint. + Instead, it simply initializes a workflow object. + + ```typescript + const workflowOne = createWorkflow( + // 👇 Request Payload Type + async (context: WorkflowContext) => { + + await context.sleep("wait 1 second", 1) + + // 👇 Workflow Response Type + return { message: "This is the data returned by the workflow" }; } ); - ``` - - + const workflowTwo = createWorkflow(async (context) => { + // 👇 Invoke the workflow with type-safe call + const { body } = await context.invoke( + "invoke workflowOne", + { + workflow: workflowOne, + body: "user-1" + } + ), + }); + ``` + + + Use `serveMany()` instead of `serve()` to expose multiple workflows on a single catch‑all route. - - Specifies the full endpoint URL of the workflow, including the route path. + If one workflow is going to invoke another, both must be included in the same `serveMany` definition. + First step of using `serveMany` is to define a catch-all route. - By default, Upstash Workflow infers the URL from `request.url` when scheduling the next step. - However, in some environments, `request.url` may resolve to an internal or unreachable address. + ```typescript app/serve-many/[...any]/route.ts + export const { POST } = serveMany( + { + "workflow-one-route": workflowOne, + "workflow-two-route": workflowTwo, + } + ) + ``` - Use this option when running behind a proxy, reverse proxy, or local tunnel during development where `request.url` cannot be used directly. + + In Next.js, a catch‑all route can be defined by creating a `route.ts` file inside a directory named with `[...]`, for example: `app/serve-many/[...any]/route.ts`. - + For implementations of `serveMany` in other frameworks, you can refer to the projects available in the [`examples` directory of the workflow-js repository](https://github.com/upstash/workflow-js/tree/main/examples). + + + + When invoking, pass the workflow object created with `createWorkflow()` (from step 1) as the argument to `context.invoke()`. + This removes the need to specify a URL explicitly and ensures the call is + fully type‑safe. - ```typescript TypeScript - export const { POST } = serve( - async (context) => { ... }, - { - url: "https://.com/api/workflow" - } - ); - ``` + ```ts + const workflowTwo = createWorkflow(async (context) => { + // 👇 Invoke the workflow with type-safe call + const { body } = await context.invoke( + "invoke workflowOne", + { + // 👇 Pass the workflow object as argument + workflow: workflowOne, + body: "user-1" + } + ), + }); + ``` - ```python Python - @serve.post("/api/example", url="https://.com/api/workflow") - async def example(context: AsyncWorkflowContext[str]) -> None: ... - ``` - - + - + + In this example, both `workflowOne` and `workflowTwo` are exposed through `serveMany`, sharing the same parent path. - Similar to `url`, but `baseUrl` only overrides the base portion of the inferred URL rather than replacing the entire path. - This is useful when you want to preserve the route structure while changing only the host or scheme. + You can start `workflowOne` by sending a trigger request to: + `https://your-app/serve-many/workflow-one-route`. - - If you have multiple workflow endpoints, you can set the `UPSTASH_WORKFLOW_URL` environment variable instead of configuring `baseUrl` on each endpoint. - The `UPSTASH_WORKFLOW_URL` environment variable corresponds directly to this option and configures it globally. - + ```typescript + import { Client } from "@upstash/workflow"; - + const client = new Client({ token: "" }) - ```typescript TypeScript - export const { POST } = serve( - async (context) => { - ... - }, - // options: - { - baseUrl: "" - } - ); - ``` + const { workflowRunId } = await client.trigger({ + // 👇 URL of workflow one + url: "https://your-app/serve-many/workflow-one-route" + }) + ``` - ```python Python - @serve.post("/api/example", base_url="") - async def example(context: AsyncWorkflowContext[str]) -> None: ... + Route names are inferred from the keys you pass to `serveMany`. + For example, you can start `workflowTwo` by sending trigger request to: `https://your-app/serve-many/workflow-two-route`. + + - ``` +# Notify +Source: https://upstash.com/docs/workflow/features/notify - - +You can notify all the workflow runs waitingi for a specific event ID. +There are two ways to send a notify request. - +## Notify within Workflow - Use `qstashClient` if you want to provide your own QStash client instead of letting Workflow use the default from environment variables. +Notifies other workflows waiting for a specific event from within a workflow. - This is useful if you're working with multiple QStash projects in the same app. + +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; - +export const { POST } = serve(async (context) => { + const { orderId, processingResult } = context.requestPayload; - ```typescript TypeScript - import { Client } from "@upstash/qstash"; - import { serve } from "@upstash/workflow/nextjs"; + await context.run("process-order", async () => { + // ... + }) - export const { POST } = serve( - async (context) => { ... }, - { - qstashClient: new Client({ token: "" }) - } - ); - ``` + const { notifyResponse } = await context.notify( + "notify-processing-complete", + `order-${orderId}`, + { + orderId, + status: "completed", + result: processingResult, + completedAt: new Date().toISOString() + } + ); - ```python Python - from qstash import AsyncQStash +}); +``` - @serve.post("/api/example", qstash_client=AsyncQStash(os.environ["QSTASH_TOKEN"])) - async def example(context: AsyncWorkflowContext[str]) -> None: ... +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext +from datetime import datetime - ``` +app = FastAPI() +serve = Serve(app) - +@serve.post("/api/order-processor") +async def order_processor(context: AsyncWorkflowContext[str]) -> None: + order_id = context.request_payload["order_id"] + processing_result = context.request_payload["processing_result"] - + # Process the order + async def _process_order(): + return await process_order(order_id) - + result = await context.run("process-order", _process_order) - The `Receiver` verifies that every request to your endpoint actually comes from QStash, blocking anyone else from triggering your workflow. + # Notify waiting workflows that processing is complete + notify_response = await context.notify( + "notify-processing-complete", + f"order-{order_id}", + { + "order_id": order_id, + "status": "completed", + "result": processing_result, + "completed_at": datetime.utcnow().isoformat() + } + ) - The `receiver` option allows you to pass a QStash Receiver explicitly. + # Log notification results + async def _log_notification(): + print(f"Notified {len(notify_response)} waiting workflows") + return notify_response - By default, Workflow initializes the Receiver automatically using the environment variables `QSTASH_CURRENT_SIGNING_KEY` and `QSTASH_NEXT_SIGNING_KEY`. + await context.run("log-notification", _log_notification) +``` + - This is useful if you're working with multiple QStash projects in the same app. +## External Notification - +You can also notify workflows from external systems using the Workflow Client: - ```typescript TypeScript - import { Receiver } from "@upstash/qstash"; - import { serve } from "@upstash/workflow/nextjs"; + +```typescript TypeScript +import { Client } from "@upstash/workflow"; - export const { POST } = serve( - async (context) => { ... }, - { - receiver: new Receiver({ - currentSigningKey: "", - nextSigningKey: "", - }) - } - ); - ``` +const client = new Client({ token: "" }); - ```python Python - from qstash import Receiver +await client.notify({ + eventId: "order-completed-123", + eventData: { + orderId: "123", + status: "completed", + deliveryTime: "2 days", + trackingNumber: "TRK123456" + } +}); +``` - @serve.post( - "/api/example", - receiver=Receiver( - current_signing_key=os.environ["QSTASH_CURRENT_SIGNING_KEY"], - next_signing_key=os.environ["QSTASH_NEXT_SIGNING_KEY"], - ), - ) - async def example(context: AsyncWorkflowContext[str]) -> None: - ... - ``` +```python Python +from upstash_workflow import Client - +client = Client("") - +# Notify workflows waiting for a specific event +await client.notify( + event_id="order-completed-123", + event_data={ + "order_id": "123", + "status": "completed", + "delivery_time": "2 days", + "tracking_number": "TRK123456" + } +) +``` + - +## Lookback Functionality -By default, Workflow uses `process.env` to read credentials and initialize QStash. -If you're in an environment where `process.env` isn't available, or you want to inject values manually, you can pass them with `env`. +By default, if you call `notify` before a workflow reaches its `waitForEvent` step, the notification will be lost (race condition). To prevent this, you can provide a `workflowRunId` parameter which enables **lookback** - the notification will be stored and delivered even if sent before the wait step. -Inside your workflow, these values are also exposed on `context.env`. +This is particularly useful when: +* You trigger a workflow and immediately want to send it an event +* You have concurrent operations where timing is unpredictable +* You want to eliminate race conditions in your event-driven workflows - ```typescript TypeScript -import { Receiver } from "@upstash/qstash"; -import { serve } from "@upstash/workflow/nextjs"; +import { Client } from "@upstash/workflow"; -export const { POST } = serve( - async (context) => { - // the env option will be available in the env field of the context: - const env = context.env; - }, - { - env: { - QSTASH_URL: "", - QSTASH_TOKEN: "", - QSTASH_CURRENT_SIGNING_KEY: "", - QSTASH_NEXT_SIGNING_KEY: "", - } - } -); +const client = new Client({ token: "" }); + +// Trigger a workflow and get its run ID +const { workflowRunId } = await client.trigger({ + url: "https://your-app.com/api/process-order", + body: { orderId: "123" } +}); + +// Immediately notify it with lookback enabled +// The notification will be delivered even if the workflow +// hasn't reached waitForEvent yet +await client.notify({ + eventId: "payment-verified", + eventData: { verified: true, amount: 100 }, + workflowRunId: workflowRunId, // Enables lookback +}); ``` ```python Python -@serve.post( - "/api/example", - env={ - "QSTASH_CURRENT_SIGNING_KEY": os.environ["QSTASH_CURRENT_SIGNING_KEY"], - "QSTASH_NEXT_SIGNING_KEY": os.environ["QSTASH_NEXT_SIGNING_KEY"], - }, +from upstash_workflow import Client + +client = Client("") + +# Trigger a workflow and get its run ID +workflow_run = await client.trigger( + url="https://your-app.com/api/process-order", + body={"order_id": "123"} ) -async def example(context: AsyncWorkflowContext[str]) -> None: - ... -``` +# Immediately notify it with lookback enabled +await client.notify( + event_id="payment-verified", + event_data={"verified": True, "amount": 100}, + workflow_run_id=workflow_run.workflow_run_id # Enables lookback +) +``` - +The same also applies to `context.notify` - + +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; - Enables verbose mode to print detailed logs of workflow execution to the application's `stdout`. +export const { POST } = serve(async (context) => { + const { orderId, processingResult } = context.requestPayload; - Verbose mode is disabled by default. + await context.run("process-order", async () => { + // ... + }) - ```typescript - export const { POST } = serve( - async (context) => { ... }, - { - verbose: true - } - ); - ``` + const { notifyResponse } = await context.notify( + "notify-processing-complete", + `order-${orderId}`, + { + orderId, + status: "completed", + result: processingResult, + completedAt: new Date().toISOString(), + workflowRunId: "targetWorkflowRunId" // Enables lookback + } + ); - +}); +``` + - - Disables anonymous telemetry data collection for this workflow endpoint. Since we don't collect telemetry - in Python SDK, this option is only available in the TypeScript SDK. + +When using lookback with `workflowRunId`, the notification is targeted to a specific workflow run rather than all waiters with that event ID. + - By default, the Upstash Workflow SDK collects anonymous telemetry data to help improve the service. - The collected data includes: +# Parallel Steps +Source: https://upstash.com/docs/workflow/features/parallel-steps - * SDK version - * Platform (Vercel, AWS, etc.) - * Runtime version (Node.js, Python, etc.) +Upstash Workflow supports executing multiple steps in parallel. - Set `disableTelemetry` to `true` to opt out of telemetry for this specific workflow endpoint. +Since each step returns a `Promise`, you can execute multiple steps concurrently by using `Promise.all()`. +This behavior works out of the box. No additional configuration is required. - +```typescript app/api/workflow/route.ts +import { serve } from "@upstash/workflow/nextjs"; +import { checkInventory, brewCoffee, printReceipt } from "@/utils"; - ```typescript TypeScript - export const { POST } = serve( - async (context) => { ... }, - { - disableTelemetry: true - } - ); - ``` +export const { POST } = serve(async (context) => { - ```python Python - @serve.post("/api/example", disable_telemetry=True) - async def example(context: AsyncWorkflowContext[str]) -> None: ... - ``` - + // 👇 Execute steps in parallel + const [coffeeBeansAvailable, cupsAvailable, milkAvailable] = + await Promise.all([ + context.run("check-coffee-beans", () => checkInventory("coffee-beans")), + context.run("check-cups", () => checkInventory("cups")), + context.run("check-milk", () => checkInventory("milk")), + ]); - - You should also - set [`disableTelemetry` when triggering workflow runs via `client.trigger()`](/docs/workflow/basics/client/trigger#param-disable-telemetry) to fully disable telemetry - - +}); +``` -# Changelog -Source: https://upstash.com/docs/workflow/changelog +The results of the parallel steps are available as usual once awaited. - - We have moved the roadmap and the changelog to [Github Discussions](https://github.com/orgs/upstash/discussions) starting from October 2025.Now you can follow `In Progress` features. You can see that your `Feature Requests` are recorded. You can vote for them and comment your specific use-cases to shape the feature to your needs. - +The dashboard visualizes parallel execution as shown below: - -* **TypeScript SDK (`workflow-js`):** - * Multiple labels per workflow run are now supported. `label` on [`client.trigger`](/docs/workflow/basics/client/trigger) and `context.invoke` accepts `string | string[]`, and log/DLQ/cancel filters accept an array to match runs that have any of the given labels (OR semantics). Workflow run logs now expose a `labels: string[]` field, and `context.labels: string[]` replaces the now-deprecated `context.label`. - + - -* **TypeScript SDK (`workflow-js`):** - * Added optional `workflowRunId` parameter to `notify` method, enabling **lookback functionality**. When provided, notifications are stored and delivered even if sent before a workflow reaches `waitForEvent`, preventing race conditions. See [notify documentation](/docs/workflow/basics/client/notify) and [wait-for-event guide](/docs/workflow/features/wait-for-event#race-condition-between-wait-and-notify) for details. - +You can also await different step types together. For example, you can run a `context.call()` and a `context.run()` in parallel. - -* **TypeScript SDK (`workflow-js`):** - * `Label` feature is added. This will enable our users to label their workflow runs so that - * Logs can be filtered with user given label. - * DLQ can be filtered with user given label. - * `notBefore` parameter is added to `trigger` function that will allow starting a workflow run at a later date - given by the `notBefore` parameter. -* **Console:** - * A major Workflow redesign is landed to improve debugging and monitoring experience workflow runs logs. - * `Flat view` is removed. All the data is moved to single view. This is also to avoid confusing our - users and made over all experience simpler. - + + Whether executing sequentially or in parallel, you should always + await all promises in a workflow. + Leaving promises unawaited may cause unexpected behavior. + - -* **TypeScript SDK (`workflow-js`):** - * Added `retryDelay` option to dynamicaly program the retry duration. It can be configured on - [trigger](/docs/workflow/basics/client#trigger-workflow) , [context.call](/docs/workflow/basics/context#context-call) - or [serve](/docs/workflow/basics/serve#retrydelay) - * Added ability to detect if a given url is a workflow or not. Starting with `0.2.17` trigger made via the sdk can fail (instead of hanging), - if there is no workflow serve on the given url. -* **Console:** - * Local mode is added to enable our users to use the console with their local development envrionment and the locally deployed workflows. - See [docs](/docs/workflow/howto/local-development#development-server-recommended) for details. - +# Overview +Source: https://upstash.com/docs/workflow/features/retries - -* **TypeScript SDK (`workflow-js`):** - * Restart/Resume for DLQ is added to allow more options to handle failed runs. See [here](/docs/workflow/howto/failures#manually-handling-failed-workflow-runs) - * Added `WorkflowNonRetryableError` to fail a workflow without causing any retries. See [here](/docs/workflow/basics/context#error-handling-and-retries) - * For additional bug fixes, see the full changelog [here](https://github.com/upstash/workflow-js/compare/v0.2.14...v0.2.16). - +Upstash Workflow provides an automatic retry mechanism to improve reliability and make workflows resilient against temporary failures. +Workflow automatically handles transient errors such as network issues or service unavailability. - -* **TypeScript SDK (`workflow-js`):** - * Added `useFailureFunction` and `failureFunction` to `client.trigger`. See [here](https://github.com/upstash/workflow-js/pull/107). - * Added batch triggering support to `client.trigger`. See [here](https://github.com/upstash/workflow-js/pull/110). - * For additional bug fixes, see the release notes [here](https://github.com/upstash/workflow-js/releases/tag/v0.2.14). -* **Python SDK (`workflow-py`):**   - * Failure function is implemented. This feature enables to act on a failure of a workflow on the code. See docs [here](/docs/workflow/howto/failures#using-a-failurefunction-recommended) - * For other bug fixes, see the full changelog [here](https://github.com/upstash/workflow-py/compare/v0.1.0...v0.1.1). -* **Console:** - * A major redesign is coming next month to improve Workflow usability. -* **Workflow Server:**   - * An issue causing Workflows not usable with `CloudFront` is fixed. - - - -* **TypeScript SDK (`workflow-js`):** - * Added a `workflow` parameter to `context.call`, enabling type-safe workflow calls. See [here](https://github.com/upstash/workflow-js/pull/75). - * Enabled passing `context.call` settings when defining an Agent. See [here](https://github.com/upstash/workflow-js/pull/90). - * Added `delay` support to `client.trigger`. See [here](https://github.com/upstash/workflow-js/pull/100). - * Introduced `period` and improved `rate` support in flow control. See [here](https://github.com/upstash/workflow-js/pull/101). - Previously, `period` was fixed at 1 second. For example, `rate: 3 period: 1d` throttles publishes to 3 per day. - * For additional bug fixes, see the release notes [here](https://github.com/upstash/workflow-js/releases/tag/v0.2.13). -* **Workflow Server:** - * Added support for custom `period` in flow control, allowing users to set a period of up to 1 week. - Previously, `period` was fixed at 1 second. For example, `rate: 3 period: 1d` throttles publishes to 3 per day. - * Implemented **Workflow Resume** and **Restart** features (SDK and Console support in progress): - * **Resume** allows users to retry a workflow run from the point it stopped. - * **Restart** allows users to retry a workflow run from the beginning. -* **Console:** - * A major redesign is coming to improve Workflow usability. - +## How Retries Work - -* **Python SDK (`workflow-py`):**   - * Minor bug fixes. - See the full changelog [here](https://github.com/upstash/workflow-py/compare/v0.1.0...v0.1.1). -* **Workflow Server:** - * Prevented intermediate Workflow calls from failing due to request/message quota limits. - * Fixed handling of `RUN_STARTED` so that it correctly returns unfinished Workflow Runs as documented. - Previously, some Workflow Runs could be skipped if internal state was logged after `RUN_STARTED`. - * Applied several performance optimizations. - +When a step fails, Upstash Workflow automatically retries the failed step with configurable retry attempts and delay strategy. +This allows temporary issues to resolve without manual intervention. - -* **TypeScript SDK (`workflow-js`):** - * Added `onError` support to `serve` by the community. See [here](https://github.com/upstash/workflow-js/pull/79). - * Enabled support for all fetch-compatible models in Agents. See [more details here](https://github.com/upstash/workflow-js/pull/77). - * For additional bug fixes, see the full changelog [here](https://github.com/upstash/workflow-js/compare/v0.2.11...v0.2.12). - + - -* **TypeScript SDK (`workflow-js`):** - * Fixed a Unicode issue in `context.call` where binary responses from endpoints could break. See [here](https://github.com/upstash/workflow-js/pull/71). - * Introduced `WorkflowTool`, allowing Workflow Agents to define multi-step workflows as a tool. See [here](/docs/workflow/agents/features#tools). - * Added `context.invoke` to call one workflow from another with full type-safety. See the guide [here](/docs/workflow/features/invoke). - * Introduced flow control parameters to limit the rate or concurrency of workflow runs. Learn more [here](/docs/workflow/features/flow-control). - * For additional bug fixes, see the full changelog [here](https://github.com/upstash/workflow-js/compare/v0.2.3...v0.2.6). -* **Workflow Server:** - * Added RateLimit and Parallelism controls to manage the frequency and concurrency of workflow runs. Learn more [here](/docs/workflow/features/flow-control). - +By default, the retry count is set to **3**, and an **exponential backoff** delay strategy is used. - -* **TypeScript SDK (`workflow-js`):** - * Added the Agents API to workflows. You can now create AI agents to run workflows on your own infrastructure with all the benefits of workflows: reduced environment costs, fault tolerance, and scalability. Learn more about agents [here](/docs/workflow/agents/overview). - * For other bug fixes, see the full changelog [here](https://github.com/upstash/workflow-js/compare/v0.2.3...v0.2.6). -* **Python SDK (workflow-py):** - * Released [`workflow-py`](https://github.com/upstash/workflow-py). -* **Local Development Server:** - * The local development server is now available for public use. This server allows you to test your workflows locally. Learn more about the local development server [here](/docs/workflow/howto/local-development#development-server-recommended). -* **Console:** - * Separated Workflow and QStash consoles for a better user experience. - * Separated their DLQ messages as well. -* **Workflow Server:** - * The core team focused on RateLimit and Parallelism features. These features are ready on the server and will be announced next month after the documentation and SDKs are completed. - +```javascript Default Backoff Algorithm +// n = how many times this request has been retried +delay = min(86400, e ** (2.5*n)) // in seconds +``` - -* **TypeScript SDK (`workflow-js`):** - * Introduced third-party integrations, starting with Anthropic, Resend, and OpenAI. These integrations are automatically offloaded to workflows, ensuring long-running calls do not consume user environment time. See the related documentation [here](/docs/workflow/basics/context#context-api). - * Added a `timeout` parameter to `context.call`. Learn more in the [documentation](/docs/workflow/basics/context#context-call). - * Improved support for workflows in Express and SvelteKit by adding the `useJSONContent` option. - * Resolved loop detection issues on Cloudflare and Render. - * Full changelog, including all fixes, is available [here](https://github.com/upstash/workflow-js/compare/v0.2.0...v0.2.3). +| Retry Attempt | Algorithm | Delay | +|---------------|--------------|--------| +| 1 | $$e^{2.5}$$ | 12s | +| 2 | $$e^5$$ | 2m28s | +| 3 | $$e^{7.5}$$ | 30m8s | +| 4+ | $$86400$$ | 24h | -* **Workflow Server:** - * Added the `WorkflowCreatedAt` filter for Dead Letter Queue (DLQ) and Events. - * Prepared the local development server for public release (coming soon). - * Enhanced `context.SleepUntil` to support float values. - * Increased the event retention period from 10,000 events to up to 14 days. Learn more on the [Pricing page](https://upstash.com/pricing/workflow). - +## Configuration - -* **Python SDK (workflow-py):** - * Began development of the Python SDK. -* **TypeScript SDK (workflow-js):** - * Added support for string durations (e.g., `1d`, `30s`) in `context.sleep` and `context.waitForEvent`. - * Introduced integrations for [Astro](/docs/workflow/quickstarts/astro) and [Express](/docs/workflow/quickstarts/express). - * Added `client.trigger`, enabling workflows to start and return the workflow run ID. See the [documentation](/docs/workflow/basics/client#trigger-workflow). - * Added a retry option for `context.call`. See the [documentation](/docs/workflow/basics/context#context-call). - * Introduced a lazy fetch feature to support longer and larger workflows on resource-limited platforms. - * Added `context.cancel` to cancel the current workflow. See the [documentation](/docs/workflow/basics/context#context-cancel). - * Full changelog, including fixes, is available [here](https://github.com/upstash/workflow-js/compare/v0.1.2...v0.2.0). -* **Workflow Server:** - * Added bulk cancel functionality for workflow runs. See the [REST API](/docs/workflow/api-reference/runs/bulk-cancel-workflow-runs). - * Introduced content-based deduplication for workflows and retry-until-success functionality. This will allow workflows to be used in areas with unstable network connection. - +You can configure retry behavior when starting a new workflow run. - -* Optimized the console by trimming event bodies, reducing resource usage and enabling efficient querying of events with large payloads. -* Began development on a new architecture to deliver faster event processing on the server. -* Added [Wait Notify](/docs/workflow/features/wait-for-event) feature. - +### Configure Retry Attempt Count - -* Bug fixes and internal logging improvements. - +You can specify how many times a step should be retried upon failure. - -* Released [Upstash Workflow](/docs/workflow/getstarted). - +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; -- [AI Generation](https://upstash.com/docs/workflow/examples/allInOne.md) -- [Auth Provider Webhook](https://upstash.com/docs/workflow/examples/authWebhook.md) -- [Custom Retry Logic](https://upstash.com/docs/workflow/examples/customRetry.md) -- [Customer Onboarding](https://upstash.com/docs/workflow/examples/customerOnboarding.md) -- [Dynamic Workflows](https://upstash.com/docs/workflow/examples/dynamicWorkflow.md) -- [E-commerce Order Fulfillment](https://upstash.com/docs/workflow/examples/eCommerceOrderFulfillment.md) -- [Image Processing](https://upstash.com/docs/workflow/examples/imageProcessing.md) -- [Payment Retries](https://upstash.com/docs/workflow/examples/paymentRetry.md) -- [Waiting for Events](https://upstash.com/docs/workflow/examples/waitForEvent.md) +const client = new Client({ token: "" }) -# Overview -Source: https://upstash.com/docs/workflow/features/dlq +const { workflowRunId } = await client.trigger({ + url: "https:///", + retries: 3 +}) +``` -The Dead Letter Queue (DLQ) automatically captures failed workflow runs that have exhausted all retry attempts. +### Configure Retry Delay Strategy -This ensures that no workflow execution is lost and provides multiple options for recovering from failures gracefully. +Retry delay is the time to wait before trying again after a failure. You can define a custom retry delay strategy. -## How it works? +The delay is defined as a math expression that is calculated on every retry. +The expression can use the `retried` variable, which represents how many times the step has already retried (starting from 0). -When a workflow step fails and exhausts all configured retries, Upstash Workflow automatically moves the failed run to the DLQ. -This happens automatically without any additional configuration required. +To apply a constant delay, you can simply provide a fixed value. - +The expression must return the delay in **milliseconds**. -The DLQ serves as a safety net, preserving failed workflow runs with their complete execution context. +```typescript Configure Retry Delay Strategy +import { Client } from "@upstash/workflow"; - - Dead Letter Queue entries have retention period based on your pricing plan: - * **Free**: 3 days - * **Pay-as-you-go**: 1 week - * **Fixed pricing**: Up to 3 months +const client = new Client({ token: "" }) - After the retention duration expires, DLQ items are automatically removed and cannot be recovered. - +const { workflowRunId } = await client.trigger({ + url: "https:///", + retries: 3, + retryDelay: "(1 + retried) * 1000" +}) +``` -## Recovery Actions +# Prevent Retries +Source: https://upstash.com/docs/workflow/features/retries/prevent-retries -Once a workflow run is in the DLQ, you can take the following actions: +It is recommended to enable retries for workflow runs to improve reliability. -* **[Restart](/docs/workflow/features/dlq/restart)** – trigger the workflow from the beginning. -* **[Resume](/docs/workflow/features/dlq/resume)** – continue the workflow from the point of failure. -* **[Re-run Failure Function](/docs/workflow/features/dlq/callback)** – execute the workflow's failure handling logic again. -* **[Delete](/docs/workflow/features/dlq/delete)** – remove the DLQ entry if no action is required. +However, in some cases, you may want to stop execution immediately when an error occurs, without causing additional retries. +Upstash Workflow provides several mechanisms to terminate workflow execution gracefully. -You can apply these actions in bulk to multiple DLQ entries. Check the individual action pages for more details. +## Using `WorkflowNonRetryableError` -# Rerun Failure Function -Source: https://upstash.com/docs/workflow/features/dlq/callback +`WorkflowNonRetryableError` lets you explicitly fail a workflow without entering the retry cycle. -The **Rerun Failure Function** action allows you to retry the failure function that executes when a workflow run enters the Dead Letter Queue (DLQ). +When thrown, the workflow run is marked as failed, which: +* Triggers the failure function (if defined) +* Sends the workflow run to the DLQ -The failure function is typically a cleanup or notification operation that runs automatically whenever a workflow is moved to the DLQ. +```ts TypeScript highlight={7} +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload -This feature is particularly helpful for: + const isExists = await context.run("is-user-exists", () => { ... }); -* Ensuring that important cleanup operations are executed. -* Guaranteeing that logging or alerting is completed after a workflow failure. -* Recovering from temporary errors in the failure function itself. + if (!isExists) { + throw new WorkflowNonRetryableError("The user does not exists!") + } +}) +``` -By manually rerunning this function, you can ensure that critical operations—such as cleanup tasks, logging, or alerting—complete successfully even if the main workflow has failed. +## Using `context.cancel()` - +You can cancel a workflow run explicitly from inside the workflow. -You can perform this action programmatically as well: +When canceled, the run is labeled as canceled instead of failed. This means: +* The failure handler will **NOT** be triggered +* The workflow will **NOT** be sent to the DLQ - ```typescript TypeScript - import { Client } from "@upstash/workflow"; - - const client = new Client({ token: "" }); +```typescript highlight={10-11} TypeScript +export const { POST } = serve<{ orderId: string }>(async (context) => { + const { orderId } = context.requestPayload; - await client.dlq.retryFailureFunction({ - dlqId: "dlq-12345", - }); - ``` - + // Check if order is still valid + const orderStatus = await context.run("check-order-status", async () => { + return await getOrderStatus(orderId); + }); - - This action is only available if the failure function itself has failed as well. - If the failure function already succeeded, it cannot be rerun. + if (orderStatus === "cancelled") { + // Stop execution gracefully without error + await context.cancel(); + return; + } - You can view the status of the failure function in the **DLQ** and **Logs** dashboards, which indicate whether it succeeded or failed. - + // Continue processing if order is valid + await context.run("process-order", async () => { + return await processOrder(orderId); + }); +}); +``` -# Delete -Source: https://upstash.com/docs/workflow/features/dlq/delete +```python Python +@serve.post("/graceful-cancellation") +async def graceful_cancellation(context: AsyncWorkflowContext[dict]) -> None: + order_id = context.request_payload["order_id"] -The **Delete** action allows you to remove failed workflow runs from the Dead Letter Queue when no recovery action is needed. + async def _check_order_status(): + return await get_order_status(order_id) -You can delete entries individually, in bulk by IDs, or by using filters such as label, URL, or date range. + # Check if order is still valid + order_status = await context.run("check-order-status", _check_order_status) - - ```typescript TypeScript - import { Client } from "@upstash/workflow"; - - const client = new Client({ token: "" }); - - // delete a single entry - await client.dlq.delete("dlq-12345"); - - // delete multiple entries - await client.dlq.delete(["dlq-12345", "dlq-67890"]); + if order_status == "cancelled": + # Stop execution gracefully without error + await context.cancel() + return - // delete by filters - await client.dlq.delete({ label: "my-label" }); + # Continue processing if order is valid + async def _process_order(): + return await process_order(order_id) - // delete all - await client.dlq.delete({ all: true }); - ``` + await context.run("process-order", _process_order) +``` -For the full API reference, see [client.dlq.delete](/docs/workflow/basics/client/dlq/delete). +## Using conditional execution -# Restart -Source: https://upstash.com/docs/workflow/features/dlq/restart +You can also use guard conditions to skip certain steps and exit early, without throwing errors or canceling the workflow. -The **Restart** action allows you to re-execute a failed workflow run from the beginning. -All previous step results are discarded, and the workflow executes from scratch using the original configuration and initial payload. +In this case, the workflow run completes successfully because no error was raised. + + ```typescript TypeScript highlight={10-11} + export const { POST } = serve<{ data: any }>(async (context) => { + const { data } = context.requestPayload; -This approach is ideal when: + // Check if order is still valid + const orderStatus = await context.run("check-order-status", async () => { + return await getOrderStatus(orderId); + }); -* Previous step results are no longer relevant. -* The failure was caused by corrupted or inconsistent state. -* You need a completely fresh execution with updated or clean data. + if (orderStatus === "not-found") { + // Stop execution without error + return; + } - + // Continue processing if order is valid + await context.run("process-order", async () => { + return await processOrder(orderId); + }); + }); + ``` -You can perform this action programmatically as well: + ```python Python + @serve.post("/conditional-execution") + async def conditional_execution(context: AsyncWorkflowContext[dict]) -> None: + data = context.request_payload["data"] - - ```typescript TypeScript - import { Client } from "@upstash/workflow"; + async def _validate_data(): + return validate_input_data(data) - const client = new Client({ token: "" }); + # Validate data first + validation_result = await context.run("validate-data", _validate_data) - await client.dlq.restart({ - dlqId: "dlq-12345", - retries: 3, - }); - ``` - + if not validation_result["is_valid"]: + # Log the validation failure + async def _log_validation_failure(): + await log_validation_error(validation_result["errors"]) -# Resume -Source: https://upstash.com/docs/workflow/features/dlq/resume + await context.run("log-validation-failure", _log_validation_failure) -The **Resume** action allows you to continue a failed workflow run from the exact point of failure, preserving all successfully completed steps and their results. + # Stop execution without error + return -This approach is ideal when: + # Only execute if validation passes + async def _process_valid_data(): + return await process_data(data) -* The workflow has long-running or resource-intensive steps that have already succeeded. -* You want to preserve progress and avoid re-executing successful operations. -* The failure was a temporary issue that can now be resolved. + await context.run("process-valid-data", _process_valid_data) + ``` + - +# Sleep +Source: https://upstash.com/docs/workflow/features/sleep -You can perform this action programmatically as well: +Upstash Workflow provides a **Sleep** feature that allows you to pause workflow execution for specified durations without consuming compute resources. - - ```typescript TypeScript - import { Client } from "@upstash/workflow"; +This feature enables you to build time-based workflows, implement delays between steps, and create scheduled operations without the limitations of traditional serverless timeouts. - const client = new Client({ token: "" }); +## How Sleep Works - await client.dlq.resume({ - dlqId: "dlq-12345", - retries: 3, - }); - ``` - +When you use `context.sleep` or `context.sleepUntil` in your workflow, Upstash Workflow automatically pauses execution and schedules the next step to run after the specified delay. +This happens without keeping your serverless function running, making it cost-effective and reliable for long delays. - You can modify workflow code as long as changes occur **after** the failed steps. - Changes to steps prior to the failure are not allowed and may break the workflow. - - For more details, check out the [Handle workflow route code changes](/docs/workflow/howto/changes) page. + **Important:** Sleep durations have limits based on your pricing plan: + * **Free**: Maximum delay of 7 days + * **Pay-as-you-go**: Maximum delay of 1 year + * **Fixed pricing**: Custom delays (no limit) -# Overview -Source: https://upstash.com/docs/workflow/features/failure-callback - -When you define a workflow endpoint, you can attach a failure function to the workflow that allows you to execute custom logic when a workflow run fails after exhausting all retry attempts. +## Sleep Methods -This feature ensures that you can perform cleanup operations, logging, alerting, or any other custom error handling logic before the failed workflow run is moved to the Dead Letter Queue (DLQ). +Upstash Workflow provides two methods for implementing delays in your workflows: - +### 1. context.sleep -The failure function automatically receives the workflow run context and the reason for the failure, so you can decide how to handle it. +Pauses workflow execution for a specified duration relative to the current time. ```typescript TypeScript import { serve } from "@upstash/workflow/nextjs"; -export const { POST } = serve( - async (context) => { - // Your workflow logic... - }, - { - failureFunction: async ({ - context, - failStatus, - failResponse, - failHeaders, - }) => { +export const { POST } = serve(async (context) => { + const { userId } = context.requestPayload; - // 👇 Log error to monitoring system - await logToSentry(...); + // Send welcome email immediately + await context.run("send-welcome-email", async () => { + return await sendWelcomeEmail(userId); + }); - // 👇 Send alert to team - await sendSlackAlert(...); + // Wait for 3 days before sending follow-up + await context.sleep("wait-for-follow-up", "3d"); - // 👇 Perform cleanup operations - await cleanupWorkflowResources(...); - }, - } + // Send follow-up email + await context.run("send-follow-up-email", async () => { + return await sendFollowUpEmail(userId); + }); +}); ``` -); - +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext -You cannot create new workflow steps inside the `failureFunction` using `context`. -The `context` provided here is only meant to expose workflow run properties (like URL, payload, and headers). -Think of the failure function as an individual `context.run` step. It executes once with the provided context but cannot define further steps. +app = FastAPI() +serve = Serve(app) - - If you use a custom authorization method to secure your workflow endpoint, add authorization to the `failureFunction` too. - Otherwise, anyone could invoke your failure function with a request. +@serve.post("/api/onboarding") +async def onboarding(context: AsyncWorkflowContext[str]) -> None: + user_id = context.request_payload["user_id"] - Read more here: [securing your workflow endpoint](/docs/workflow/howto/security). - + # Send welcome email immediately + async def _send_welcome_email(): + return await send_welcome_email(user_id) -## Parameters + await context.run("send-welcome-email", _send_welcome_email) -The `failureFunction` receives an object with the following parameters: + # Wait for 3 days before sending follow-up + await context.sleep("wait-for-follow-up", "3d") - - The workflow context object containing: + # Send follow-up email + async def _send_follow_up_email(): + return await send_follow_up_email(user_id) - - - The ID of the failed workflow run - + await context.run("send-follow-up-email", _send_follow_up_email) +``` + - - The publicly accessible workflow endpoint URL - +You can specify durations using human-readable strings: - - The original request payload that triggered the workflow - +* `"10s"` = 10 seconds +* `"1m"` = 1 minute +* `"30m"` = 30 minutes +* `"2h"` = 2 hours +* `"1d"` = 1 day +* `"1w"` = 1 week +* `"1mo"` = 1 month +* `"1y"` = 1 year - - The original request headers - +You can also use numeric values in seconds: - - Environment variables - - - +* `60` = 60 seconds (1 minute) +* `3600` = 3600 seconds (1 hour) +* `86400` = 86400 seconds (1 day) - - The HTTP status code returned by the failed workflow step. - +### 2. context.sleepUntil - - The response body returned by the failed workflow step. - +Pauses workflow execution until a specific timestamp in the future. - - The response headers returned by the failed workflow step. - + +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; -# Advanced failureUrl Option -Source: https://upstash.com/docs/workflow/features/failureFunction/advanced +export const { POST } = serve(async (context) => { + const { userId, scheduledTime } = context.requestPayload; -The `failureUrl` is an advanced option that sends failure callback to a different endpoint rather than to the workflow endpoint (failure function). -This approach is useful for handling failures on separate infrastructure. + // Calculate the scheduled time + const scheduledDate = new Date(scheduledTime); -You can use either `failureFunction` or `failureUrl`, but not both. These options are mutually exclusive. + // Wait until the scheduled time + await context.sleepUntil("wait-until-scheduled", scheduledDate); -For most users, **Failure Function** is the better choice because: -* It runs alongside your workflow and has access to the same context and dependencies -* Failure function requests are automatically retried on failure as well. -* You can manually retry failure function if it fails via DLQ. - -If you think this advanced option fits your need, you can configure it by passing `failureUrl` configuration. + // Execute the scheduled task + await context.run("execute-scheduled-task", async () => { + return await executeTask(userId); + }); +}); +``` - - ```typescript - import { Client } from "@upstash/workflow"; +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext +from datetime import datetime - const client = new Client({ token: "" }) +app = FastAPI() +serve = Serve(app) - const { workflowRunId } = await client.trigger({ - url: "https:///workflow" - failureUrl: "https:///workflow-failure" - }) - ``` +@serve.post("/api/scheduled-task") +async def scheduled_task(context: AsyncWorkflowContext[str]) -> None: + user_id = context.request_payload["user_id"] + scheduled_time = context.request_payload["scheduled_time"] - ```python Python - @serve.post("/api/example", failure_url="https:///workflow-failure") - async def example(context: AsyncWorkflowContext[str]) -> None: - # Your workflow logic... - pass - ``` - + # Calculate the scheduled time + scheduled_date = datetime.fromisoformat(scheduled_time) -# Reliability of Failure Function -Source: https://upstash.com/docs/workflow/features/failureFunction/reliability + # Wait until the scheduled time + await context.sleep_until("wait-until-scheduled", scheduled_date) -The failure function is executed whenever a workflow run fails. + # Execute the scheduled task + async def _execute_task(): + return await execute_task(user_id) -In some cases, the failure function itself may throw an error. -When this happens, it will be retried according to the workflow run's retry configuration. -If all retry attempts also fail, the failure function execution is marked as failed. + await context.run("execute-scheduled-task", _execute_task) +``` + -You can view and filter workflow runs with failed failure function executions in the DLQ dashboard. +For `context.sleepUntil`, you can use: - +* `Date` objects (JavaScript/TypeScript) +* Unix timestamps (Python) +* ISO string dates -From the DLQ dashboard, you can retry the failure function. + + Sleep operations have a precision of approximately 1 second. Very short delays (less than 1 second) may not be exact. + - +The sleep feature in Upstash Workflow provides a powerful way to create time-based, reliable workflows without the limitations of traditional serverless timeouts. -You can perform this action programmatically as well: + By leveraging this feature, you can build sophisticated business logic that spans hours, days, or even months while maintaining cost efficiency and reliability. -```ts -import { Client } from "@upstash/workflow"; +# Wait +Source: https://upstash.com/docs/workflow/features/wait -const client = new Client({ token: "" }); +You can pause a workflow run with the `waitForEvent` step. An event is uniquely identified by event ID. -const response = await client.dlq.retryFailureFunction({ - dlqId: "dlq-12345" // The ID of the DLQ message to retry -}); -``` +The workflow will resume when the matching event is published. -# Overview -Source: https://upstash.com/docs/workflow/features/flow-control +`waitForEvent` supports configurable timeouts to prevent workflows from waiting indefinitely. +When a timeout occurs, the returned object includes `timeout: true`, allowing you to handle the failure case gracefully (for example, cancel an order, notify the user, or retry later). -Flow Control allows you to limit how many workflow steps are executed by delaying and queuing their delivery. + +If no timeout is specified, the default is **7 days**. + -This feature helps to: -* Manage resource consumption -* Prevent violations of external API rate limits -* Ensure workflows run within defined system constraints + +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; -## How Flow Control Works +export const { POST } = serve(async (context) => { + const { orderId, userEmail } = context.requestPayload; -When defined limits are exceeded, Flow Control automatically queues and delays step executions instead of rejecting them. -This guarantees that all steps are eventually processed while staying within configured thresholds. + // Wait for order processing completion + const { eventData, timeout } = await context.waitForEvent( + "wait-for-order-processing", + `order-${orderId}`, + { + timeout: "1d" // 1 day timeout + } + ); -To configure Flow Control, you define a flow control key, a unique identifier used to group related steps under the same rate and parallelism limits. -The steps that has the same flow control key respect the same constraints. + if (timeout) { + // Handle timeout scenario + await context.run("handle-timeout", async () => { + return await handleOrderTimeout(orderId, userEmail); + }); + return; + } -There are two main parameters to configure: +}); +``` -* [Rate and Period](/docs/workflow/features/flow-control/rate-period): Maximum number of steps that may start within a time window -* [Parallelism](/docs/workflow/features/flow-control/parallelism): Maximum number of steps allowed to run concurrently +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext -These parameters can be combined for fine‑grained control. -For example, you can allow up to 10 steps per minute but restrict concurrency -to 5 steps in parallel, ensuring more predictable load patterns. +app = FastAPI() +serve = Serve(app) -## Example +@serve.post("/api/order-processing") +async def order_processing(context: AsyncWorkflowContext[str]) -> None: + order_id = context.request_payload["order_id"] + user_email = context.request_payload["user_email"] -Suppose you have the following workflow: + # Send order processing request + async def _request_order_processing(): + return await request_order_processing(order_id) -```typescript -export const { POST } = serve<{ topic: string }>(async (context) => { - const payload = context.requestPayload + await context.run("request-order-processing", _request_order_processing) - await context.run("step-1", () => { ... }); + # Wait for order processing completion + result = await context.wait_for_event( + "wait-for-order-processing", + f"order-{order_id}", + timeout="10m" # 10 minutes timeout + ) - await context.run("step-2", () => { ... }); + if result["timeout"]: + # Handle timeout scenario + async def _handle_timeout(): + return await handle_order_timeout(order_id, user_email) - await context.run("step-3", () => { ... }); -}) -``` + await context.run("handle-timeout", _handle_timeout) + return -Now imagine you trigger **N workflow runs** for this workflow with the following configuration: + # Process the completed order + async def _process_completed_order(): + return await process_completed_order(order_id, result["event_data"]) -```typescript -const { workflowRunId } = await client.trigger({ - url: "https:///", - flowControl: { - key: "fw_example", - parallelism: 7, - rate: 3, - period: "1m", - } -}) + await context.run("process-completed-order", _process_completed_order) ``` + -Without Flow Control, all workflow runs immediately execute their steps as soon as possible. -If the workflow calls an external API in a step, this would likely result in ~N concurrent requests being fired in a very short timeframe, potentially overloading services or breaching API limits. +# Overview +Source: https://upstash.com/docs/workflow/features/wait-for-event - +Wait for Event feature that allows you to pause workflow execution until an external event occurs. -With the configuration above: -* **Rate:** At most 3 steps per minute can start across all workflow runs. -* **Parallelism:** At most 7 steps can be running at the same time. +This feature enables you to build asynchronous workflows that can wait for user interactions, external system responses, or any other events without consuming compute resources. -Steps that exceed these limits are automatically queued and executed later. +## How Wait for Event Works - +When you use `context.waitForEvent()` in your workflow, Upstash Workflow automatically pauses execution and waits for an external notification to resume. +This happens without keeping your serverless function running, making it cost-effective and reliable for event-driven workflows. -Note that each step above corresponds to a separate workflow run. -Because this workflow is sequential, each workflow run has only one pending step at a time. -In workflows with **parallel branches**, multiple steps from the same workflow run may appear in the schedule simultaneously. +Each waiter has a timeout duration to wait for the event and then fires automatically. -Parallelism slots are consumed by running steps. -If no slots are available, new steps enter the **waitlist** until resources free up: + + Wait for Event timeouts have limits based on your pricing plan: + * **Free**: Maximum timeout of 7 days + * **Pay-as-you-go**: Maximum timeout of 1 year + * **Fixed pricing**: Custom timeouts (no limit) + - +## Race Condition Between Wait and Notify - -Upstash Workflow does not support per-step level configuration. Meaning that you can attach a flow-control configuration -to the workflow run and all the steps will inherit to the same limits. -Following the analogy above, you cannot enforce parallelism limit on "green" steps natively. +A race condition can occur when `notify` is called before `waitForEvent` is executed. +In this scenario, the notification will be sent but no workflow will be waiting to receive it, causing the event to be lost. -The context.call and context.invoke steps are exception this to this rule and accept their own flow control configuration: +### Solutions -* [context.call](/docs/workflow/basics/context/call) – lets you run external HTTP requests under a separate key, so you can throttle third‑party API calls independently of your workflow logic. -* [context.invoke](/docs/workflow/basics/context/invoke) – starts a new workflow run with its own flow control configuration. This allows the invoked workflow to run under different limits than the parent workflow, giving you more precise control. +There are three ways to handle race conditions: -If you want to throttle a specific `context.run` step, the recommended approach is to **extract it into a separate workflow** and call it using `context.invoke()` with its own flow control configuration with a stricter limits. - +1. **Use lookback with `workflowRunId`** (Recommended for targeted notifications) +2. **Use [Webhooks](/docs/workflow/features/webhooks)** (Recommended for general use) +3. **Check and retry** (Manual approach) -## Configuration +#### Option 1: Lookback with workflowRunId -You can configure flow control when starting a workflow run: +When you know which specific workflow run should receive the notification, you can provide a `workflowRunId` to enable lookback. The notification will be stored and delivered even if sent before `waitForEvent`: -```typescript Configure Retry Attempt Count + +```typescript TypeScript import { Client } from "@upstash/workflow"; -const client = new Client({ token: "" }) +const client = new Client({ token: "" }); +// Trigger a workflow const { workflowRunId } = await client.trigger({ - url: "https:///", - flowControl: { - key: "user-signup", - parallelism: 1, - rate: 10, - period: 100, - } -}) + url: "https://your-app.com/api/process-order", + body: { orderId: "123" } +}); + +// Immediately notify with lookback - no race condition! +await client.notify({ + eventId: "payment-verified", + eventData: { verified: true }, + workflowRunId: workflowRunId, // Enables lookback +}); ``` + -All steps within a workflow run will adhere to the specified flow control configuration. +#### Option 2: Use Webhooks - -Keep in mind that rate/period and parallelism info are kept on each step separately. -If you change the rate/period or parallelism on a new deployment, the old fired ones will not be affected. -They will keep their flow control configuration. +[Webhooks](/docs/workflow/features/webhooks) have built-in lookback and are safer against timing issues for general event handling. -During the period that old steps have not been delivered but there are also steps with new rates, Upstash Workflow will effectively allow the highest rate/period or highest parallelism. Eventually (after the old publishes are delivered), the new rate/period and parallelism will be used. - +#### Option 3: Check and Retry -# Monitor & Manage -Source: https://upstash.com/docs/workflow/features/flow-control/monitor +Alternatively, you can check the response of the `notify` operation and retry if needed: -You can monitor wait list size of your flow control key's from the console `FlowControl` tab. The console also allows you to pin, unpin, and reset rate for flow control keys directly. + +```typescript TypeScript +import { Client } from "@upstash/workflow"; - +const client = new Client({ token: "" }); -Also you can get the same info using the REST API. -* [List All Flow Control Keys](/docs/workflow/api-reference/flow-control/list-flow-control-keys). -* [Single Flow Control Key](/docs/workflow/api-reference/flow-control/get-flow-control-key). -* [Global Parallelism](/docs/workflow/api-reference/flow-control/get-global-parallelism). +const result = await client.notify({ + eventId, + eventData +}); -## Manage +// Check if any workflows were notified +if (result.waiters && result.waiters.length > 0) { + console.log(`Notified ${result.waiters.length} workflows`); + return result; +} -You can manage flow control keys by pausing/resuming delivery, pinning/unpinning configurations, and resetting rate counts. +// If no workflows were waiting, wait and retry once +console.log("No workflows waiting, retrying in 5 seconds..."); +await new Promise(resolve => setTimeout(resolve, 5000)); -These operations are available via the SDK, REST API, or directly from the **Flow Control** tab in the [Upstash Console](https://console.upstash.com). +return await client.notify({ + eventId, + eventData +}); +``` + -Since Upstash Workflow uses QStash under the hood, flow control management is done through the QStash client. See the [QStash Flow Control Management API](/docs/qstash/features/flowcontrol#management-api) for full details and code examples. +## Selecting an Event ID -# Parallelism -Source: https://upstash.com/docs/workflow/features/flow-control/parallelism +When a workflow run waits on an event ID, it's appended to a list of waiters for the event ID. -The parallelism limit controls the maximum number of calls that can be executed concurrently. -Unlike rate limiting (which works per time window), parallelism enforces concurrency control with a token-based system. +When a notify request is sent, all workflow runs waiting for that event are notified sequentially. +To avoid heavy notify operations, it’s recommended to use unique event IDs instead of generic ones. +For example, instead of waiting on `user-sent-verification`, wait the workflow on `user-{userId}-sent-verification` event. -```typescript Configure Retry Attempt Count -import { Client } from "@upstash/workflow"; +# Webhooks +Source: https://upstash.com/docs/workflow/features/webhooks -const client = new Client({ token: "" }) +The Webhook feature allows you to pause workflow execution and wait for a webhook URL to be called, enabling seamless integration with third-party APIs and asynchronous operations. -const { workflowRunId } = await client.trigger({ - url: "https:///", - flowControl: { - key: "user-signup", - parallelism: 10, - } -}) -``` +This feature is perfect for scenarios where you need to: +* Wait for external API processing to complete +* Integrate with services that use callback URLs +* Receive notifications from external webhooks -**Example**: -If `parallelism = 3`, at most 3 requests can run concurrently. +## How Webhooks Work -When tokens are available, requests acquire one and start execution: - +When you use webhooks in your workflow, Upstash Workflow: -When all tokens are in use, additional requests are not failed — they’re queued in a **waitlist**: - +1. **Creates a unique webhook URL** via `context.createWebhook()` +2. **Provides the URL to external services** (typically through `context.call()`) +3. **Pauses execution** via `context.waitForWebhook()` until the webhook is called +4. **Resumes workflow** when the webhook receives a request or timeout is reached -The step in the waitlist will wait for a step to complete and hand off it's token to a pending request: +This happens without keeping your serverless function running, making it cost-effective for long-running integrations. - - Token handoff does not guarantee strict ordering. - A later request in the waitlist may acquire a token before an earlier one. - +## Examples - +### Basic Usage -# Rate and Period -Source: https://upstash.com/docs/workflow/features/flow-control/rate-period +```typescript +import { serve } from "@upstash/workflow/nextjs"; -The rate specifies the maximum number of requests allowed in a given period (time window). +export const { POST } = serve(async (context) => { + // Step 1: Create webhook + const webhook = await context.createWebhook("create webhook"); -```typescript Configure Retry Attempt Count -import { Client } from "@upstash/workflow"; + // Step 2: Call an external endpoint, which calls the webhookUrl upon completion + const callResult = await context.call("call webhook caller", { + url: "https://webhook/caller", + method: "POST", + body: JSON.stringify({ + webhookUrl: webhook.webhookUrl, + }), + }); -const client = new Client({ token: "" }) + // Step 3: Wait for the webhook to be called + const webhookResponse = await context.waitForWebhook( + "wait for webhook", + webhook, + "30s" // timeout + ); -const { workflowRunId } = await client.trigger({ - url: "https:///", - flowControl: { - key: "user-signup", - rate: 10, - period: 100, + if (webhookResponse.timeout) { + console.log("Webhook was not called in time"); + // Handle timeout scenario + } else { + console.log("Webhook received:", webhookResponse.request); + // Process the webhook data } -}) +}); ``` -**Example**: -If `rate = 2` and `period = 1 minute`, then **a maximum of 2 steps** can be executed per minute. +### Waiting for Multiple Calls -The first 2 requests within the minute are executed immediately: +Some services send multiple progress updates to a webhook URL as processing continues. You can wait for the same webhook multiple times in a loop until you receive a final "finished" signal: - +```typescript +while (true) { + const webhookResponse = await context.waitForWebhook( + `wait for progress update ${stepCount}`, + webhook, + "5m" // 5 minute timeout between updates + ); -The 3rd request in the same minute is not executed immediately: + if (webhookResponse.timeout) { + console.log("No progress update received in time, exiting"); + break; + } else { + const request = webhookResponse.request; + console.log("Progress update received:", await request.json()); - + if (request.headers.get("x-task-finished") === "true") { + console.log("Task finished, exiting loop"); + break; + } + } +} +``` -Instead of rejecting it, Workflow schedules the request in the next available time window: +## Race Condition Safety - +Webhooks have built-in **lookback** protection, making them safer against race conditions. If an external service calls the webhook URL before `context.waitForWebhook()` is executed, the webhook call is stored and will be returned when `waitForWebhook` is called. -Note that step executions may take longer than the defined period. -The rate limit only controls how many steps are **started** within each time window, -it does not limit their execution duration. +This means you don't need to worry about timing issues between creating the webhook and waiting for it - the workflow will always receive the callback even if it arrives early. -# Overview -Source: https://upstash.com/docs/workflow/features/invoke +## Comparison with Wait for Event -You can start another workflow run inside a workflow and await its execution to complete. -This allows to orchestrate multiple workflows together without external synchronization. +Webhooks and [Wait for Event](/docs/workflow/features/wait-for-event) serve similar purposes but with different approaches: -When you use `context.invoke`, invoking workflow will wait until the invoked workflow finishes before running the next step. +| Feature | Webhooks | Wait for Event | +|---------|----------|----------------| +| **Trigger** | External HTTP call to unique URL | Notify via Upstash Workflow Client | +| **Setup** | Create webhook, pass URL to external service | Share event ID with external service | +| **Integration** | Easy with callback-based APIs | Requires Workflow Client integration | +| **Lookback** | ✅ Yes - safe against race conditions | ❌ No - notify before wait will be lost | +| **Best For** | Third-party API callbacks | Internal event notifications | -```typescript -const { - body, // response from the invoked workflow - isFailed, // whether the invoked workflow was canceled - isCanceled // whether the invoked workflow failed -} = await context.invoke( - "analyze-content", - { - workflow: analyzeContent, - body: "test", - header: {...}, // headers to pass to anotherWorkflow (optional) - retries, // number of retries (optional, default: 3) - flowControl, // flow control settings (optional) - workflowRunId // workflowRunId to set (optional) - } -) -``` +Choose webhooks when integrating with external services that support callback URLs, or when you need protection against race conditions. Choose Wait for Event when you have control over the notification mechanism and can use the Workflow Client. -You can return a response from a workflow, which will be delivered to invoker workflow run. +## API Reference - + + + Create a unique webhook URL for external services to call + + + Wait for the webhook to be called or timeout + + - - You cannot create an infinite chain of workflow invocations. If you set up an 'invoke loop' where workflows continuously invoke each other, the process will fail once it reaches a depth of 100. - +# Overview +Source: https://upstash.com/docs/workflow/getstarted -# Using Serve Many -Source: https://upstash.com/docs/workflow/features/invoke/serveMany +Upstash Workflow lets you write **durable, reliable and performant serverless functions**. Get delivery guarantees, automatic retries on failure, scheduling and more without managing any infrastructure. -Normally, workflows are created with `serve()`, which exposes each workflow as its own HTTP endpoint. -If workflows were invoked only by their full URL, it would mean: + -* You'd have to provide the URL explicitly like a trigger request -* You'd lose type safety for request and response payloads +## Quickstarts -To avoid these issues, Upstash Workflow lets you define workflows as objects and expose them under the same parent path. -This way, you can invoke a workflow simply by passing the object to `context.invoke`, with full type safety and no URLs required. +Upstash Workflow supports Next.js, Cloudflare Workers and [many other frameworks](/docs/workflow/quickstarts/platforms) in TypeScript and Python. - - - Use `createWorkflow()` to define workflows as objects. + + + Build a Next.js application with QStash Workflow + + + Use and deploy Upstash Workflow on Cloudflare Workers + + + Use Upstash Workflow for Python with Next.js and FastAPI + + - It works just like `serve()`—accepting the same arguments—but does **not** expose the workflow directly as an HTTP endpoint. - Instead, it simply initializes a workflow object. - - ```typescript - const workflowOne = createWorkflow( - // 👇 Request Payload Type - async (context: WorkflowContext) => { - - await context.sleep("wait 1 second", 1) - - // 👇 Workflow Response Type - return { message: "This is the data returned by the workflow" }; - } - ); - - const workflowTwo = createWorkflow(async (context) => { - // 👇 Invoke the workflow with type-safe call - const { body } = await context.invoke( - "invoke workflowOne", - { - workflow: workflowOne, - body: "user-1" - } - ), - }); - ``` - - - Use `serveMany()` instead of `serve()` to expose multiple workflows on a single catch‑all route. +## Key Features - If one workflow is going to invoke another, both must be included in the same `serveMany` definition. - First step of using `serveMany` is to define a catch-all route. + + + If your platform experiences a temporary outage, your workflow can pick up right where it left off, ensuring stability even in unstable environments. + + + Run long-running REST endpoints, such as complex AI models or video processing tools, even on serverless platforms with strict time limits. + + + Create workflows that wait for external events before proceeding. Ideal for user confirmations and asynchronous notifications. + + + Run jobs at regular intervals with support for cron expressions. Perfect for recurring tasks like reminders, reports, or newsletters. + + + Start independent tasks in parallel and wait for them to finish simultaneously, reducing latency. + + + Need your code to “sleep” for days, weeks, or even months? Supports long delays beyond serverless time limits. + + + Ensures at-least-once delivery. Failed requests are logged in a Dead Letter Queue to prevent data loss. + + + Prevent overwhelming your app or external services by configuring rate per second or parallelism limits. + + + Monitor workflow steps with insights. Filter events to track successes, failures, retries, and stalls. + + - ```typescript app/serve-many/[...any]/route.ts - export const { POST } = serveMany( - { - "workflow-one-route": workflowOne, - "workflow-two-route": workflowTwo, - } - ) - ``` +## Example Use Cases - - In Next.js, a catch‑all route can be defined by creating a `route.ts` file inside a directory named with `[...]`, for example: `app/serve-many/[...any]/route.ts`. +Here are some example real world use-cases for Upstash Workflow: - For implementations of `serveMany` in other frameworks, you can refer to the projects available in the [`examples` directory of the workflow-js repository](https://github.com/upstash/workflow-js/tree/main/examples). - - - - When invoking, pass the workflow object created with `createWorkflow()` (from step 1) as the argument to `context.invoke()`. - This removes the need to specify a URL explicitly and ensures the call is - fully type‑safe. + + + Use LLM Agents equipped with custom tools to achieve tasks + + + Download a large dataset without timeouts, process the data in chunks and generate a report. + + + Control workflow execution with events, log event data and send emails + + + Start a workflow from a webhook. Handle user creation, trial management, + email reminders and notifications. + + + Register a new user, send welcome emails, and periodically check and respond + to the user's activity state with emails. + + + Receive an order request, verify the stock, process the payment, and handle + order dispatch and customer notifications. + + + Manage uploading images to the data store. Apply filters and resize the + images to different resolutions in parallel. + + + Retry payments with a day of delay, send emails, and suspend account if + payment fails after the retries. + + - ```ts - const workflowTwo = createWorkflow(async (context) => { - // 👇 Invoke the workflow with type-safe call - const { body } = await context.invoke( - "invoke workflowOne", - { - // 👇 Pass the workflow object as argument - workflow: workflowOne, - body: "user-1" - } - ), - }); - ``` +## How it works - +Upstash Workflow builds on the principle of steps. Instead of defining a single, complex piece of business logic, workflows contain multiple individual steps. - - In this example, both `workflowOne` and `workflowTwo` are exposed through `serveMany`, sharing the same parent path. +Each of the steps are executed by a separate request to your application, by preserving the output of previous steps. - You can start `workflowOne` by sending a trigger request to: - `https://your-app/serve-many/workflow-one-route`. +In case of an error, a failed step is retried individually without needing to re-run any previous steps. Instead of the entire business logic, _each step_ can take up your serverless function execution duration, and many more benefits. - ```typescript - import { Client } from "@upstash/workflow"; + - const client = new Client({ token: "" }) +## Support - const { workflowRunId } = await client.trigger({ - // 👇 URL of workflow one - url: "https://your-app/serve-many/workflow-one-route" - }) - ``` +Need help or have questions? We're here to support you: - Route names are inferred from the keys you pass to `serveMany`. - For example, you can start `workflowTwo` by sending trigger request to: `https://your-app/serve-many/workflow-two-route`. - - +* Join our Discord community to ask questions and share feedback +* Open a ticket through the Intercom chatbox in the dashboard for any issue -# Notify -Source: https://upstash.com/docs/workflow/features/notify +# Cancel a Run +Source: https://upstash.com/docs/workflow/howto/cancel -You can notify all the workflow runs waitingi for a specific event ID. -There are two ways to send a notify request. +You can cancel a running workflow both programatically and from your Upstash Workflow console. -## Notify within Workflow +## Cancelling via console -Notifies other workflows waiting for a specific event from within a workflow. +In your Upstash Workflow console, find the run you'd like to cancel and press the `Cancel Workflow` button on the right side: - -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; + -export const { POST } = serve(async (context) => { - const { orderId, processingResult } = context.requestPayload; +## Cancelling programatically - await context.run("process-order", async () => { - // ... - }) + + This feature is not yet available in + [workflow-py](https://github.com/upstash/workflow-py). See our + [Roadmap](/docs/workflow/roadmap) for feature parity plans and + [Changelog](/docs/workflow/changelog) for updates. + - const { notifyResponse } = await context.notify( - "notify-processing-complete", - `order-${orderId}`, - { - orderId, - status: "completed", - result: processingResult, - completedAt: new Date().toISOString() - } - ); +```javascript +import { Client } from "@upstash/workflow"; -}); +const client = new Client({ token: "" }); +await client.cancel({ ids: "" }); ``` -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext -from datetime import datetime +And replace `` with your actual run ID. See [the documentation of `client.cancel` method for more information about other ways of canceling workflows](/docs/workflow/basics/client/cancel). -app = FastAPI() -serve = Serve(app) +You can also use the [Upstash Workflow REST API](/docs/workflow/api-reference/runs/cancel-workflow-run) to cancel a run programatically. -@serve.post("/api/order-processor") -async def order_processor(context: AsyncWorkflowContext[str]) -> None: - order_id = context.request_payload["order_id"] - processing_result = context.request_payload["processing_result"] +# Update a Workflow +Source: https://upstash.com/docs/workflow/howto/changes - # Process the order - async def _process_order(): - return await process_order(order_id) +Workflows are composed of multiple steps. When you modify workflow code, it's important to consider how these changes might affect in-progress workflows. - result = await context.run("process-order", _process_order) +## Issues - # Notify waiting workflows that processing is complete - notify_response = await context.notify( - "notify-processing-complete", - f"order-{order_id}", - { - "order_id": order_id, - "status": "completed", - "result": processing_result, - "completed_at": datetime.utcnow().isoformat() - } - ) +You cannot change the step order of an existing workflow. - # Log notification results - async def _log_notification(): - print(f"Notified {len(notify_response)} waiting workflows") - return notify_response +If your code changes remove or reorder existing steps, in-progress workflows may attempt to continue from a point that no longer exists. This can lead to workflow failures, typically resulting in the following error: - await context.run("log-notification", _log_notification) +```bash +HTTP status 400. Incompatible step name. Expected , got ``` - - -## External Notification -You can also notify workflows from external systems using the Workflow Client: +## Safe changes - -```typescript TypeScript -import { Client } from "@upstash/workflow"; +Updating workflow code is safe in the following cases: -const client = new Client({ token: "" }); +* No active workflow runs exist +* Only new steps are added to the end of the workflow -await client.notify({ - eventId: "order-completed-123", - eventData: { - orderId: "123", - status: "completed", - deliveryTime: "2 days", - trackingNumber: "TRK123456" - } -}); -``` +## Guidelines for updating workflows -```python Python -from upstash_workflow import Client +Consider the following approaches when updating your workflow code: -client = Client("") +* **Accept potential failures:** If you're fine with in-progress workflows failing, you can make any code changes. +* **Use a different route:** To avoid failures, consider serving the updated workflow under a different route. +* **Stop traffic before deployment:** If you need to keep the same route, stop all traffic before deploying new code. +* **Add steps only:** If stopping traffic is not an option, limit your changes to adding new steps at the end of the workflow. -# Notify workflows waiting for a specific event -await client.notify( - event_id="order-completed-123", - event_data={ - "order_id": "123", - "status": "completed", - "delivery_time": "2 days", - "tracking_number": "TRK123456" - } -) -``` - +For a deeper understanding of these limitations, see our [how workflows work](/docs/workflow/basics/how) section. -## Lookback Functionality +# Configure a Run +Source: https://upstash.com/docs/workflow/howto/configure -By default, if you call `notify` before a workflow reaches its `waitForEvent` step, the notification will be lost (race condition). To prevent this, you can provide a `workflowRunId` parameter which enables **lookback** - the notification will be stored and delivered even if sent before the wait step. +You can configure a workflow run when starting it. The following are the options you can configure: -This is particularly useful when: -* You trigger a workflow and immediately want to send it an event -* You have concurrent operations where timing is unpredictable -* You want to eliminate race conditions in your event-driven workflows +1. Retries: The number of retry attempt Upstash Workflow does when a step fails in the workflow run +2. Retry Delay: The delay strategy between retries when Upstash Workflow attempts retries. +3. Flow Control: The rate, period and parallelism that steps should respect and logical grouping key to share with other workflow runs. - -```typescript TypeScript +You can pass these configuration options when starting a workflow run: + +```typescript import { Client } from "@upstash/workflow"; -const client = new Client({ token: "" }); +const client = Client() -// Trigger a workflow and get its run ID const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/process-order", - body: { orderId: "123" } -}); - -// Immediately notify it with lookback enabled -// The notification will be delivered even if the workflow -// hasn't reached waitForEvent yet -await client.notify({ - eventId: "payment-verified", - eventData: { verified: true, amount: 100 }, - workflowRunId: workflowRunId, // Enables lookback + url: `http://localhost:3000/api/workflow`, + retries: 3, + retryDelay: "(1 + retries) * 1000", + flowControl: { + key: "limit-ads", + rate: 1, + parallelism: 10 + } }); ``` -```python Python -from upstash_workflow import Client +The workflow run configuration does **not** apply to `context.call()` and `context.invoke()` steps. +These steps accept their own configuration options, allowing fine-grained control over external requests. +If not specified, they fall back to their default values. -client = Client("") +For details, see: +* [context.call](/docs/workflow/basics/context/run) +* [context.invoke](/docs/workflow/basics/context/run) -# Trigger a workflow and get its run ID -workflow_run = await client.trigger( - url="https://your-app.com/api/process-order", - body={"order_id": "123"} -) + +Upstash Workflow does not support step level configuration. The configuration applies to all steps executed by a workflow run. -# Immediately notify it with lookback enabled -await client.notify( - event_id="payment-verified", - event_data={"verified": True, "amount": 100}, - workflow_run_id=workflow_run.workflow_run_id # Enables lookback -) -``` - +If you want to specifically throttle a step, there is a workaround by splitting step to another workflow and using `context.invoke()`. + -The same also applies to `context.notify` +# Handle Failed Runs +Source: https://upstash.com/docs/workflow/howto/failures - -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; +This guide shows you how to **gracefully handle failed workflow runs**. This involves best practices on resolving runtime errors, logging and manually retrying runs that have failed multiple times. -export const { POST } = serve(async (context) => { - const { orderId, processingResult } = context.requestPayload; +## Why a workflow might fail - await context.run("process-order", async () => { - // ... - }) +* A step in your workflow throws a database error that causes your code to fail at runtime. +* QStash calls your workflow URL, but the URL is not reachable - for example, because of a temporary outage of your deployment platform. +* A single step takes longer than your platform's function execution limit. - const { notifyResponse } = await context.notify( - "notify-processing-complete", - `order-${orderId}`, - { - orderId, - status: "completed", - result: processingResult, - completedAt: new Date().toISOString(), - workflowRunId: "targetWorkflowRunId" // Enables lookback - } - ); +Workflow automatically retries a failed step based on your configuration (by default, it retries three times with exponential backoff). +This helps handle temporary outages or intermittent failures gracefully. -}); -``` - + - -When using lookback with `workflowRunId`, the notification is targeted to a specific workflow run rather than all waiters with that event ID. - +If, even after all retries, your step does not succeed, we'll move the failed run into your [Dead Letter Queue (DLQ)](/docs/qstash/howto/handling-failures#dead-letter-queue). That way, you can always manually retry it again and debug the issue. -# Parallel Steps -Source: https://upstash.com/docs/workflow/features/parallel-steps + -Upstash Workflow supports executing multiple steps in parallel. +If you want to take an action (a cleanup/log), you can configure either `failureFunction` or a `failureUrl` on the `serve` method of your workflow. +These options allow you to define custom logic or an external endpoint that will be triggered when a failure occurs. -Since each step returns a `Promise`, you can execute multiple steps concurrently by using `Promise.all()`. -This behavior works out of the box. No additional configuration is required. +## Using a `failureFunction` (recommended) -```typescript app/api/workflow/route.ts -import { serve } from "@upstash/workflow/nextjs"; -import { checkInventory, brewCoffee, printReceipt } from "@/utils"; +The `serve` function you use to create a workflow endpoint accepts a `failureFunction` parameter - an easy way to gracefully handle errors (i.e. logging them to Sentry) or your custom handling logic. -export const { POST } = serve(async (context) => { + +```typescript TypeScript +export const { POST } = serve( + async (context) => { + // Your workflow logic... + }, + { + failureFunction: async ({ + context, + failStatus, + failResponse, + failHeaders, + }) => { + // Handle error, i.e. log to Sentry + console.error("Workflow failed:", failResponse); - // 👇 Execute steps in parallel - const [coffeeBeansAvailable, cupsAvailable, milkAvailable] = - await Promise.all([ - context.run("check-coffee-beans", () => checkInventory("coffee-beans")), - context.run("check-cups", () => checkInventory("cups")), - context.run("check-milk", () => checkInventory("milk")), - ]); + // You can optionally return a string that will be visible + // in the UI (coming soon) and in workflow logs + return `Workflow failed with status ${failStatus}: ${failResponse}`; + }, + } +); +``` -}); +```python Python +async def failure_function( + context, # context during failure + fail_status, # failure status + fail_response, # failure message + fail_headers # failure headers +): + # handle the failure + pass + +@serve.post("/api/example", failure_function=failure_function) +async def example(context: AsyncWorkflowContext[str]) -> None: ... ``` -The results of the parallel steps are available as usual once awaited. + -The dashboard visualizes parallel execution as shown below: +Note: If you use a custom authorization method to secure your workflow endpoint, add authorization to the `failureFunction` too. Otherwise, anyone can invoke your failure function. Read more here: [securing your workflow endpoint](/docs/workflow/howto/security). - +In `@upstash/workflow`, the `failureFunction` can optionally return a string value that will be displayed in the UI (coming soon) and included in the workflow logs. This is useful for providing custom error messages, debugging information, or tracking specific failure conditions. -You can also await different step types together. For example, you can run a `context.call()` and a `context.run()` in parallel. +## Using a `failureUrl` - - Whether executing sequentially or in parallel, you should always - await all promises in a workflow. - Leaving promises unawaited may cause unexpected behavior. - +Instead of using the built-in failure function, you can define a separate failure callback URL. +Unlike the failure function, which only works when your application is running, the failure URL allows you to handle errors even if your application is completely down. +If the URL is a different service other than your application, it will be reachable in these cases. -# Overview -Source: https://upstash.com/docs/workflow/features/retries +By pointing the failure URL to an external service (not hosted within your main application), you ensure that it remains accessible even when your primary app is unavailable. -Upstash Workflow provides an automatic retry mechanism to improve reliability and make workflows resilient against temporary failures. -Workflow automatically handles transient errors such as network issues or service unavailability. + -## How Retries Work +```typescript TypeScript +export const { POST } = serve( + async (context) => { + // Your workflow logic... + }, + { + failureUrl: "https:///workflow-failure", + } +); +``` -When a step fails, Upstash Workflow automatically retries the failed step with configurable retry attempts and delay strategy. -This allows temporary issues to resolve without manual intervention. +```python Python +@serve.post("/api/example", failureUrl="https:///...") +async def example(context: AsyncWorkflowContext[str]) -> None: ... +``` - + -By default, the retry count is set to **3**, and an **exponential backoff** delay strategy is used. +The callback body sent to you will be a JSON object with the following fields: -```javascript Default Backoff Algorithm -// n = how many times this request has been retried -delay = min(86400, e ** (2.5*n)) // in seconds +```javascript JavaScript +{ + "status": 200, + "header": { "key": ["value"] }, // Response header + "body": "YmFzZTY0IGVuY29kZWQgcm9keQ==", // base64 encoded response body + "retried": 2, // How many times we retried to deliver the original message + "maxRetries": 3, // Number of retries before the message assumed to be failed to delivered. + "sourceMessageId": "msg_xxx", // The ID of the message that triggered the callback + "topicName": "myTopic", // The name of the URL Group (topic) if the request was part of a URL Group + "endpointName": "myEndpoint", // The endpoint name if the endpoint is given a name within a topic + "url": "http://myurl.com", // The destination url of the message that triggered the callback + "method": "GET", // The http method of the message that triggered the callback + "sourceHeader": { "key": "value" }, // The http header of the message that triggered the callback + "sourceBody": "YmFzZTY0kZWQgcm9keQ==", // The base64 encoded body of the message that triggered the callback + "notBefore": "1701198458025", // The unix timestamp of the message that triggered the callback is/will be delivered in milliseconds + "createdAt": "1701198447054", // The unix timestamp of the message that triggered the callback is created in milliseconds + "scheduleId": "scd_xxx", // The scheduleId of the message if the message is triggered by a schedule + "callerIP": "178.247.74.179" // The IP address where the message that triggered the callback is published from +} ``` -| Retry Attempt | Algorithm | Delay | -|---------------|--------------|--------| -| 1 | $$e^{2.5}$$ | 12s | -| 2 | $$e^5$$ | 2m28s | -| 3 | $$e^{7.5}$$ | 30m8s | -| 4+ | $$86400$$ | 24h | +In Next.js you can use the following code to handle the callback: -## Configuration +```javascript JavaScript +// pages/api/callback.js -You can configure retry behavior when starting a new workflow run. +import { verifySignature } from "@upstash/qstash/nextjs"; -### Configure Retry Attempt Count +function handler(req, res) { + // responses from qstash are base64-encoded + const decoded = atob(req.body.body); + console.log(decoded); -You can specify how many times a step should be retried upon failure. + return res.status(200).end(); +} + +export default verifySignature(handler); + +export const config = { + api: { + bodyParser: false, + }, +}; +``` + +`verifySignature` allows to verify the signature of request, which is signed by Upstash using your signing keys. +If you don't want to verify the signature, you can remove `QSTASH_CURRENT_SIGNING_KEY` and `QSTASH_NEXT_SIGNING_KEY` environment variables and remove `verifySignature` function. + +## Manually Handling Failed Workflow Runs -```typescript Configure Retry Attempt Count -import { Client } from "@upstash/workflow"; +When a workflow run fails and is moved to the Dead Letter Queue (DLQ), you have several options to handle it manually via the REST API: -const client = new Client({ token: "" }) +### [Resume](/docs/workflow/api-reference/dlq/resume-workflow-from-dlq) +* **What it does:** Continues a failed workflow run from exactly where it failed, preserving all successful step results. +* **When to use:** Use this if you want to retry only the failed/pending steps without re-executing the entire workflow. -const { workflowRunId } = await client.trigger({ - url: "https:///", - retries: 3 -}) -``` +### [Restart](/docs/workflow/api-reference/dlq/restart-workflow-from-dlq) +* **What it does:** Starts the failed workflow run over from the beginning, discarding all previous step results. +* **When to use:** Use this if you want a clean execution, or if the failure may have been caused by a corrupted state that requires a fresh start. -### Configure Retry Delay Strategy +### [Callback](/docs/workflow/api-reference/dlq/retry-failure-callback) +* **What it does:** Reruns the failure callback for a workflow run, in case the original failure callback was not delivered or failed. +* **When to use:** Use this to ensure your system is notified of workflow failures, even if the original callback attempt did not succeed. -Retry delay is the time to wait before trying again after a failure. You can define a custom retry delay strategy. +## Debugging failed runs -The delay is defined as a math expression that is calculated on every retry. -The expression can use the `retried` variable, which represents how many times the step has already retried (starting from 0). +In your DLQ, filter messages via the `Workflow URL` or `Workflow Run ID` to search for a particular failure. We include all request and response headers and bodies to simplify debugging failed runs. -To apply a constant delay, you can simply provide a fixed value. +For example, let's debug the following failed run. Judging by the status code `404`, the `Ngrok-Error-Code` header of `ERR_NGROK_3200` and the returned HTML body, we know that the URL our workflow called does not exist. -The expression must return the delay in **milliseconds**. + -```typescript Configure Retry Delay Strategy -import { Client } from "@upstash/workflow"; +# Flow control +Source: https://upstash.com/docs/workflow/howto/flow-control -const client = new Client({ token: "" }) +# Development Server +Source: https://upstash.com/docs/workflow/howto/local-development/development-server -const { workflowRunId } = await client.trigger({ - url: "https:///", - retries: 3, - retryDelay: "(1 + retried) * 1000" -}) -``` +Upstash Workflow is built on top of Upstash QStash. +The QStash CLI provides a local development server that performs QStash functionality locally for development and testing purposes. -# Prevent Retries -Source: https://upstash.com/docs/workflow/features/retries/prevent-retries +## Automatic dev server (recommended) -It is recommended to enable retries for workflow runs to improve reliability. +If you are using `@upstash/workflow`, you can just set `QSTASH_DEV=true` in your environment, and the SDK will download and connect to the dev server automatically. No tokens or signing keys required. -However, in some cases, you may want to stop execution immediately when an error occurs, without causing additional retries. -Upstash Workflow provides several mechanisms to terminate workflow execution gracefully. +```bash .env +QSTASH_DEV=true +``` -## Using `WorkflowNonRetryableError` +With `QSTASH_DEV=true` set, both the workflow client and the `serve()` endpoint pick up the dev server automatically. The endpoint also verifies incoming signatures against the dev server's deterministic signing keys, so signature verification works end-to-end with no extra setup. -`WorkflowNonRetryableError` lets you explicitly fail a workflow without entering the retry cycle. +```typescript +// app/api/workflow/route.ts +import { serve } from "@upstash/workflow/nextjs"; -When thrown, the workflow run is marked as failed, which: -* Triggers the failure function (if defined) -* Sends the workflow run to the DLQ +export const { POST } = serve(async (context) => { + await context.run("step-1", () => console.log("running locally")); +}); +``` -```ts TypeScript highlight={7} -export const { POST } = serve<{ topic: string }>(async (context) => { - const payload = context.requestPayload +```typescript +import { Client } from "@upstash/workflow"; - const isExists = await context.run("is-user-exists", () => { ... }); +const client = new Client({ token: process.env.QSTASH_TOKEN ?? "" }); - if (!isExists) { - throw new WorkflowNonRetryableError("The user does not exists!") - } -}) +await client.trigger({ + url: "http://localhost:3000/api/workflow", +}); ``` -## Using `context.cancel()` +For details on the dev server behavior, ports, and the `registerQStashDev()` helper for Next.js edge routes, see the [QStash Local Development docs](/docs/qstash/howto/local-development). -You can cancel a workflow run explicitly from inside the workflow. +## Manual setup -When canceled, the run is labeled as canceled instead of failed. This means: -* The failure handler will **NOT** be triggered -* The workflow will **NOT** be sent to the DLQ +If you would rather start and manage the QStash dev server yourself, follow the steps below. - -```typescript highlight={10-11} TypeScript -export const { POST } = serve<{ orderId: string }>(async (context) => { - const { orderId } = context.requestPayload; + + + Start the development server using the QStash CLI: - // Check if order is still valid - const orderStatus = await context.run("check-order-status", async () => { - return await getOrderStatus(orderId); - }); + ```javascript + npx @upstash/qstash-cli dev + ``` - if (orderStatus === "cancelled") { - // Stop execution gracefully without error - await context.cancel(); - return; - } + The QStash CLI output will look something like this: - // Continue processing if order is valid - await context.run("process-order", async () => { - return await processOrder(orderId); - }); -}); -``` + ```plaintext QStash CLI Output + Upstash QStash development server is runnning at -```python Python -@serve.post("/graceful-cancellation") -async def graceful_cancellation(context: AsyncWorkflowContext[dict]) -> None: - order_id = context.request_payload["order_id"] + A default user has been created for you to authorize your requests. + QSTASH_TOKEN=eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0= + QSTASH_CURRENT_SIGNING_KEY=sig_7RvLjqfZBvP5KEUimQCE1pvpLuou + QSTASH_NEXT_SIGNING_KEY=sig_7W3ZNbfKWk5NWwEs3U4ixuQ7fxwE - async def _check_order_status(): - return await get_order_status(order_id) + Sample cURL request: + curl -X POST http://127.0.0.1:8080/v2/publish/https://example.com -H "Authorization: Bearer eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" - # Check if order is still valid - order_status = await context.run("check-order-status", _check_order_status) + Check out documentation for more details: + https://upstash.com/docs/qstash/howto/local-development + ``` - if order_status == "cancelled": - # Stop execution gracefully without error - await context.cancel() - return + For detailed instructions on setting up the development server, see our [QStash Local Development Guide](/docs/qstash/howto/local-development). + - # Continue processing if order is valid - async def _process_order(): - return await process_order(order_id) + + Once you start the local server, you can go to the Workflow tab on Upstash Console and enable local mode, which will allow you to monitor and debug workflow runs with the local server. - await context.run("process-order", _process_order) -``` - + + -## Using conditional execution + + Once your development server is running, update your environment variables to route QStash requests to your local server. -You can also use guard conditions to skip certain steps and exit early, without throwing errors or canceling the workflow. + ```env + QSTASH_URL="http://127.0.0.1:8080" + QSTASH_TOKEN="eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" + QSTASH_CURRENT_SIGNING_KEY="sig_7RvLjqfZBvP5KEUimQCE1pvpLuou" + QSTASH_NEXT_SIGNING_KEY="sig_7W3ZNbfKWk5NWwEs3U4ixuQ7fxwE" + ``` -In this case, the workflow run completes successfully because no error was raised. - - ```typescript TypeScript highlight={10-11} - export const { POST } = serve<{ data: any }>(async (context) => { - const { data } = context.requestPayload; + - // Check if order is still valid - const orderStatus = await context.run("check-order-status", async () => { - return await getOrderStatus(orderId); - }); + + It's all set up 🎉 - if (orderStatus === "not-found") { - // Stop execution without error - return; - } + Now, you can use your local address when triggering the workflow runs. - // Continue processing if order is valid - await context.run("process-order", async () => { - return await processOrder(orderId); - }); - }); - ``` + ```javascript + import { Client } from "@upstash/workflow"; - ```python Python - @serve.post("/conditional-execution") - async def conditional_execution(context: AsyncWorkflowContext[dict]) -> None: - data = context.request_payload["data"] + const client = Client() - async def _validate_data(): - return validate_input_data(data) + const { workflowRunId } = await client.trigger({ + url: `http://localhost:3000/api/workflow`, + retries: 3 + }); + ``` + + Inside the `trigger()` call, you need to provide the URL of your workflow endpoint: - # Validate data first - validation_result = await context.run("validate-data", _validate_data) + * Local development → use the URL where your app is running, for example: http://localhost:3000/api/PATH + * Production → use the URL of your deployed app, for example: https://yourapp.com/api/PATH - if not validation_result["is_valid"]: - # Log the validation failure - async def _log_validation_failure(): - await log_validation_error(validation_result["errors"]) + To avoid hardcoding URLs, you can define a `BASE_URL` constant and set it based on the environment. + A common pattern is to check an environment variable that only exists in production: - await context.run("log-validation-failure", _log_validation_failure) + ```javascript + const BASE_URL = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : `http://localhost:3000` - # Stop execution without error - return + const { workflowRunId } = await client.trigger({ + url: `${BASE_URL}/api/workflow`, + retries: 3 + }); + ``` + - # Only execute if validation passes - async def _process_valid_data(): - return await process_data(data) + - await context.run("process-valid-data", _process_valid_data) - ``` - + -# Sleep -Source: https://upstash.com/docs/workflow/features/sleep +# Local Tunnel +Source: https://upstash.com/docs/workflow/howto/local-development/local-tunnel -Upstash Workflow provides a **Sleep** feature that allows you to pause workflow execution for specified durations without consuming compute resources. +Upstash Workflow requires your application to be publicly accessible in production. +The recommended approach is running the development server we provide locally and work with local addresses. +An alternative is to making your application publibly accessible so that you can work with the managed Upstash Workflow servers. -This feature enables you to build time-based workflows, implement delays between steps, and create scheduled operations without the limitations of traditional serverless timeouts. +The easiest way to make a local URL publically available is [ngrok](https://ngrok.com), a free tunneling service. -## How Sleep Works +Create an account on [dashboard.ngrok.com/signup](https://dashboard.ngrok.com/signup) and follow the [setup instructions](https://dashboard.ngrok.com/get-started/setup) to download the ngrok CLI and connect your account. This process takes only a few minutes and is totally free. -When you use `context.sleep` or `context.sleepUntil` in your workflow, Upstash Workflow automatically pauses execution and schedules the next step to run after the specified delay. -This happens without keeping your serverless function running, making it cost-effective and reliable for long delays. +You can connect your account like this: - - **Important:** Sleep durations have limits based on your pricing plan: - * **Free**: Maximum delay of 7 days - * **Pay-as-you-go**: Maximum delay of 1 year - * **Fixed pricing**: Custom delays (no limit) - + + + + + + + + + -## Sleep Methods +Once you have installed the ngrok CLI, add your ngrok-issued auth token like this: -Upstash Workflow provides two methods for implementing delays in your workflows: +```bash Terminal +ngrok config add-authtoken +``` -### 1. context.sleep +and replace `` with your actual auth token. -Pauses workflow execution for a specified duration relative to the current time. +### Start the tunnel - -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; +Make your local server available publically by running the following command: -export const { POST } = serve(async (context) => { - const { userId } = context.requestPayload; +```bash +ngrok http +``` - // Send welcome email immediately - await context.run("send-welcome-email", async () => { - return await sendWelcomeEmail(userId); - }); +for example, if your Next.js server is running on port `3000`, the command is: - // Wait for 3 days before sending follow-up - await context.sleep("wait-for-follow-up", "3d"); +```bash +ngrok http 3000 +``` - // Send follow-up email - await context.run("send-follow-up-email", async () => { - return await sendFollowUpEmail(userId); - }); -}); +The output will look something like this: + +```plaintext +Session Status online +Account (Plan: Free) +Version 3.1.0 +Region Europe (eu) +Latency - +Web Interface http://127.0.0.1:4040 +Forwarding https://e02f-2a02-810d-af40-5284-b139-58cc-89df-b740.eu.ngrok.io -> http://localhost:3000 +Connections ttl opn rt1 rt5 p50 p90 + 0 0 0.00 0.00 0.00 0.00 ``` -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext +The long URL in the `Forwarding` line serves the same purpose as your localhost URL, the only difference being that it is publically accessible. We need this URL to make our workflow available to QStash for local development, either as the `baseUrl` parameter or the `UPSTASH_WORKFLOW_URL` environment variable (both options provide the same functionality). -app = FastAPI() -serve = Serve(app) +Note: The `UPSTASH_WORKFLOW_URL` environment variable is only necessary for local development. In production, the `baseUrl` parameter is automatically set and can be omitted. -@serve.post("/api/onboarding") -async def onboarding(context: AsyncWorkflowContext[str]) -> None: - user_id = context.request_payload["user_id"] + + Ensure that the port of your local server matches the one you're using with ngrok. For example, if your server is + running on port 8080, use `ngrok http 8080`. + - # Send welcome email immediately - async def _send_welcome_email(): - return await send_welcome_email(user_id) +# Middlewares +Source: https://upstash.com/docs/workflow/howto/middlewares - await context.run("send-welcome-email", _send_welcome_email) +Middlewares allow you to intercept and respond to workflow lifecycle events and debug messages. They're useful for logging, monitoring, error tracking, and custom integrations. - # Wait for 3 days before sending follow-up - await context.sleep("wait-for-follow-up", "3d") +## Overview - # Send follow-up email - async def _send_follow_up_email(): - return await send_follow_up_email(user_id) +A middleware can hook into: +* **Lifecycle Events**: Run started, run completed, before/after step execution +* **Debug Events**: Errors, warnings, and info messages - await context.run("send-follow-up-email", _send_follow_up_email) -``` - +## Built-in Middleware -You can specify durations using human-readable strings: +Upstash Workflow provides a built-in logging middleware that you can use out of the box: -* `"10s"` = 10 seconds -* `"1m"` = 1 minute -* `"30m"` = 30 minutes -* `"2h"` = 2 hours -* `"1d"` = 1 day -* `"1w"` = 1 week -* `"1mo"` = 1 month -* `"1y"` = 1 year +```typescript +import { serve } from "@upstash/workflow/nextjs"; +import { loggingMiddleware } from "@upstash/workflow"; -You can also use numeric values in seconds: +export const { POST } = serve( + async (context) => { + await context.run("step-1", () => { + return "Hello World"; + }); + }, + { + middlewares: [loggingMiddleware] + } +); +``` -* `60` = 60 seconds (1 minute) -* `3600` = 3600 seconds (1 hour) -* `86400` = 86400 seconds (1 day) +The logging middleware outputs detailed execution logs to your application's console, including: +* When workflow runs start and complete +* Before and after each step execution +* Error, warning, and info messages -### 2. context.sleepUntil +## Creating Custom Middleware -Pauses workflow execution until a specific timestamp in the future. +You can create your own middleware by instantiating a `WorkflowMiddleware` class. - -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; +### Using Direct Callbacks -export const { POST } = serve(async (context) => { - const { userId, scheduledTime } = context.requestPayload; +The simplest way to create a middleware is by providing callbacks directly: - // Calculate the scheduled time - const scheduledDate = new Date(scheduledTime); +```typescript +import { WorkflowMiddleware } from "@upstash/workflow"; - // Wait until the scheduled time - await context.sleepUntil("wait-until-scheduled", scheduledDate); +const customMiddleware = new WorkflowMiddleware({ + name: "custom-logger", + callbacks: { + // Lifecycle events + runStarted: async ({ context }) => { + console.log(`Workflow ${context.workflowRunId} started`); + }, + beforeExecution: async ({ context, stepName }) => { + console.log(`Executing step: ${stepName}`); + }, + afterExecution: async ({ context, stepName, result }) => { + console.log(`Step ${stepName} completed with result:`, result); + }, + runCompleted: async ({ context, result }) => { + console.log(`Workflow ${context.workflowRunId} completed:`, result); + }, - // Execute the scheduled task - await context.run("execute-scheduled-task", async () => { - return await executeTask(userId); - }); + // Debug events + onError: async ({ workflowRunId, error }) => { + console.error(`Error in ${workflowRunId}:`, error); + }, + onWarning: async ({ workflowRunId, warning }) => { + console.warn(`Warning in ${workflowRunId}:`, warning); + }, + onInfo: async ({ workflowRunId, info }) => { + console.info(`Info from ${workflowRunId}:`, info); + } + } }); ``` -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext -from datetime import datetime - -app = FastAPI() -serve = Serve(app) - -@serve.post("/api/scheduled-task") -async def scheduled_task(context: AsyncWorkflowContext[str]) -> None: - user_id = context.request_payload["user_id"] - scheduled_time = context.request_payload["scheduled_time"] +### Using Init Function - # Calculate the scheduled time - scheduled_date = datetime.fromisoformat(scheduled_time) +For middlewares that need to initialize resources (like database connections or external clients), use the `init` pattern: - # Wait until the scheduled time - await context.sleep_until("wait-until-scheduled", scheduled_date) +```typescript +import { WorkflowMiddleware } from "@upstash/workflow"; - # Execute the scheduled task - async def _execute_task(): - return await execute_task(user_id) +const databaseMiddleware = new WorkflowMiddleware({ + name: "database-logger", + init: async () => { + // Initialize your resources + const db = await connectToDatabase(); - await context.run("execute-scheduled-task", _execute_task) + // Return the callbacks that use the initialized resources + return { + runStarted: async ({ context }) => { + await db.insert({ workflowRunId: context.workflowRunId, status: 'started' }); + }, + runCompleted: async ({ context, result }) => { + await db.update({ workflowRunId: context.workflowRunId, status: 'completed', result }); + }, + onError: async ({ workflowRunId, error }) => { + await db.insert({ workflowRunId, level: 'error', message: error.message }); + } + }; + } +}); ``` - -For `context.sleepUntil`, you can use: +## Event Types -* `Date` objects (JavaScript/TypeScript) -* Unix timestamps (Python) -* ISO string dates +### Lifecycle Events - - Sleep operations have a precision of approximately 1 second. Very short delays (less than 1 second) may not be exact. - + + Called when a workflow run begins. -The sleep feature in Upstash Workflow provides a powerful way to create time-based, reliable workflows without the limitations of traditional serverless timeouts. + **Parameters:** + * `context`: The workflow context - By leveraging this feature, you can build sophisticated business logic that spans hours, days, or even months while maintaining cost efficiency and reliability. + ```typescript + runStarted: async ({ context }) => { + // Handle run start + } + ``` + -# Wait -Source: https://upstash.com/docs/workflow/features/wait + + Called before each step executes. -You can pause a workflow run with the `waitForEvent` step. An event is uniquely identified by event ID. + **Parameters:** + * `context`: The workflow context + * `stepName`: Name of the step about to execute -The workflow will resume when the matching event is published. + ```typescript + beforeExecution: async ({ context, stepName }) => { + // Handle step start + } + ``` + -`waitForEvent` supports configurable timeouts to prevent workflows from waiting indefinitely. -When a timeout occurs, the returned object includes `timeout: true`, allowing you to handle the failure case gracefully (for example, cancel an order, notify the user, or retry later). + + Called after each step completes. - -If no timeout is specified, the default is **7 days**. - + **Parameters:** + * `context`: The workflow context + * `stepName`: Name of the completed step + * `result`: The result returned by the step - -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; + ```typescript + afterExecution: async ({ context, stepName, result }) => { + // Handle step completion + } + ``` + -export const { POST } = serve(async (context) => { - const { orderId, userEmail } = context.requestPayload; + + Called when the entire workflow run finishes. - // Wait for order processing completion - const { eventData, timeout } = await context.waitForEvent( - "wait-for-order-processing", - `order-${orderId}`, - { - timeout: "1d" // 1 day timeout - } - ); + **Parameters:** + * `context`: The workflow context + * `result`: The final result of the workflow - if (timeout) { - // Handle timeout scenario - await context.run("handle-timeout", async () => { - return await handleOrderTimeout(orderId, userEmail); - }); - return; + ```typescript + runCompleted: async ({ context, result }) => { + // Handle run completion } + ``` + -}); -``` +### Debug Events -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext + + Called when an error occurs. -app = FastAPI() -serve = Serve(app) + **Parameters:** + * `workflowRunId`: The workflow run ID (optional) + * `error`: The error object -@serve.post("/api/order-processing") -async def order_processing(context: AsyncWorkflowContext[str]) -> None: - order_id = context.request_payload["order_id"] - user_email = context.request_payload["user_email"] + ```typescript + onError: async ({ workflowRunId, error }) => { + // Handle error + } + ``` + - # Send order processing request - async def _request_order_processing(): - return await request_order_processing(order_id) + + Called when a warning is logged. - await context.run("request-order-processing", _request_order_processing) + **Parameters:** + * `workflowRunId`: The workflow run ID (optional) + * `warning`: The warning message - # Wait for order processing completion - result = await context.wait_for_event( - "wait-for-order-processing", - f"order-{order_id}", - timeout="10m" # 10 minutes timeout - ) + ```typescript + onWarning: async ({ workflowRunId, warning }) => { + // Handle warning + } + ``` + - if result["timeout"]: - # Handle timeout scenario - async def _handle_timeout(): - return await handle_order_timeout(order_id, user_email) + + Called when an info message is logged. - await context.run("handle-timeout", _handle_timeout) - return + **Parameters:** + * `workflowRunId`: The workflow run ID (optional) + * `info`: The info message - # Process the completed order - async def _process_completed_order(): - return await process_completed_order(order_id, result["event_data"]) + ```typescript + onInfo: async ({ workflowRunId, info }) => { + // Handle info + } + ``` + - await context.run("process-completed-order", _process_completed_order) -``` - +## Examples -# Overview -Source: https://upstash.com/docs/workflow/features/wait-for-event +### Error Tracking Middleware -Wait for Event feature that allows you to pause workflow execution until an external event occurs. +Send errors to an external monitoring service: -This feature enables you to build asynchronous workflows that can wait for user interactions, external system responses, or any other events without consuming compute resources. +```typescript +import { WorkflowMiddleware } from "@upstash/workflow"; -## How Wait for Event Works +const errorTrackingMiddleware = new WorkflowMiddleware({ + name: "error-tracking", + callbacks: { + onError: async ({ workflowRunId, error }) => { + await fetch("https://your-monitoring-service.com/errors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + workflowRunId, + error: error.message, + stack: error.stack, + timestamp: new Date().toISOString() + }) + }); + } + } +}); +``` -When you use `context.waitForEvent()` in your workflow, Upstash Workflow automatically pauses execution and waits for an external notification to resume. -This happens without keeping your serverless function running, making it cost-effective and reliable for event-driven workflows. +### Multiple Middlewares -Each waiter has a timeout duration to wait for the event and then fires automatically. +You can use multiple middlewares together: - - Wait for Event timeouts have limits based on your pricing plan: - * **Free**: Maximum timeout of 7 days - * **Pay-as-you-go**: Maximum timeout of 1 year - * **Fixed pricing**: Custom timeouts (no limit) - +```typescript +import { serve } from "@upstash/workflow/nextjs"; +import { loggingMiddleware } from "@upstash/workflow"; -## Race Condition Between Wait and Notify +export const { POST } = serve( + async (context) => { + // Your workflow logic + }, + { + middlewares: [ + loggingMiddleware, + errorTrackingMiddleware, + performanceMiddleware + ] + } +); +``` -A race condition can occur when `notify` is called before `waitForEvent` is executed. -In this scenario, the notification will be sent but no workflow will be waiting to receive it, causing the event to be lost. +Middlewares are executed in the order they're provided in the array. -### Solutions +# Migration Guide +Source: https://upstash.com/docs/workflow/howto/migrations -There are three ways to handle race conditions: +This guide covers migration between different versions of Upstash Workflow. -1. **Use lookback with `workflowRunId`** (Recommended for targeted notifications) -2. **Use [Webhooks](/docs/workflow/features/webhooks)** (Recommended for general use) -3. **Check and retry** (Manual approach) + + + In January 2026, we released 1.0.0 version of the TypeScript SDK with several breaking changes to improve the developer experience, reduce bundle size, and simplify configuration. -#### Option 1: Lookback with workflowRunId + ## Agents API → Separate Package -When you know which specific workflow run should receive the notification, you can provide a `workflowRunId` to enable lookback. The notification will be stored and delivered even if sent before `waitForEvent`: + The Agents API has been moved to a separate package to remove the AI SDK dependency from the core workflow package. - -```typescript TypeScript -import { Client } from "@upstash/workflow"; + ### Migration Steps -const client = new Client({ token: "" }); + 1. Install the new package: -// Trigger a workflow -const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/process-order", - body: { orderId: "123" } -}); + ```bash + npm install @upstash/workflow-agents + ``` -// Immediately notify with lookback - no race condition! -await client.notify({ - eventId: "payment-verified", - eventData: { verified: true }, - workflowRunId: workflowRunId, // Enables lookback -}); -``` - + 2. Update your imports: -#### Option 2: Use Webhooks + ```typescript + // Old + import { serve } from "@upstash/workflow/nextjs"; -[Webhooks](/docs/workflow/features/webhooks) have built-in lookback and are safer against timing issues for general event handling. + export const { POST } = serve(async (context) => { + const model = context.agents.openai('gpt-3.5-turbo'); + const agent = context.agents.agent({ ... }); + const task = context.agents.task({ ... }); + }); -#### Option 3: Check and Retry + // New + import { serve } from "@upstash/workflow/nextjs"; + import { agentWorkflow } from "@upstash/workflow-agents"; -Alternatively, you can check the response of the `notify` operation and retry if needed: + export const { POST } = serve(async (context) => { + const agents = agentWorkflow(context) - -```typescript TypeScript -import { Client } from "@upstash/workflow"; + const model = agents.openai('gpt-3.5-turbo'); + const agent = agents.agent({ ... }); + const task = agents.task({ ... }); + }); + ``` -const client = new Client({ token: "" }); + See [Agents documentation](/docs/workflow/agents/overview) for more details. -const result = await client.notify({ - eventId, - eventData -}); + ## Removed `keepTriggerConfig` and `useFailureFunction` -// Check if any workflows were notified -if (result.waiters && result.waiters.length > 0) { - console.log(`Notified ${result.waiters.length} workflows`); - return result; -} + These parameters are no longer needed in `client.trigger()` as both are now `true` by default. -// If no workflows were waiting, wait and retry once -console.log("No workflows waiting, retrying in 5 seconds..."); -await new Promise(resolve => setTimeout(resolve, 5000)); + ### Migration Steps -return await client.notify({ - eventId, - eventData -}); -``` - + Simply remove these parameters from your trigger calls: -## Selecting an Event ID + ```typescript + // Old + const { workflowRunId } = await client.trigger({ + url: "https://your-app.com/api/workflow", + retries: 3, + keepTriggerConfig: true, + useFailureFunction: true + }); -When a workflow run waits on an event ID, it's appended to a list of waiters for the event ID. + // New + const { workflowRunId } = await client.trigger({ + url: "https://your-app.com/api/workflow", + retries: 3 + }); + ``` -When a notify request is sent, all workflow runs waiting for that event are notified sequentially. -To avoid heavy notify operations, it’s recommended to use unique event IDs instead of generic ones. -For example, instead of waiting on `user-sent-verification`, wait the workflow on `user-{userId}-sent-verification` event. + Configuration passed to `trigger()` now automatically applies to the entire workflow. -# Webhooks -Source: https://upstash.com/docs/workflow/features/webhooks + ## Configuration Moved from `serve` to `trigger` -The Webhook feature allows you to pause workflow execution and wait for a webhook URL to be called, enabling seamless integration with third-party APIs and asynchronous operations. + The `retries`, `flowControl`, `retryDelay`, and `failureUrl` options have been removed from `serve()` and should now be passed in `client.trigger()`. -This feature is perfect for scenarios where you need to: -* Wait for external API processing to complete -* Integrate with services that use callback URLs -* Receive notifications from external webhooks + ### Migration Steps -## How Webhooks Work + Move configuration from serve options to trigger: -When you use webhooks in your workflow, Upstash Workflow: + ```typescript + // Old + export const { POST } = serve( + async (context) => { ... }, + { + retries: 3, + retryDelay: "1000 * (1 + retried)", + flowControl: { key: "my-key", rate: 10 } + } + ); -1. **Creates a unique webhook URL** via `context.createWebhook()` -2. **Provides the URL to external services** (typically through `context.call()`) -3. **Pauses execution** via `context.waitForWebhook()` until the webhook is called -4. **Resumes workflow** when the webhook receives a request or timeout is reached + // Trigger call + await client.trigger({ url: "..." }); -This happens without keeping your serverless function running, making it cost-effective for long-running integrations. + // New + export const { POST } = serve( + async (context) => { ... } + // No configuration here anymore + ); -## Examples + // Configuration in trigger call + await client.trigger({ + url: "...", + retries: 3, + retryDelay: "1000 * (1 + retried)", + flowControl: { key: "my-key", rate: 10 } + }); + ``` -### Basic Usage + This change makes it easier to configure different behavior for different workflow runs of the same endpoint. -```typescript -import { serve } from "@upstash/workflow/nextjs"; + + In the Python SDK, these options remain in the `serve` decorator as they were before. + -export const { POST } = serve(async (context) => { - // Step 1: Create webhook - const webhook = await context.createWebhook("create webhook"); + ## Removed `stringifyBody` from `context.call` and `context.invoke` - // Step 2: Call an external endpoint, which calls the webhookUrl upon completion - const callResult = await context.call("call webhook caller", { - url: "https://webhook/caller", - method: "POST", - body: JSON.stringify({ - webhookUrl: webhook.webhookUrl, - }), - }); + The `stringifyBody` parameter has been removed. The `body` parameter now expects a string. - // Step 3: Wait for the webhook to be called - const webhookResponse = await context.waitForWebhook( - "wait for webhook", - webhook, - "30s" // timeout - ); + ### Migration Steps - if (webhookResponse.timeout) { - console.log("Webhook was not called in time"); - // Handle timeout scenario - } else { - console.log("Webhook received:", webhookResponse.request); - // Process the webhook data - } -}); -``` + Update your call and invoke methods to use `JSON.stringify()`: -### Waiting for Multiple Calls + ```typescript + // Old + const result = await context.call("call-api", { + url: "https://api.example.com/endpoint", + method: "POST", + body: { key: "value" }, + stringifyBody: true + }); -Some services send multiple progress updates to a webhook URL as processing continues. You can wait for the same webhook multiple times in a loop until you receive a final "finished" signal: + // New + const result = await context.call("call-api", { + url: "https://api.example.com/endpoint", + method: "POST", + body: JSON.stringify({ key: "value" }) + }); + ``` -```typescript -while (true) { - const webhookResponse = await context.waitForWebhook( - `wait for progress update ${stepCount}`, - webhook, - "5m" // 5 minute timeout between updates - ); + The same applies to `context.invoke()`: - if (webhookResponse.timeout) { - console.log("No progress update received in time, exiting"); - break; - } else { - const request = webhookResponse.request; - console.log("Progress update received:", await request.json()); + ```typescript + // Old + await context.invoke("invoke-workflow", { + workflow: otherWorkflow, + body: { key: "value" }, + stringifyBody: true + }); - if (request.headers.get("x-task-finished") === "true") { - console.log("Task finished, exiting loop"); - break; - } - } -} -``` + // New + await context.invoke("invoke-workflow", { + workflow: otherWorkflow, + body: JSON.stringify({ key: "value" }) + }); + ``` -## Race Condition Safety + ## Logger → Middleware System -Webhooks have built-in **lookback** protection, making them safer against race conditions. If an external service calls the webhook URL before `context.waitForWebhook()` is executed, the webhook call is stored and will be returned when `waitForWebhook` is called. + The logging system has been replaced with a more flexible middleware system. -This means you don't need to worry about timing issues between creating the webhook and waiting for it - the workflow will always receive the callback even if it arrives early. + ### Migration Steps -## Comparison with Wait for Event + Replace the old logger with the new middleware: -Webhooks and [Wait for Event](/docs/workflow/features/wait-for-event) serve similar purposes but with different approaches: + ```typescript + // Old + // Logging was automatic or controlled via verbose option + export const { POST } = serve( + async (context) => { ... }, + { verbose: true } + ); -| Feature | Webhooks | Wait for Event | -|---------|----------|----------------| -| **Trigger** | External HTTP call to unique URL | Notify via Upstash Workflow Client | -| **Setup** | Create webhook, pass URL to external service | Share event ID with external service | -| **Integration** | Easy with callback-based APIs | Requires Workflow Client integration | -| **Lookback** | ✅ Yes - safe against race conditions | ❌ No - notify before wait will be lost | -| **Best For** | Third-party API callbacks | Internal event notifications | + // New + import { loggingMiddleware } from "@upstash/workflow"; -Choose webhooks when integrating with external services that support callback URLs, or when you need protection against race conditions. Choose Wait for Event when you have control over the notification mechanism and can use the Workflow Client. + export const { POST } = serve( + async (context) => { ... }, + { + middlewares: [loggingMiddleware] + } + ); + ``` -## API Reference + You can also create custom middlewares for more control. See [Middlewares documentation](/docs/workflow/howto/middlewares) for details. - - - Create a unique webhook URL for external services to call - - - Wait for the webhook to be called or timeout - - + ## Removed `onStepFinish` -# Getting Started -Source: https://upstash.com/docs/workflow/getstarted + The `onStepFinish` callback has been removed. Use middlewares instead. -## Overview + ### Migration Steps -Upstash Workflow lets you write **durable, reliable and performant serverless functions**. Get delivery guarantees, automatic retries on failure, scheduling and more without managing any infrastructure. + Replace `onStepFinish` with a custom middleware: - + ```typescript + // Old + export const { POST } = serve( + async (context) => { ... }, + { + onStepFinish: (stepName, result) => { + console.log(`Step ${stepName} finished with:`, result); + } + } + ); -## Quickstarts + // New + import { WorkflowMiddleware } from "@upstash/workflow"; -Upstash Workflow supports Next.js, Cloudflare Workers and [many other frameworks](/docs/workflow/quickstarts/platforms) in TypeScript and Python. + const stepFinishMiddleware = new WorkflowMiddleware({ + name: "step-finish", + callbacks: { + afterExecution: async ({ stepName, result }) => { + console.log(`Step ${stepName} finished with:`, result); + } + } + }); - - - Build a Next.js application with QStash Workflow - - - Use and deploy Upstash Workflow on Cloudflare Workers - - - Use Upstash Workflow for Python with Next.js and FastAPI - - + export const { POST } = serve( + async (context) => { ... }, + { + middlewares: [stepFinishMiddleware] + } + ); + ``` -## Key Features + See [Middlewares documentation](/docs/workflow/howto/middlewares) for more details. - - - If your platform experiences a temporary outage, your workflow can pick up right where it left off, ensuring stability even in unstable environments. - - - Run long-running REST endpoints, such as complex AI models or video processing tools, even on serverless platforms with strict time limits. - - - Create workflows that wait for external events before proceeding. Ideal for user confirmations and asynchronous notifications. - - - Run jobs at regular intervals with support for cron expressions. Perfect for recurring tasks like reminders, reports, or newsletters. - - - Start independent tasks in parallel and wait for them to finish simultaneously, reducing latency. - - - Need your code to “sleep” for days, weeks, or even months? Supports long delays beyond serverless time limits. - - - Ensures at-least-once delivery. Failed requests are logged in a Dead Letter Queue to prevent data loss. - - - Prevent overwhelming your app or external services by configuring rate per second or parallelism limits. - - - Monitor workflow steps with insights. Filter events to track successes, failures, retries, and stalls. - - + -## Example Use Cases + + In October 2024, we released a new SDK, `@upstash/workflow`, for Upstash Workflow, separating its development from the QStash SDK. Although Upstash Workflow is built on QStash, our goal is to improve the developer experience and support with a dedicated SDK. Development for Upstash Workflow will occur in `@upstash/workflow`, and Workflow-related imports will be removed from `@upstash/qstash` in future releases. -Here are some example real world use-cases for Upstash Workflow: + If you started using Upstash Workflow with `@upstash/qstash`, you will need to migrate to `@upstash/workflow`. We have made some backward-incompatible changes, but we aim to make the transition as smooth as possible. + In this guide, we will explain the changes you may need to make for migration. - - - Use LLM Agents equipped with custom tools to achieve tasks - - - Download a large dataset without timeouts, process the data in chunks and generate a report. - - - Control workflow execution with events, log event data and send emails - - - Start a workflow from a webhook. Handle user creation, trial management, - email reminders and notifications. - - - Register a new user, send welcome emails, and periodically check and respond - to the user's activity state with emails. - - - Receive an order request, verify the stock, process the payment, and handle - order dispatch and customer notifications. - - - Manage uploading images to the data store. Apply filters and resize the - images to different resolutions in parallel. - - - Retry payments with a day of delay, send emails, and suspend account if - payment fails after the retries. - - + ### Install `@upstash/workflow` -## How it works + First, we will need to install the new package with: -Upstash Workflow builds on the principle of steps. Instead of defining a single, complex piece of business logic, workflows contain multiple individual steps. + + + ```bash + npm install @upstash/workflow + ``` + + + ```bash + pnpm install @upstash/workflow + ``` + + + ```bash + bun add @upstash/workflow + ``` + + -Each of the steps are executed by a separate request to your application, by preserving the output of previous steps. + If you were using `@upstash/qstash` only for workflow, you can uninstall it from your project. -In case of an error, a failed step is retried individually without needing to re-run any previous steps. Instead of the entire business logic, _each step_ can take up your serverless function execution duration, and many more benefits. + ### Serve methods - + You will need to change the imports from `@upstash/qstash` to @upstash/workflow: -## Support + ```ts + // old + import { serve } from "@upstash/qstash/nextjs" -Need help or have questions? We're here to support you: + // new + import { serve } from "@upstash/workflow/nextjs" + ``` -* Join our Discord community to ask questions and share feedback -* Open a ticket through the Intercom chatbox in the dashboard for any issue + We have updated what our `serve` methods return. We made this change to make it + easier to extend the API in the future. -# Cancel a Run -Source: https://upstash.com/docs/workflow/howto/cancel + For instance, Next.js method changed like this: -You can cancel a running workflow both programatically and from your Upstash Workflow console. + ```javascript + // old + export const POST = serve(...); -## Cancelling via console + // new + export const { POST } = serve(...); + ``` -In your Upstash Workflow console, find the run you'd like to cancel and press the `Cancel Workflow` button on the right side: + We kept the `serve` method of `Hono` the same. The rest are updated in a similar way. + See [the quickstarts](/docs/workflow/quickstarts/platforms) for the new way `serve` + should be used. - + Additionally, `@upstash/workflow/nuxt` import is removed. You should use `@upstash/workflow/h3` + instead. This change was made because `nuxt` uses `h3` under the hood and our `serve` method + for `nuxt` can work with any project using `h3`. -## Cancelling programatically + ### Updating `context.call` - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/docs/workflow/roadmap) for feature parity plans and - [Changelog](/docs/workflow/changelog) for updates. - + If you were using [`context.call` method](/docs/workflow/basics/context#context-call) in your workflow, you will need to change + how it's called and what it returns. Here is what the change looks like: -```javascript -import { Client } from "@upstash/workflow"; + ```javascript + // old + const result = await context.call("call step", "", "POST", ...) -const client = new Client({ token: "" }); -await client.cancel({ ids: "" }); -``` + // new + const { + status, // response status + headers, // response headers + body // response body + } = await context.call("call step", { + url: "", + method: "POST", + ... + }) + ``` -And replace `` with your actual run ID. See [the documentation of `client.cancel` method for more information about other ways of canceling workflows](/docs/workflow/basics/client/cancel). + In the old version, we only returned the response body. Also, if the request + to the url failed, [the workflow run would fail](/docs/workflow/howto/failures). -You can also use the [Upstash Workflow REST API](/docs/workflow/api-reference/runs/cancel-workflow-run) to cancel a run programatically. + In the new version, we update how the parameters are passed to the `context.call`. + Additionally, we change the fail behavior: if the request fails, it doesn't make the + workflow fail. Instead, the status and the body is simply returned and workflow + continues as usual. -# Update a Workflow -Source: https://upstash.com/docs/workflow/howto/changes + If you have ongoing workflow runs which call `context.call` during your transition, + `status` and `headers` fields may not be available in these old runs. After your + transition, all workflow runs will have all three fields. -Workflows are composed of multiple steps. When you modify workflow code, it's important to consider how these changes might affect in-progress workflows. + ### Renaming Errors -## Issues + The errors in Workflow were renamed from `QStashWorkflowError` and `QStashWorkflowAbort` to `WorkflowError` and `WorkflowAbort`. -You cannot change the step order of an existing workflow. + + -If your code changes remove or reorder existing steps, in-progress workflows may attempt to continue from a point that no longer exists. This can lead to workflow failures, typically resulting in the following error: +# Select a Region +Source: https://upstash.com/docs/workflow/howto/multi-region -```bash -HTTP status 400. Incompatible step name. Expected , got -``` +## Overview + +Upstash Workflow operates on top of QStash, which is available in two distinct regions: **EU region** and **US region**. Each region is completely independent with its own infrastructure, pricing, resources, and workflow runs. + +## Regional URLs + +* **EU Region**: `https://qstash-eu-central-1.upstash.io`, or `https://qstash.upstash.io` +* **US Region**: `https://qstash-us-east-1.upstash.io` + +## Key Concepts + +Each region maintains: +* Usage in each region is tracked and billed independently +* Workflow runs, events, and configurations are region-specific +* Each region has its own API tokens and signing keys -## Safe changes +### Migration Between Regions -Updating workflow code is safe in the following cases: +If you don't have any active resources (active workflow runs, schedules, url groups etc), you can simply update your environment variables with the new region to migrate. If you have active resources, you will need to migrate more gracefully, as described below. -* No active workflow runs exist -* Only new steps are added to the end of the workflow +You can migrate your Workflow resources from one region to another using the Upstash Console: -## Guidelines for updating workflows +1. Navigate to the [Workflow tab on Upstash Console](https://console.upstash.com/workflow) +2. Click the **Migrate** button +3. Follow the guided migration process -Consider the following approaches when updating your workflow code: + -* **Accept potential failures:** If you're fine with in-progress workflows failing, you can make any code changes. -* **Use a different route:** To avoid failures, consider serving the updated workflow under a different route. -* **Stop traffic before deployment:** If you need to keep the same route, stop all traffic before deploying new code. -* **Add steps only:** If stopping traffic is not an option, limit your changes to adding new steps at the end of the workflow. +The migration tool will: +* Help you set up migration-mode environment variables +* Copy and update your QStash resources (schedules, url groups, queues) -For a deeper understanding of these limitations, see our [how workflows work](/docs/workflow/basics/how) section. +Your workflow logs or DLQ aren't part of the migration. They will remain in the old region. -# Configure a Run -Source: https://upstash.com/docs/workflow/howto/configure + +After migration, your app will be able to handle requests from both regions simultaneously to ensure a smooth transition. + -You can configure a workflow run when starting it. The following are the options you can configure: +## Operating Modes -1. Retries: The number of retry attempt Upstash Workflow does when a step fails in the workflow run -2. Retry Delay: The delay strategy between retries when Upstash Workflow attempts retries. -3. Flow Control: The rate, period and parallelism that steps should respect and logical grouping key to share with other workflow runs. +Workflow SDK supports two modes of operation: -You can pass these configuration options when starting a workflow run: +### Single-Region Mode (Default) -```typescript -import { Client } from "@upstash/workflow"; +When `QSTASH_REGION` environment variable is **not set**, the SDK operates in single-region mode: -const client = Client() +* Uses `QSTASH_TOKEN` and `QSTASH_URL` (or defaults to EU region) +* All workflow triggers are sent through the configured region +* Incoming workflow requests are verified using default signing keys -const { workflowRunId } = await client.trigger({ - url: `http://localhost:3000/api/workflow`, - retries: 3, - retryDelay: "(1 + retries) * 1000", - flowControl: { - key: "limit-ads", - rate: 1, - parallelism: 10 - } -}); +```bash +# Single-region configuration (EU) +QSTASH_URL="https://qstash.upstash.io" +QSTASH_TOKEN="your_eu_token" +QSTASH_CURRENT_SIGNING_KEY="your_eu_current_key" +QSTASH_NEXT_SIGNING_KEY="your_eu_next_key" ``` -The workflow run configuration does **not** apply to `context.call()` and `context.invoke()` steps. -These steps accept their own configuration options, allowing fine-grained control over external requests. -If not specified, they fall back to their default values. +### Migration Mode -For details, see: -* [context.call](/docs/workflow/basics/context/run) -* [context.invoke](/docs/workflow/basics/context/run) +When `QSTASH_REGION` is set to `US_EAST_1` or `EU_CENTRAL_1`, the SDK enables migration mode: - -Upstash Workflow does not support step level configuration. The configuration applies to all steps executed by a workflow run. +* Uses region-specific credentials for the primary region (`QSTASH_REGION`) +* Automatically handles region detection for incoming workflow requests +* Supports receiving workflow calls from multiple regions simultaneously -If you want to specifically throttle a step, there is a workaround by splitting step to another workflow and using `context.invoke()`. - + + If a workflow run was started in one region, all its steps will execute in that region. + -# Handle Failed Runs -Source: https://upstash.com/docs/workflow/howto/failures +Environment variables: -This guide shows you how to **gracefully handle failed workflow runs**. This involves best practices on resolving runtime errors, logging and manually retrying runs that have failed multiple times. +```bash +# Migration mode configuration with US as primary +QSTASH_REGION="US_EAST_1" -## Why a workflow might fail +US_EAST_1_QSTASH_URL="https://qstash-us-east-1.upstash.io" +US_EAST_1_QSTASH_TOKEN="your_us_token" +US_EAST_1_QSTASH_CURRENT_SIGNING_KEY="your_us_current_key" +US_EAST_1_QSTASH_NEXT_SIGNING_KEY="your_us_next_key" -* A step in your workflow throws a database error that causes your code to fail at runtime. -* QStash calls your workflow URL, but the URL is not reachable - for example, because of a temporary outage of your deployment platform. -* A single step takes longer than your platform's function execution limit. +EU_CENTRAL_1_QSTASH_URL="https://qstash-eu-central-1.upstash.io" +EU_CENTRAL_1_QSTASH_TOKEN="your_eu_token" +EU_CENTRAL_1_QSTASH_CURRENT_SIGNING_KEY="your_eu_current_key" +EU_CENTRAL_1_QSTASH_NEXT_SIGNING_KEY="your_eu_next_key" +``` -Workflow automatically retries a failed step based on your configuration (by default, it retries three times with exponential backoff). -This helps handle temporary outages or intermittent failures gracefully. + +Migration mode relies on environment variables being available via `process.env`. It won't work on platforms where `process.env` is not available, such as Cloudflare Workers. + - +## SDK Requirements -If, even after all retries, your step does not succeed, we'll move the failed run into your [Dead Letter Queue (DLQ)](/docs/qstash/howto/handling-failures#dead-letter-queue). That way, you can always manually retry it again and debug the issue. +Migration support requires: +* `@upstash/workflow` >= 1.1.0 +* `@upstash/qstash` >= 2.9.0 - +Update your dependencies: -If you want to take an action (a cleanup/log), you can configure either `failureFunction` or a `failureUrl` on the `serve` method of your workflow. -These options allow you to define custom logic or an external endpoint that will be triggered when a failure occurs. +```bash +npm install @upstash/workflow@latest @upstash/qstash@latest +``` -## Using a `failureFunction` (recommended) +# Parallel Runs +Source: https://upstash.com/docs/workflow/howto/parallel-runs -The `serve` function you use to create a workflow endpoint accepts a `failureFunction` parameter - an easy way to gracefully handle errors (i.e. logging them to Sentry) or your custom handling logic. + + This feature is not yet available in + [workflow-py](https://github.com/upstash/workflow-py). See our + [Roadmap](/docs/workflow/roadmap) for feature parity plans and + [Changelog](/docs/workflow/changelog) for updates. + - -```typescript TypeScript -export const { POST } = serve( - async (context) => { - // Your workflow logic... - }, - { - failureFunction: async ({ - context, - failStatus, - failResponse, - failHeaders, - }) => { - // Handle error, i.e. log to Sentry - console.error("Workflow failed:", failResponse); +Just like you can execute multiple JavaScript promises at the same time using `Promise.all`, you can run multiple workflow steps at the same time: - // You can optionally return a string that will be visible - // in the UI (coming soon) and in workflow logs - return `Workflow failed with status ${failStatus}: ${failResponse}`; - }, - } -); +```typescript +const [result1, result2, result3] = + await Promise.all([ + ctx.run("parallel-step-1", async () => { ... }), + ctx.run("parallel-step-2", async () => { ... }), + ctx.run("parallel-step-3", async () => { ... }), + ]) ``` -```python Python -async def failure_function( - context, # context during failure - fail_status, # failure status - fail_response, # failure message - fail_headers # failure headers -): - # handle the failure - pass +In a complete code example, your workflow could look like this: -@serve.post("/api/example", failure_function=failure_function) -async def example(context: AsyncWorkflowContext[str]) -> None: ... -``` +```typescript app/api/workflow/route.ts +import { serve } from "@upstash/workflow/nextjs"; +import { checkInventory, brewCoffee, printReceipt } from "@/utils"; - +export const { POST } = serve(async (ctx) => { + const [coffeeBeansAvailable, cupsAvailable, milkAvailable] = + await Promise.all([ + ctx.run("check-coffee-beans", () => checkInventory("coffee-beans")), + ctx.run("check-cups", () => checkInventory("cups")), + ctx.run("check-milk", () => checkInventory("milk")), + ]); -Note: If you use a custom authorization method to secure your workflow endpoint, add authorization to the `failureFunction` too. Otherwise, anyone can invoke your failure function. Read more here: [securing your workflow endpoint](/docs/workflow/howto/security). + // If all ingedients available, brew coffee + if (coffeeBeansAvailable && cupsAvailable && milkAvailable) { + const price = await ctx.run("brew-coffee", async () => { + return await brewCoffee({ style: "cappuccino" }); + }); -In `@upstash/workflow`, the `failureFunction` can optionally return a string value that will be displayed in the UI (coming soon) and included in the workflow logs. This is useful for providing custom error messages, debugging information, or tracking specific failure conditions. + await printReceipt(price); + } +}); +``` -## Using a `failureUrl` +After running your workflow, your dashboard shows each step in detail: -Instead of using the built-in failure function, you can define a separate failure callback URL. -Unlike the failure function, which only works when your application is running, the failure URL allows you to handle errors even if your application is completely down. -If the URL is a different service other than your application, it will be reachable in these cases. + -By pointing the failure URL to an external service (not hosted within your main application), you ensure that it remains accessible even when your primary app is unavailable. +# Realtime Quickstart +Source: https://upstash.com/docs/workflow/howto/realtime/basic - +[**Upstash Realtime**](/docs/realtime/overall/quickstart) lets you emit events from your workflow and subscribe to them in real-time on your frontend. -```typescript TypeScript -export const { POST } = serve( - async (context) => { - // Your workflow logic... - }, - { - failureUrl: "https:///workflow-failure", - } -); -``` +## How It Works -```python Python -@serve.post("/api/example", failureUrl="https:///...") -async def example(context: AsyncWorkflowContext[str]) -> None: ... -``` +Upstash Realtime is powered by Upstash Redis and provides a clean, 100% type-safe API for publishing and subscribing to events: - +* Your frontend can subscribe to events +* When you **emit** an event, it's instantly delivered to live subscribers on the frontend +* You can also replay events that happened in the past -The callback body sent to you will be a JSON object with the following fields: +This guide shows you how to integrate Upstash Workflow with Upstash Realtime to display real-time progress updates in your frontend. -```javascript JavaScript -{ - "status": 200, - "header": { "key": ["value"] }, // Response header - "body": "YmFzZTY0IGVuY29kZWQgcm9keQ==", // base64 encoded response body - "retried": 2, // How many times we retried to deliver the original message - "maxRetries": 3, // Number of retries before the message assumed to be failed to delivered. - "sourceMessageId": "msg_xxx", // The ID of the message that triggered the callback - "topicName": "myTopic", // The name of the URL Group (topic) if the request was part of a URL Group - "endpointName": "myEndpoint", // The endpoint name if the endpoint is given a name within a topic - "url": "http://myurl.com", // The destination url of the message that triggered the callback - "method": "GET", // The http method of the message that triggered the callback - "sourceHeader": { "key": "value" }, // The http header of the message that triggered the callback - "sourceBody": "YmFzZTY0kZWQgcm9keQ==", // The base64 encoded body of the message that triggered the callback - "notBefore": "1701198458025", // The unix timestamp of the message that triggered the callback is/will be delivered in milliseconds - "createdAt": "1701198447054", // The unix timestamp of the message that triggered the callback is created in milliseconds - "scheduleId": "scd_xxx", // The scheduleId of the message if the message is triggered by a schedule - "callerIP": "178.247.74.179" // The IP address where the message that triggered the callback is published from -} -``` +## Setup -In Next.js you can use the following code to handle the callback: +### 1. Install Packages -```javascript JavaScript -// pages/api/callback.js +```bash +npm install @upstash/workflow @upstash/realtime @upstash/redis zod +``` -import { verifySignature } from "@upstash/qstash/nextjs"; +### 2. Configure Upstash Realtime -function handler(req, res) { - // responses from qstash are base64-encoded - const decoded = atob(req.body.body); - console.log(decoded); +Create a Realtime instance in `lib/realtime.ts`: - return res.status(200).end(); -} +```typescript title="lib/realtime.ts" +import { InferRealtimeEvents, Realtime } from "@upstash/realtime"; +import { Redis } from "@upstash/redis"; +import z from "zod/v4"; -export default verifySignature(handler); +const redis = Redis.fromEnv(); -export const config = { - api: { - bodyParser: false, +const schema = { + workflow: { + runFinish: z.object({}), + stepFinish: z.object({ + stepName: z.string(), + result: z.unknown().optional(), + }), }, }; + +export const realtime = new Realtime({ schema, redis }); +export type RealtimeEvents = InferRealtimeEvents; ``` -`verifySignature` allows to verify the signature of request, which is signed by Upstash using your signing keys. -If you don't want to verify the signature, you can remove `QSTASH_CURRENT_SIGNING_KEY` and `QSTASH_NEXT_SIGNING_KEY` environment variables and remove `verifySignature` function. +### 3. Create a Realtime Middleware -## Manually Handling Failed Workflow Runs +Create a custom middleware that will emit events to Realtime at `lib/middleware.ts`: -When a workflow run fails and is moved to the Dead Letter Queue (DLQ), you have several options to handle it manually via the REST API: +```typescript title="lib/middleware.ts" +import { WorkflowMiddleware } from "@upstash/workflow"; +import { realtime } from "./realtime"; -### [Resume](/docs/workflow/api-reference/dlq/resume-workflow-from-dlq) -* **What it does:** Continues a failed workflow run from exactly where it failed, preserving all successful step results. -* **When to use:** Use this if you want to retry only the failed/pending steps without re-executing the entire workflow. +export const realtimeMiddleware = new WorkflowMiddleware({ + name: "realtime-events", + callbacks: { + afterExecution: async ({ context, stepName, result }) => { + const channel = realtime.channel(context.workflowRunId); + await channel.emit("workflow.stepFinish", { + stepName, + result, + }); + }, + runCompleted: async ({ context }) => { + const channel = realtime.channel(context.workflowRunId); + await channel.emit("workflow.runFinish", {}); + }, + }, +}); +``` -### [Restart](/docs/workflow/api-reference/dlq/restart-workflow-from-dlq) -* **What it does:** Starts the failed workflow run over from the beginning, discarding all previous step results. -* **When to use:** Use this if you want a clean execution, or if the failure may have been caused by a corrupted state that requires a fresh start. +**Key points:** -### [Callback](/docs/workflow/api-reference/dlq/retry-failure-callback) -* **What it does:** Reruns the failure callback for a workflow run, in case the original failure callback was not delivered or failed. -* **When to use:** Use this to ensure your system is notified of workflow failures, even if the original callback attempt did not succeed. +* The `afterExecution` callback is triggered after each workflow step completes +* The `runCompleted` callback is triggered when the entire workflow finishes +* We use `context.workflowRunId` to create a unique channel for each workflow run +* Events are automatically emitted without needing to manually call emit inside your workflow steps -## Debugging failed runs +### 4. Create a Realtime Endpoint -In your DLQ, filter messages via the `Workflow URL` or `Workflow Run ID` to search for a particular failure. We include all request and response headers and bodies to simplify debugging failed runs. +Create an API route at `app/api/realtime/route.ts` to handle Realtime connections: -For example, let's debug the following failed run. Judging by the status code `404`, the `Ngrok-Error-Code` header of `ERR_NGROK_3200` and the returned HTML body, we know that the URL our workflow called does not exist. +```typescript title="app/api/realtime/route.ts" +import { handle } from "@upstash/realtime"; +import { realtime } from "@/lib/realtime"; - +export const GET = handle({ realtime }); +``` -# Flow control -Source: https://upstash.com/docs/workflow/howto/flow-control +This endpoint enables Server-Sent Events (SSE) connections for real-time updates. -# Development Server -Source: https://upstash.com/docs/workflow/howto/local-development/development-server +### 5. Add the Realtime Provider -Upstash Workflow is built on top of Upstash QStash. -The QStash CLI provides a local development server that performs QStash functionality locally for development and testing purposes. +Wrap your application in the `RealtimeProvider` by updating your root layout at `app/layout.tsx`: -## Automatic dev server (recommended) +```tsx title="app/layout.tsx" +"use client"; -If you are using `@upstash/workflow`, you can just set `QSTASH_DEV=true` in your environment, and the SDK will download and connect to the dev server automatically. No tokens or signing keys required. +import { RealtimeProvider } from "@upstash/realtime/client"; -```bash .env -QSTASH_DEV=true +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} ``` -With `QSTASH_DEV=true` set, both the workflow client and the `serve()` endpoint pick up the dev server automatically. The endpoint also verifies incoming signatures against the dev server's deterministic signing keys, so signature verification works end-to-end with no extra setup. +### 6. Create a Typed Client Hook -```typescript -// app/api/workflow/route.ts -import { serve } from "@upstash/workflow/nextjs"; +Create a typed `useRealtime` hook at `lib/realtime-client.ts`: -export const { POST } = serve(async (context) => { - await context.run("step-1", () => console.log("running locally")); -}); +```typescript title="lib/realtime-client.ts" +"use client"; + +import { createRealtime } from "@upstash/realtime/client"; +import type { RealtimeEvents } from "./realtime"; + +export const { useRealtime } = createRealtime(); ``` -```typescript -import { Client } from "@upstash/workflow"; +*** -const client = new Client({ token: process.env.QSTASH_TOKEN ?? "" }); +## Building the Workflow -await client.trigger({ - url: "http://localhost:3000/api/workflow", -}); -``` +### 1. Create the Workflow Endpoint -For details on the dev server behavior, ports, and the `registerQStashDev()` helper for Next.js edge routes, see the [QStash Local Development docs](/docs/qstash/howto/local-development). +Create your workflow at `app/api/workflow/route.ts`: -## Manual setup +```typescript title="app/api/workflow/route.ts" +import { serve } from "@upstash/workflow/nextjs"; +import { realtimeMiddleware } from "@/lib/middleware"; -If you would rather start and manage the QStash dev server yourself, follow the steps below. +type WorkflowPayload = { + userId: string; + action: string; +}; - - - Start the development server using the QStash CLI: +export const { POST } = serve( + async (context) => { + const { userId, action } = context.requestPayload; - ```javascript - npx @upstash/qstash-cli dev - ``` + await context.run("validate-data", async () => { + return { valid: true, userId, action }; + }); - The QStash CLI output will look something like this: + await context.run("process-action", async () => { + // Your business logic here + return { processed: true, userId, action }; + }); - ```plaintext QStash CLI Output - Upstash QStash development server is runnning at + return { success: true, workflowRunId: context.workflowRunId }; + }, + { + middlewares: [realtimeMiddleware], + } +); +``` - A default user has been created for you to authorize your requests. - QSTASH_TOKEN=eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0= - QSTASH_CURRENT_SIGNING_KEY=sig_7RvLjqfZBvP5KEUimQCE1pvpLuou - QSTASH_NEXT_SIGNING_KEY=sig_7W3ZNbfKWk5NWwEs3U4ixuQ7fxwE +**Key points:** - Sample cURL request: - curl -X POST http://127.0.0.1:8080/v2/publish/https://example.com -H "Authorization: Bearer eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" +* Import the `realtimeMiddleware` from `@/lib/middleware` +* Pass the middleware in the `middlewares` array to the `serve` function +* The middleware automatically emits events after each step and when the workflow completes - Check out documentation for more details: - https://upstash.com/docs/qstash/howto/local-development - ``` +### 2. Create a Trigger Endpoint - For detailed instructions on setting up the development server, see our [QStash Local Development Guide](/docs/qstash/howto/local-development). - +Create an endpoint to trigger workflows at `app/api/trigger/route.ts`: - - Once you start the local server, you can go to the Workflow tab on Upstash Console and enable local mode, which will allow you to monitor and debug workflow runs with the local server. +```typescript title="app/api/trigger/route.ts" +import { NextRequest, NextResponse } from "next/server"; +import { Client } from "@upstash/workflow"; - - +export const workflowClient = new Client({ + token: process.env.QSTASH_TOKEN, + baseUrl: process.env.QSTASH_URL, +}); - - Once your development server is running, update your environment variables to route QStash requests to your local server. +export async function POST(request: NextRequest) { + const workflowUrl = `${request.nextUrl.origin}/api/workflow`; - ```env - QSTASH_URL="http://127.0.0.1:8080" - QSTASH_TOKEN="eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" - QSTASH_CURRENT_SIGNING_KEY="sig_7RvLjqfZBvP5KEUimQCE1pvpLuou" - QSTASH_NEXT_SIGNING_KEY="sig_7W3ZNbfKWk5NWwEs3U4ixuQ7fxwE" - ``` + const { workflowRunId } = await workflowClient.trigger({ + url: workflowUrl, + body: { + userId: "user-123", + action: "process-data", + }, + }); - + return NextResponse.json({ workflowRunId }); +} +``` - - It's all set up 🎉 +*** - Now, you can use your local address when triggering the workflow runs. +## Building the Frontend - ```javascript - import { Client } from "@upstash/workflow"; +### 1. Create a Custom Hook - const client = Client() +Create a React hook to manage the Realtime subscription at `hooks/useWorkflow.ts`: - const { workflowRunId } = await client.trigger({ - url: `http://localhost:3000/api/workflow`, - retries: 3 - }); - ``` - - Inside the `trigger()` call, you need to provide the URL of your workflow endpoint: +```typescript +"use client"; - * Local development → use the URL where your app is running, for example: http://localhost:3000/api/PATH - * Production → use the URL of your deployed app, for example: https://yourapp.com/api/PATH +import { useRealtime } from "@/lib/realtime-client"; +import { useState, useCallback } from "react"; - To avoid hardcoding URLs, you can define a `BASE_URL` constant and set it based on the environment. - A common pattern is to check an environment variable that only exists in production: +interface WorkflowStep { + stepName: string; + result?: unknown; +} + +export function useWorkflow() { + const [workflowRunId, setWorkflowRunId] = useState(null); + const [steps, setSteps] = useState([]); + const [isRunFinished, setIsRunFinished] = useState(false); + + useRealtime({ + enabled: Boolean(workflowRunId), + channels: workflowRunId ? [workflowRunId] : [], + events: ["workflow.stepFinish", "workflow.runFinish"], + onData({ event, data }) { + if (event === "workflow.stepFinish") { + setSteps((prev) => [...prev, data]); + } - ```javascript - const BASE_URL = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : `http://localhost:3000` + if (event === "workflow.runFinish") { + setIsRunFinished(true); + } + }, + }); - const { workflowRunId } = await client.trigger({ - url: `${BASE_URL}/api/workflow`, - retries: 3 - }); - ``` - + const trigger = () => { + setSteps([]); + setIsRunFinished(false); - + const response = await fetch("/api/trigger", { + method: "POST", + }); - + const data = await response.json(); + setWorkflowRunId(data.workflowRunId); + }; -# Local Tunnel -Source: https://upstash.com/docs/workflow/howto/local-development/local-tunnel + return { + trigger, + workflowRunId, + steps, + isRunFinished, + }; +} +``` -Upstash Workflow requires your application to be publicly accessible in production. -The recommended approach is running the development server we provide locally and work with local addresses. -An alternative is to making your application publibly accessible so that you can work with the managed Upstash Workflow servers. +**Key features:** -The easiest way to make a local URL publically available is [ngrok](https://ngrok.com), a free tunneling service. +* Subscribe to multiple events using the `events` array: `["workflow.stepFinish", "workflow.runFinish"]` +* The hook manages both triggering the workflow and subscribing to updates +* Type-safe event handling with TypeScript -Create an account on [dashboard.ngrok.com/signup](https://dashboard.ngrok.com/signup) and follow the [setup instructions](https://dashboard.ngrok.com/get-started/setup) to download the ngrok CLI and connect your account. This process takes only a few minutes and is totally free. +### 2. Use the Hook in Your Component -You can connect your account like this: +```tsx +"use client"; - - - - +import { useWorkflow } from "@/hooks/useWorkflow"; - - - - +export default function WorkflowPage() { + const { trigger, steps, isRunFinished } = useWorkflow(); -Once you have installed the ngrok CLI, add your ngrok-issued auth token like this: + return ( +
+ -```bash Terminal -ngrok config add-authtoken -``` + {isRunFinished &&

✅ Workflow Finished!

} -and replace `` with your actual auth token. +

Workflow Steps:

-### Start the tunnel + {steps.map((step, index) => ( +
+ {step.stepName} + {Boolean(step.result) && : {JSON.stringify(step.result)}} +
+ ))} +
+ ); +} +``` -Make your local server available publically by running the following command: +## How It All Works Together -```bash -ngrok http -``` +1. **User triggers workflow**: The frontend calls `/api/trigger`, which returns a `workflowRunId` +2. **Frontend subscribes**: Using the `workflowRunId`, the frontend subscribes to the Realtime channel +3. **Workflow executes**: The workflow runs as a background job, emitting events at each step +4. **Real-time updates**: As the workflow emits events, they're instantly delivered to the frontend via Server-Sent Events -for example, if your Next.js server is running on port `3000`, the command is: +## Full Example -```bash -ngrok http 3000 -``` +For a complete working example with all steps, error handling, and UI components, check out the [Upstash Realtime example on GitHub](https://github.com/upstash/workflow-js/tree/main/examples/upstash-realtime). -The output will look something like this: +## Next Steps -```plaintext -Session Status online -Account (Plan: Free) -Version 3.1.0 -Region Europe (eu) -Latency - -Web Interface http://127.0.0.1:4040 -Forwarding https://e02f-2a02-810d-af40-5284-b139-58cc-89df-b740.eu.ngrok.io -> http://localhost:3000 -Connections ttl opn rt1 rt5 p50 p90 - 0 0 0.00 0.00 0.00 0.00 -``` +* Learn about [human-in-the-loop workflows with Realtime](./human-in-the-loop) +* Explore [Realtime features](/docs/realtime/overall/quickstart) +* Check out [Workflow configuration options](/docs/workflow/howto/configure) -The long URL in the `Forwarding` line serves the same purpose as your localhost URL, the only difference being that it is publically accessible. We need this URL to make our workflow available to QStash for local development, either as the `baseUrl` parameter or the `UPSTASH_WORKFLOW_URL` environment variable (both options provide the same functionality). +# Human-in-the-Loop +Source: https://upstash.com/docs/workflow/howto/realtime/human-in-the-loop -Note: The `UPSTASH_WORKFLOW_URL` environment variable is only necessary for local development. In production, the `baseUrl` parameter is automatically set and can be omitted. +Some workflows require human approval or input before proceeding. When combined with [**Upstash Realtime**](/docs/realtime/overall/quickstart), you can create interactive workflows that pause for user input and provide real-time feedback to your frontend during the entire process. - - Ensure that the port of your local server matches the one you're using with ngrok. For example, if your server is - running on port 8080, use `ngrok http 8080`. - +This guide shows you how to implement a human-in-the-loop workflow pattern with real-time updates using Upstash Workflow and Upstash Realtime. -# Middlewares -Source: https://upstash.com/docs/workflow/howto/middlewares +## How It Works -Middlewares allow you to intercept and respond to workflow lifecycle events and debug messages. They're useful for logging, monitoring, error tracking, and custom integrations. +In a human-in-the-loop workflow: -## Overview +1. The workflow executes initial steps and emits progress events +2. The workflow pauses at a specific point using [`context.waitForEvent()`](/docs/workflow/features/wait-for-event) +3. A "waiting for input" event is emitted to notify the frontend +4. The user makes a decision in the frontend (approve/reject) +5. The frontend calls an API to notify the workflow using [`client.notify()`](/docs/workflow/basics/client/notify) +6. The workflow resumes with the user's decision +7. An "input resolved" event is emitted so the frontend can update its UI +8. The workflow continues and completes based on the decision -A middleware can hook into: -* **Lifecycle Events**: Run started, run completed, before/after step execution -* **Debug Events**: Errors, warnings, and info messages +## Prerequisites -## Built-in Middleware +* An Upstash account with: + * A QStash project for workflows + * A Redis database for Realtime +* Next.js application set up +* Completed the [basic real-time workflow setup](./basic) -Upstash Workflow provides a built-in logging middleware that you can use out of the box: +## Event Types -```typescript -import { serve } from "@upstash/workflow/nextjs"; -import { loggingMiddleware } from "@upstash/workflow"; +For human-in-the-loop workflows, extend your schema in `lib/realtime.ts` with these additional event types: -export const { POST } = serve( - async (context) => { - await context.run("step-1", () => { - return "Hello World"; - }); +```typescript {8-14} +const schema = { + workflow: { + runFinish: z.object({}), + stepFinish: z.object({ + stepName: z.string(), + result: z.unknown().optional(), + }), + waitingForInput: z.object({ + eventId: z.string(), + message: z.string(), + }), + inputResolved: z.object({ + eventId: z.string(), + }), }, - { - middlewares: [loggingMiddleware] - } -); +}; ``` -The logging middleware outputs detailed execution logs to your application's console, including: -* When workflow runs start and complete -* Before and after each step execution -* Error, warning, and info messages - -## Creating Custom Middleware +The new event types are: -You can create your own middleware by instantiating a `WorkflowMiddleware` class. +* **`waitingForInput`**: Emitted when the workflow pauses and needs user input +* **`inputResolved`**: Emitted when the user provides input, so the frontend knows to clear the waiting state -### Using Direct Callbacks +## Create the Realtime Middleware -The simplest way to create a middleware is by providing callbacks directly: +Create a custom middleware that will emit events to Realtime at `lib/middleware.ts`: -```typescript +```typescript title="lib/middleware.ts" import { WorkflowMiddleware } from "@upstash/workflow"; +import { realtime } from "./realtime"; -const customMiddleware = new WorkflowMiddleware({ - name: "custom-logger", +export const realtimeMiddleware = new WorkflowMiddleware({ + name: "realtime-events", callbacks: { - // Lifecycle events - runStarted: async ({ context }) => { - console.log(`Workflow ${context.workflowRunId} started`); - }, beforeExecution: async ({ context, stepName }) => { - console.log(`Executing step: ${stepName}`); - }, - afterExecution: async ({ context, stepName, result }) => { - console.log(`Step ${stepName} completed with result:`, result); - }, - runCompleted: async ({ context, result }) => { - console.log(`Workflow ${context.workflowRunId} completed:`, result); - }, + const channel = realtime.channel(context.workflowRunId); - // Debug events - onError: async ({ workflowRunId, error }) => { - console.error(`Error in ${workflowRunId}:`, error); - }, - onWarning: async ({ workflowRunId, warning }) => { - console.warn(`Warning in ${workflowRunId}:`, warning); + // Detect wait-for-event steps and emit waitingForInput + if (stepName === "wait-for-approval") { + await channel.emit("workflow.waitingForInput", { + eventId: `approval-${context.workflowRunId}`, + message: `Waiting for approval`, + }); + } }, - onInfo: async ({ workflowRunId, info }) => { - console.info(`Info from ${workflowRunId}:`, info); - } - } -}); -``` - -### Using Init Function - -For middlewares that need to initialize resources (like database connections or external clients), use the `init` pattern: - -```typescript -import { WorkflowMiddleware } from "@upstash/workflow"; - -const databaseMiddleware = new WorkflowMiddleware({ - name: "database-logger", - init: async () => { - // Initialize your resources - const db = await connectToDatabase(); - - // Return the callbacks that use the initialized resources - return { - runStarted: async ({ context }) => { - await db.insert({ workflowRunId: context.workflowRunId, status: 'started' }); - }, - runCompleted: async ({ context, result }) => { - await db.update({ workflowRunId: context.workflowRunId, status: 'completed', result }); - }, - onError: async ({ workflowRunId, error }) => { - await db.insert({ workflowRunId, level: 'error', message: error.message }); + afterExecution: async ({ context, stepName, result }) => { + const channel = realtime.channel(context.workflowRunId); + + // Emit inputResolved after wait-for-event steps complete + if (stepName === "wait-for-approval") { + await channel.emit("workflow.inputResolved", { + eventId: `approval-${context.workflowRunId}`, + }); } - }; - } + + // Emit stepFinish for all steps + await channel.emit("workflow.stepFinish", { + stepName, + result, + }); + }, + runCompleted: async ({ context }) => { + const channel = realtime.channel(context.workflowRunId); + await channel.emit("workflow.runFinish", {}); + }, + }, }); ``` -## Event Types - -### Lifecycle Events - - - Called when a workflow run begins. +**Key points:** - **Parameters:** - * `context`: The workflow context +* The middleware handles all realtime event emissions automatically +* `beforeExecution`: Detects wait-for-event steps by checking the stepName and emits `workflow.waitingForInput` +* `afterExecution`: Emits `workflow.inputResolved` for wait steps and `workflow.stepFinish` for all steps +* `runCompleted`: Emits `workflow.runFinish` when the workflow finishes +* All emission logic is centralized in the middleware, keeping workflow code clean - ```typescript - runStarted: async ({ context }) => { - // Handle run start - } - ``` - +## Building the Workflow - - Called before each step executes. +### 1. Create the Workflow Endpoint - **Parameters:** - * `context`: The workflow context - * `stepName`: Name of the step about to execute +Create your workflow at `app/api/workflow/human-in-loop/route.ts`: - ```typescript - beforeExecution: async ({ context, stepName }) => { - // Handle step start - } - ``` - +```typescript title="app/api/workflow/human-in-loop/route.ts" +import { serve } from "@upstash/workflow/nextjs"; +import { realtimeMiddleware } from "@/lib/middleware"; - - Called after each step completes. +type WorkflowPayload = { + userId: string; + action: string; +}; - **Parameters:** - * `context`: The workflow context - * `stepName`: Name of the completed step - * `result`: The result returned by the step +export const { POST } = serve( + async (context) => { + const { userId, action } = context.requestPayload; - ```typescript - afterExecution: async ({ context, stepName, result }) => { - // Handle step completion - } - ``` - + // Step 1: Initial Processing + await context.run("initial-processing", async () => { + // Your processing logic + return { + preprocessed: true, + userId, + action, + requiresApproval: true, + }; + }); - - Called when the entire workflow run finishes. + // Step 2: Wait for Human Approval + const eventId = `approval-${context.workflowRunId}`; - **Parameters:** - * `context`: The workflow context - * `result`: The final result of the workflow + const { eventData, timeout } = await context.waitForEvent<{ + approved: boolean; + }>("wait-for-approval", eventId, { timeout: "5m" }); - ```typescript - runCompleted: async ({ context, result }) => { - // Handle run completion - } - ``` - + // Handle timeout + if (timeout) { + return { success: false, reason: "timeout" }; + } -### Debug Events + const status = eventData.approved ? "approved" : "rejected"; - - Called when an error occurs. + // Step 3: Process based on approval + await context.run(`process-${status}`, async () => { + return { + status, + processedAt: Date.now(), + action, + userId, + }; + }); - **Parameters:** - * `workflowRunId`: The workflow run ID (optional) - * `error`: The error object + // Step 4: Finalize (only if approved) + if (eventData.approved) { + // Additional steps... + } - ```typescript - onError: async ({ workflowRunId, error }) => { - // Handle error + return { + success: true, + approved: eventData.approved, + workflowRunId: context.workflowRunId, + }; + }, + { + middlewares: [realtimeMiddleware], } - ``` - - - - Called when a warning is logged. +); +``` - **Parameters:** - * `workflowRunId`: The workflow run ID (optional) - * `warning`: The warning message +**Key patterns:** - ```typescript - onWarning: async ({ workflowRunId, warning }) => { - // Handle warning - } - ``` - +1. **Middleware for all events**: The `realtimeMiddleware` automatically handles all realtime event emissions by detecting wait-for-event steps through the stepName +2. **Step name detection**: The middleware checks if `stepName === "wait-for-approval"` to know when to emit `waitingForInput` and `inputResolved` events +3. **Unique event IDs**: Use a unique `eventId` (like `approval-${workflowRunId}`) to identify which approval request this is +4. **Timeout handling**: Always handle the timeout case when waiting for events - - Called when an info message is logged. +### 2. Create the Notify Endpoint - **Parameters:** - * `workflowRunId`: The workflow run ID (optional) - * `info`: The info message +Create an endpoint at `app/api/notify/route.ts` to handle user input: - ```typescript - onInfo: async ({ workflowRunId, info }) => { - // Handle info - } - ``` - +```typescript +import { Client } from "@upstash/workflow"; +import { NextRequest, NextResponse } from "next/server"; -## Examples +const workflowClient = new Client({ + baseUrl: process.env.QSTASH_URL!, + token: process.env.QSTASH_TOKEN!, +}); -### Error Tracking Middleware +export async function POST(request: NextRequest) { + const body = await request.json(); + const { eventId, eventData } = body; -Send errors to an external monitoring service: + if (!eventId) { + return NextResponse.json( + { success: false, error: "eventId is required" }, + { status: 400 } + ); + } -```typescript -import { WorkflowMiddleware } from "@upstash/workflow"; + // Notify the workflow + await workflowClient.notify({ + eventId, + eventData, + }); -const errorTrackingMiddleware = new WorkflowMiddleware({ - name: "error-tracking", - callbacks: { - onError: async ({ workflowRunId, error }) => { - await fetch("https://your-monitoring-service.com/errors", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - workflowRunId, - error: error.message, - stack: error.stack, - timestamp: new Date().toISOString() - }) - }); - } - } -}); + return NextResponse.json({ success: true }); +} ``` -### Multiple Middlewares - -You can use multiple middlewares together: + +**Preventing Race Conditions**: If you need to trigger a workflow and immediately send it an event, you can use the `workflowRunId` parameter to enable lookback: ```typescript -import { serve } from "@upstash/workflow/nextjs"; -import { loggingMiddleware } from "@upstash/workflow"; - -export const { POST } = serve( - async (context) => { - // Your workflow logic - }, - { - middlewares: [ - loggingMiddleware, - errorTrackingMiddleware, - performanceMiddleware - ] - } -); +await workflowClient.notify({ + eventId, + eventData, + workflowRunId: "wfr_abc123", // Ensures notification is delivered even if sent before waitForEvent +}); ``` -Middlewares are executed in the order they're provided in the array. +Learn more in the [notify documentation](/docs/workflow/basics/client/notify). + -# Migration Guide -Source: https://upstash.com/docs/workflow/howto/migrations +## Building the Frontend -This guide covers migration between different versions of Upstash Workflow. +### 1. Extend the Custom Hook - - - In January 2026, we released 1.0.0 version of the TypeScript SDK with several breaking changes to improve the developer experience, reduce bundle size, and simplify configuration. +Extend your hook from the basic example to handle waiting states: - ## Agents API → Separate Package +```typescript +"use client"; - The Agents API has been moved to a separate package to remove the AI SDK dependency from the core workflow package. +import { useRealtime } from "@/lib/realtime-client"; +import { useState, useCallback } from "react"; - ### Migration Steps +interface WorkflowStep { + stepName: string; + result?: unknown; +} - 1. Install the new package: +interface WaitingState { + eventId: string; + message: string; +} - ```bash - npm install @upstash/workflow-agents - ``` +export function useWorkflowWithRealtime() { + const [workflowRunId, setWorkflowRunId] = useState(null); + const [steps, setSteps] = useState([]); + const [waitingState, setWaitingState] = useState(null); + const [isTriggering, setIsTriggering] = useState(false); + const [isRunFinished, setIsRunFinished] = useState(false); - 2. Update your imports: + useRealtime({ + enabled: !!workflowRunId, + channels: workflowRunId ? [workflowRunId] : [], + events: [ + "workflow.stepFinish", + "workflow.runFinish", + "workflow.waitingForInput", + "workflow.inputResolved", + ], + onData({ event, data }) { + if (event === "workflow.stepFinish") { + setSteps((prev) => [ + ...prev, + { + stepName: data.stepName, + result: data.result, + }, + ]); + } else if (event === "workflow.runFinish") { + setIsRunFinished(true); + } else if (event === "workflow.inputResolved") { + // Clear waiting state if it matches + setWaitingState((prev) => + prev?.eventId === data.eventId ? null : prev + ); + } else if (event === "workflow.waitingForInput") { + setWaitingState({ + eventId: data.eventId, + message: data.message, + }); + } + }, + }); - ```typescript - // Old - import { serve } from "@upstash/workflow/nextjs"; + const trigger = useCallback(async () => { + setIsTriggering(true); + setSteps([]); + setWaitingState(null); + setIsRunFinished(false); - export const { POST } = serve(async (context) => { - const model = context.agents.openai('gpt-3.5-turbo'); - const agent = context.agents.agent({ ... }); - const task = context.agents.task({ ... }); + const response = await fetch("/api/trigger", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ workflowType: "human-in-loop" }), }); - // New - import { serve } from "@upstash/workflow/nextjs"; - import { agentWorkflow } from "@upstash/workflow-agents"; + const data = await response.json(); + setWorkflowRunId(data.workflowRunId); + setIsTriggering(false); + }, []); - export const { POST } = serve(async (context) => { - const agents = agentWorkflow(context) + const continueWorkflow = useCallback( + async (data: { approved: boolean }) => { + if (!waitingState) { + throw new Error("No workflow waiting for input"); + } - const model = agents.openai('gpt-3.5-turbo'); - const agent = agents.agent({ ... }); - const task = agents.task({ ... }); - }); - ``` + const response = await fetch("/api/notify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + eventId: waitingState.eventId, + eventData: data, + }), + }); - See [Agents documentation](/docs/workflow/agents/overview) for more details. + if (!response.ok) { + throw new Error("Failed to notify workflow"); + } - ## Removed `keepTriggerConfig` and `useFailureFunction` + // The waiting state will be cleared when we receive inputResolved event + }, + [waitingState] + ); - These parameters are no longer needed in `client.trigger()` as both are now `true` by default. + return { + trigger, + continueWorkflow, + isTriggering, + workflowRunId, + steps, + waitingState, + isRunFinished, + }; +} +``` - ### Migration Steps +**Key additions:** - Simply remove these parameters from your trigger calls: +* **`waitingState`**: Tracks when the workflow is waiting for input +* **`continueWorkflow`**: Function to submit user decisions back to the workflow +* **Multiple events subscription**: Uses `events` array to subscribe to multiple event types +* **Input resolved handling**: Clears the waiting state when the workflow receives the user's input - ```typescript - // Old - const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/workflow", - retries: 3, - keepTriggerConfig: true, - useFailureFunction: true - }); +### 2. Use the Hook with Approval UI - // New - const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/workflow", - retries: 3 - }); - ``` +```typescript +"use client"; - Configuration passed to `trigger()` now automatically applies to the entire workflow. +import { useWorkflowWithRealtime } from "@/hooks/useWorkflowWithRealtime"; - ## Configuration Moved from `serve` to `trigger` +export default function WorkflowPage() { + const { + trigger, + isTriggering, + steps, + isRunFinished, + waitingState, + continueWorkflow, + } = useWorkflowWithRealtime(); - The `retries`, `flowControl`, `retryDelay`, and `failureUrl` options have been removed from `serve()` and should now be passed in `client.trigger()`. + return ( +
+ - ### Migration Steps + {isRunFinished && ( +

✅ Workflow Finished!

+ )} - Move configuration from serve options to trigger: + {/* Show workflow steps */} +

Workflow Steps:

+
+ {steps.map((step, index) => ( +
+ {step.stepName} + {Boolean(step.result) && ( + : {JSON.stringify(step.result)} + )} +
+ ))} +
- ```typescript - // Old - export const { POST } = serve( - async (context) => { ... }, - { - retries: 3, - retryDelay: "1000 * (1 + retried)", - flowControl: { key: "my-key", rate: 10 } - } - ); + {/* Show approval UI when waiting for input */} + {waitingState && ( +
+

{waitingState.message}

+

+ +

+

+ +

+
+ )} +
+ ); +} +``` - // Trigger call - await client.trigger({ url: "..." }); +## How the Pattern Works - // New - export const { POST } = serve( - async (context) => { ... } - // No configuration here anymore - ); +### Timeline of Events - // Configuration in trigger call - await client.trigger({ - url: "...", - retries: 3, - retryDelay: "1000 * (1 + retried)", - flowControl: { key: "my-key", rate: 10 } - }); - ``` +1. **Initial processing**: `stepFinish` event → Frontend shows completed step +2. **Waiting for approval**: `waitingForInput` event → Frontend shows approval buttons +3. **User clicks approve/reject**: Frontend calls `/api/notify` +4. **Workflow resumes**: `inputResolved` event → Frontend hides approval buttons +5. **Processing continues**: More `stepFinish` events as workflow continues +6. **Workflow completes**: `runFinish` event → Frontend shows "Workflow Finished!" - This change makes it easier to configure different behavior for different workflow runs of the same endpoint. +## Benefits - - In the Python SDK, these options remain in the `serve` decorator as they were before. - +* **Real-time feedback**: Users see exactly when their approval is needed +* **No polling**: Instant updates via Server-Sent Events +* **Timeout handling**: Workflows don't hang indefinitely waiting for input - ## Removed `stringifyBody` from `context.call` and `context.invoke` +## Full Example - The `stringifyBody` parameter has been removed. The `body` parameter now expects a string. +For a complete working example with all steps, error handling, and full UI components, check out the [Upstash Realtime example on GitHub](https://github.com/upstash/workflow-js/tree/main/examples/upstash-realtime). + +## Next Steps + +* Review the [basic real-time workflow pattern](./basic) +* Learn about [workflow event handling](/docs/workflow/features/wait-for-event) +* Explore [Realtime features](/docs/realtime/overall/quickstart) +* Check out [workflow failure handling](/docs/workflow/features/failure-callback) - ### Migration Steps +# Redact Private Data +Source: https://upstash.com/docs/workflow/howto/redact-fields - Update your call and invoke methods to use `JSON.stringify()`: +Workflow runs can contain private data that you don't want visible in the Upstash Console or API responses. - ```typescript - // Old - const result = await context.call("call-api", { - url: "https://api.example.com/endpoint", - method: "POST", - body: { key: "value" }, - stringifyBody: true - }); +Upstash Workflow allows you to redact specific fields so they appear as `REDACTED:` in the dashboard and API. The original values are still used when delivering requests to your workflow endpoint. The SHA256 hash lets you verify the data without revealing the original values. - // New - const result = await context.call("call-api", { - url: "https://api.example.com/endpoint", - method: "POST", - body: JSON.stringify({ key: "value" }) - }); - ``` +To redact fields, pass the `redact` option when triggering a workflow run. - The same applies to `context.invoke()`: +Available options: +| Option | Description | +|--|--| +| body | Redact the body of the workflow steps | +| headers | Redact the headers of the workflow steps | +| headers[header_name] | Redact a specific header (e.g., `headers[Authorization]`) | - ```typescript - // Old - await context.invoke("invoke-workflow", { - workflow: otherWorkflow, - body: { key: "value" }, - stringifyBody: true - }); + +Redaction is one-way. Once a field is redacted, the original value cannot be retrieved from the API or dashboard. + - // New - await context.invoke("invoke-workflow", { - workflow: otherWorkflow, - body: JSON.stringify({ key: "value" }) - }); - ``` + +```typescript TypeScript +import { Client } from "@upstash/workflow"; - ## Logger → Middleware System +const client = new Client({ token: "" }); - The logging system has been replaced with a more flexible middleware system. +const { workflowRunId } = await client.trigger({ + url: "https://my-app.com/api/workflow", + body: { hello: "world" }, + redact: { + body: true, + header: ["Authorization"] // or `header: true` to redact all headers + }, +}); +``` - ### Migration Steps +```python Python +from upstash_workflow import Client - Replace the old logger with the new middleware: +client = Client("") +client.trigger( + url="https://my-app.com/api/workflow", + body={ + "hello": "world", + }, + redact={ + "body": True, + "header": ["Authorization"] // or `header: True` to redact all headers + }, +) +``` - ```typescript - // Old - // Logging was automatic or controlled via verbose option - export const { POST } = serve( - async (context) => { ... }, - { verbose: true } - ); +```bash cURL +curl -XPOST \ + -H 'Authorization: Bearer XXX' \ + -H "Content-Type: application/json" \ + -H "Upstash-Redact-Fields: body, header[Authorization]" \ + -d '{ "hello": "world" }' \ + 'https://qstash.upstash.io/v2/publish/https://my-app.com/api/workflow' +``` + - // New - import { loggingMiddleware } from "@upstash/workflow"; + - export const { POST } = serve( - async (context) => { ... }, - { - middlewares: [loggingMiddleware] - } - ); - ``` +Redaction is configured per workflow run, so you can redact different fields for different runs. - You can also create custom middlewares for more control. See [Middlewares documentation](/docs/workflow/howto/middlewares) for details. +When `body` is redacted, the step outputs in the dashboard and API will show `REDACTED:` instead of the actual values. The workflow still executes with the original data. - ## Removed `onStepFinish` +If a workflow run fails and moves to the DLQ, the redacted fields remain redacted in the DLQ. +However, when you retry, resume or restart a workflow run from DLQ, Workflow delivers the original values to your endpoint. - The `onStepFinish` callback has been removed. Use middlewares instead. +# Schedule a Workflow +Source: https://upstash.com/docs/workflow/howto/schedule - ### Migration Steps +You can schedule a workflow to run periodically using a cron definition. - Replace `onStepFinish` with a custom middleware: +For this feature, you would need to use Upstash QStash's Schedules feature. - ```typescript - // Old - export const { POST } = serve( - async (context) => { ... }, - { - onStepFinish: (stepName, result) => { - console.log(`Step ${stepName} finished with:`, result); - } - } - ); +## Scheduling a workflow - // New - import { WorkflowMiddleware } from "@upstash/workflow"; +For example, let's suppose that you have a workflow that creates a backup of some important data daily. Our workflow endpoint might look like this: - const stepFinishMiddleware = new WorkflowMiddleware({ - name: "step-finish", - callbacks: { - afterExecution: async ({ stepName, result }) => { - console.log(`Step ${stepName} finished with:`, result); - } - } - }); +To run this endpoint on a schedule, navigate to `Schedules` in your QStash dashboard and click `Create Schedule`: - export const { POST } = serve( - async (context) => { ... }, - { - middlewares: [stepFinishMiddleware] - } - ); - ``` + - See [Middlewares documentation](/docs/workflow/howto/middlewares) for more details. +Enter your live endpoint URL, add a CRON expression to define the interval at which your endpoint is called (i.e. every day, every 15 minutes, ...) and click `Schedule`: -
+ - - In October 2024, we released a new SDK, `@upstash/workflow`, for Upstash Workflow, separating its development from the QStash SDK. Although Upstash Workflow is built on QStash, our goal is to improve the developer experience and support with a dedicated SDK. Development for Upstash Workflow will occur in `@upstash/workflow`, and Workflow-related imports will be removed from `@upstash/qstash` in future releases. +Your workflow will now run repeatedly at the interval you have defined. For more details on CRON expressions, see our [QStash scheduling documentation](/docs/qstash/features/schedules). - If you started using Upstash Workflow with `@upstash/qstash`, you will need to migrate to `@upstash/workflow`. We have made some backward-incompatible changes, but we aim to make the transition as smooth as possible. - In this guide, we will explain the changes you may need to make for migration. +## Programmatically Schedule - ### Install `@upstash/workflow` +In order to massively improve the user experience, many applications send weekly summary reports to their users. These could be weekly analytics summaries or SEO statistics to keep users engaged with the platform. - First, we will need to install the new package with: +Let's create a user-specific schedule, sending a first report to each user exactly 7 days after they signed up: - - - ```bash - npm install @upstash/workflow - ``` - - - ```bash - pnpm install @upstash/workflow - ``` - - - ```bash - bun add @upstash/workflow - ``` - - + - If you were using `@upstash/qstash` only for workflow, you can uninstall it from your project. +```typescript api/sign-up/route.ts +import { signUp } from "@/utils/auth-utils"; +import { Client } from "@upstash/qstash"; - ### Serve methods +const client = new Client({ token: process.env.QSTASH_TOKEN! }); - You will need to change the imports from `@upstash/qstash` to @upstash/workflow: +export async function POST(request: Request) { + const userData: UserData = await request.json(); - ```ts - // old - import { serve } from "@upstash/qstash/nextjs" + // Schedule weekly account summary + await client.schedules.create({ + scheduleId: `user-summary-${user.email}`, + destination: "https:///api/send-weekly-summary", + body: { userId: user.id }, + cron: cron, + }); - // new - import { serve } from "@upstash/workflow/nextjs" - ``` + return NextResponse.json( + { success: true, message: "User registered and summary scheduled" }, + { status: 201 } + ); +} +``` - We have updated what our `serve` methods return. We made this change to make it - easier to extend the API in the future. +```python main.py +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from qstash import AsyncQStash +from datetime import datetime, timedelta - For instance, Next.js method changed like this: +app = FastAPI() - ```javascript - // old - export const POST = serve(...); +client = AsyncQStash("") - // new - export const { POST } = serve(...); - ``` +@app.post("/api/sign-up") +async def sign_up(request: Request): + user_data = await request.json() - We kept the `serve` method of `Hono` the same. The rest are updated in a similar way. - See [the quickstarts](/docs/workflow/quickstarts/platforms) for the new way `serve` - should be used. + # Simulate user registration + user = await sign_up(user_data) - Additionally, `@upstash/workflow/nuxt` import is removed. You should use `@upstash/workflow/h3` - instead. This change was made because `nuxt` uses `h3` under the hood and our `serve` method - for `nuxt` can work with any project using `h3`. + # Calculate the date for the first summary (7 days from now) + first_summary_date = datetime.now() + timedelta(days=7) - ### Updating `context.call` + # Create cron expression for weekly summaries starting 7 days from signup + cron = f"{first_summary_date.minute} {first_summary_date.hour} * * {first_summary_date.day}" - If you were using [`context.call` method](/docs/workflow/basics/context#context-call) in your workflow, you will need to change - how it's called and what it returns. Here is what the change looks like: + # Schedule weekly account summary + await client.schedule.create_json( + schedule_id=f"user-summary-{user.email}", + destination="https:///api/send-weekly-summary", + body={"userId": user.id}, + cron=cron, + ) - ```javascript - // old - const result = await context.call("call step", "", "POST", ...) + return JSONResponse( + content={"success": True, "message": "User registered and summary scheduled"}, + status_code=201, + ) - // new - const { - status, // response status - headers, // response headers - body // response body - } = await context.call("call step", { - url: "", - method: "POST", - ... - }) - ``` +``` - In the old version, we only returned the response body. Also, if the request - to the url failed, [the workflow run would fail](/docs/workflow/howto/failures). + - In the new version, we update how the parameters are passed to the `context.call`. - Additionally, we change the fail behavior: if the request fails, it doesn't make the - workflow fail. Instead, the status and the body is simply returned and workflow - continues as usual. +This code will call our workflow every week, starting exactly seven days after a user signs up. Each call to our workflow will contain the respective user's ID. - If you have ongoing workflow runs which call `context.call` during your transition, - `status` and `headers` fields may not be available in these old runs. After your - transition, all workflow runs will have all three fields. + + When creating a per-user schedule, pass a unique `scheduleId` to identify the schedule for better management and observability. + - ### Renaming Errors +# Secure a Workflow +Source: https://upstash.com/docs/workflow/howto/security - The errors in Workflow were renamed from `QStashWorkflowError` and `QStashWorkflowAbort` to `WorkflowError` and `WorkflowAbort`. +To prevent unauthorized access to your workflow endpoint, you can add an authorization layer. +Upstash Workflow supports two approaches: - -
+* **Built-in request verification** (recommended) +* **Custom authorization method** -# Select a Region -Source: https://upstash.com/docs/workflow/howto/multi-region +### Built-in request verification (recommended) -## Overview +Upstash Workflow provides a built-in mechanism to secure your workflow endpoint by verifying request signatures. +Every request to your endpoint include a valid `Upstash-Signature` header. -Upstash Workflow operates on top of QStash, which is available in two distinct regions: **EU region** and **US region**. Each region is completely independent with its own infrastructure, pricing, resources, and workflow runs. +How it works: -## Regional URLs +1. Upstash Workflow automatically adds the `Upstash-Signature` header to every request. + This signature is generated using your signing keys. -* **EU Region**: `https://qstash-eu-central-1.upstash.io`, or `https://qstash.upstash.io` -* **US Region**: `https://qstash-us-east-1.upstash.io` +2. When this mechanism is enabled, the SDK verifies that the signature is valid before processing the request. -## Key Concepts +This ensures that only requests originating from Upstash Workflow are processed. -Each region maintains: -* Usage in each region is tracked and billed independently -* Workflow runs, events, and configurations are region-specific -* Each region has its own API tokens and signing keys +To enable this verification, set the following environment variables in your application: -### Migration Between Regions +```bash .env +QSTASH_CURRENT_SIGNING_KEY=xxxxxxxxx +QSTASH_NEXT_SIGNING_KEY=xxxxxxxxx +``` -If you don't have any active resources (active workflow runs, schedules, url groups etc), you can simply update your environment variables with the new region to migrate. If you have active resources, you will need to migrate more gracefully, as described below. +You can find the values in Upstash Workflow dashboard. -You can migrate your Workflow resources from one region to another using the Upstash Console: + -1. Navigate to the [Workflow tab on Upstash Console](https://console.upstash.com/workflow) -2. Click the **Migrate** button -3. Follow the guided migration process + + For edge cases where environment variables cannot be used, you can explicitly create and pass a `Receiver` object to verify request signatures: - + -The migration tool will: -* Help you set up migration-mode environment variables -* Copy and update your QStash resources (schedules, url groups, queues) + ```typescript TypeScript + import { Receiver } from "@upstash/qstash"; + import { serve } from "@upstash/workflow/nextjs"; -Your workflow logs or DLQ aren't part of the migration. They will remain in the old region. + export const { POST } = serve( + async (context) => { ... }, + { + receiver: new Receiver({ + currentSigningKey: "", + nextSigningKey: "", + }), + } + ); + ``` - -After migration, your app will be able to handle requests from both regions simultaneously to ensure a smooth transition. - + ```python Python + from qstash import Receiver -## Operating Modes + @serve.post( + "/api/example", + receiver=Receiver( + current_signing_key=os.environ["QSTASH_CURRENT_SIGNING_KEY"], + next_signing_key=os.environ["QSTASH_NEXT_SIGNING_KEY"], + ), + ) + async def example(context: AsyncWorkflowContext[str]) -> None: + ... -Workflow SDK supports two modes of operation: + ``` + + -### Single-Region Mode (Default) +## Custom Authorization Method -When `QSTASH_REGION` environment variable is **not set**, the SDK operates in single-region mode: +You can implement your own authorization mechanism with Upstash Workflow. -* Uses `QSTASH_TOKEN` and `QSTASH_URL` (or defaults to EU region) -* All workflow triggers are sent through the configured region -* Incoming workflow requests are verified using default signing keys +The context object provides access to the initial request headers and payload on every workflow step. +You can use them to pass your custom authentication token to verify the requests. -```bash -# Single-region configuration (EU) -QSTASH_URL="https://qstash.upstash.io" -QSTASH_TOKEN="your_eu_token" -QSTASH_CURRENT_SIGNING_KEY="your_eu_current_key" -QSTASH_NEXT_SIGNING_KEY="your_eu_next_key" -``` + -### Migration Mode + ```typescript TypeScript + import { serve } from "@upstash/workflow/nextjs"; -When `QSTASH_REGION` is set to `US_EAST_1` or `EU_CENTRAL_1`, the SDK enables migration mode: + export const { POST } = serve( + async (context) => { + // 👇 Extract Bearer token form the request headers + const authHeader = context.headers.get("authorization"); + const bearerToken = authHeader?.split(" ")[1]; -* Uses region-specific credentials for the primary region (`QSTASH_REGION`) -* Automatically handles region detection for incoming workflow requests -* Supports receiving workflow calls from multiple regions simultaneously + // 👇 Use your authentication function to verify the token + if (!isValid(bearerToken)) { + console.error("Authentication failed."); + return; + } - - If a workflow run was started in one region, all its steps will execute in that region. - + // Your workflow steps.. + }, + { + failureFunction: async () => { + // 👇 Same auth check for failure function + const authHeader = context.headers.get("authorization"); + const bearerToken = authHeader?.split(" ")[1]; -Environment variables: + if (!isValid(bearerToken)) { + // ... + } + }, + } + ); + ``` -```bash -# Migration mode configuration with US as primary -QSTASH_REGION="US_EAST_1" + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext -US_EAST_1_QSTASH_URL="https://qstash-us-east-1.upstash.io" -US_EAST_1_QSTASH_TOKEN="your_us_token" -US_EAST_1_QSTASH_CURRENT_SIGNING_KEY="your_us_current_key" -US_EAST_1_QSTASH_NEXT_SIGNING_KEY="your_us_next_key" + app = FastAPI() + serve = Serve(app) -EU_CENTRAL_1_QSTASH_URL="https://qstash-eu-central-1.upstash.io" -EU_CENTRAL_1_QSTASH_TOKEN="your_eu_token" -EU_CENTRAL_1_QSTASH_CURRENT_SIGNING_KEY="your_eu_current_key" -EU_CENTRAL_1_QSTASH_NEXT_SIGNING_KEY="your_eu_next_key" -``` + @serve.post("/api/example") + async def example(context: AsyncWorkflowContext[str]) -> None: + auth_header = context.headers.get("authorization") + bearer_token = auth_header.split(" ")[1] if auth_header else None - -Migration mode relies on environment variables being available via `process.env`. It won't work on platforms where `process.env` is not available, such as Cloudflare Workers. - + if not is_valid(bearer_token): + print("Authentication failed.") + return -## SDK Requirements + # Your workflow steps... -Migration support requires: -* `@upstash/workflow` >= 1.1.0 -* `@upstash/qstash` >= 2.9.0 + ``` -Update your dependencies: + -```bash -npm install @upstash/workflow@latest @upstash/qstash@latest -``` + + If you implement custom authorization in your workflow route, you should also include the same authorization check in the failure function. -# Parallel Runs -Source: https://upstash.com/docs/workflow/howto/parallel-runs + The failure function executes independently of the route function, so without this check, unauthorized requests could trigger the failure function + - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/docs/workflow/roadmap) for feature parity plans and - [Changelog](/docs/workflow/changelog) for updates. - +# Start a Run +Source: https://upstash.com/docs/workflow/howto/start -Just like you can execute multiple JavaScript promises at the same time using `Promise.all`, you can run multiple workflow steps at the same time: +You’ve defined your workflow, and the final step is to trigger the endpoint! -```typescript -const [result1, result2, result3] = - await Promise.all([ - ctx.run("parallel-step-1", async () => { ... }), - ctx.run("parallel-step-2", async () => { ... }), - ctx.run("parallel-step-3", async () => { ... }), - ]) -``` +There are two main ways to start your workflow: -In a complete code example, your workflow could look like this: +### Using `client.trigger` (Recommended) -```typescript app/api/workflow/route.ts -import { serve } from "@upstash/workflow/nextjs"; -import { checkInventory, brewCoffee, printReceipt } from "@/utils"; +We recommend using [`client.trigger`](/docs/workflow/basics/client/trigger) to start your workflow. -export const { POST } = serve(async (ctx) => { - const [coffeeBeansAvailable, cupsAvailable, milkAvailable] = - await Promise.all([ - ctx.run("check-coffee-beans", () => checkInventory("coffee-beans")), - ctx.run("check-cups", () => checkInventory("cups")), - ctx.run("check-milk", () => checkInventory("milk")), - ]); + + ```ts Single Workflow + import { Client } from "@upstash/workflow"; - // If all ingedients available, brew coffee - if (coffeeBeansAvailable && cupsAvailable && milkAvailable) { - const price = await ctx.run("brew-coffee", async () => { - return await brewCoffee({ style: "cappuccino" }); - }); + const client = new Client({ token: "" }) + const { workflowRunId } = await client.trigger({ + url: "https:///", + body: "hello there!", // optional body + headers: { ... }, // optional headers + workflowRunId: "my-workflow", // optional workflow run id + retries: 3 // optional retries in the initial request + delay: "10s" // optional delay value + failureUrl: "https://", // optional failure url + flowControl: { ... } // optional flow control + }) - await printReceipt(price); - } -}); -``` + console.log(workflowRunId) + // prints wfr_my-workflow + ``` -After running your workflow, your dashboard shows each step in detail: + ```ts Multiple Workflows + import { Client } from "@upstash/workflow"; - + const client = new Client({ token: "" }) + const results = await client.trigger([ + { + url: "", + // other options... + }, + { + url: "", + // other options... + }, + ]) -# Realtime Quickstart -Source: https://upstash.com/docs/workflow/howto/realtime/basic + console.log(results[0].workflowRunId) + // prints wfr_my-workflow + ``` + -[**Upstash Realtime**](/docs/realtime/overall/quickstart) lets you emit events from your workflow and subscribe to them in real-time on your frontend. +### 2. Sending an HTTP Request -## How It Works +This approach is recommended for quick testing via curl during development. -Upstash Realtime is powered by Upstash Redis and provides a clean, 100% type-safe API for publishing and subscribing to events: +You should **NOT** start the workflow run in production by direct calls to your endpoint. -* Your frontend can subscribe to events -* When you **emit** an event, it's instantly delivered to live subscribers on the frontend -* You can also replay events that happened in the past +```bash +curl -X POST https:/// \ + -H "my-header: foo" \ + -d '{"foo": "bar"}' +``` -This guide shows you how to integrate Upstash Workflow with Upstash Realtime to display real-time progress updates in your frontend. + + If you’ve secured your endpoint with signing keys, only the `trigger` method will work. Direct calls to the endpoint (e.g., via `curl` or `fetch`) will not be possible since `Upstash-Signature` header is missing. -## Setup + For more information, read [Secure a workflow](/docs/workflow/howto/security) documentation. + -### 1. Install Packages +# Webhooks +Source: https://upstash.com/docs/workflow/howto/use-webhooks -```bash -npm install @upstash/workflow @upstash/realtime @upstash/redis zod -``` +This guide explains how to handle webhooks effectively in your Upstash Workflow applications. We'll walk through: -### 2. Configure Upstash Realtime +* setting up webhook endpoints +* verifying webhook requests +* and processing webhook events -Create a Realtime instance in `lib/realtime.ts`: + + You can find the project source code on GitHub. + -```typescript title="lib/realtime.ts" -import { InferRealtimeEvents, Realtime } from "@upstash/realtime"; -import { Redis } from "@upstash/redis"; -import z from "zod/v4"; +## Overview -const redis = Redis.fromEnv(); +Webhooks allow external services to notify your application when events occur. For example, you can use webhooks to receive notifications when a new order is placed in your e-commerce store, a new user signs up, or a new message is sent in your chat application. -const schema = { - workflow: { - runFinish: z.object({}), - stepFinish: z.object({ - stepName: z.string(), - result: z.unknown().optional(), - }), - }, -}; +Upstash Workflow provides a simple way to receive these events and trigger workflows based on the incoming data autonomously. -export const realtime = new Realtime({ schema, redis }); -export type RealtimeEvents = InferRealtimeEvents; -``` +### Setting Up Webhook Endpoints -### 3. Create a Realtime Middleware +#### Basic Setup -Create a custom middleware that will emit events to Realtime at `lib/middleware.ts`: +To create a webhook endpoint, use the `serve` function from `@upstash/workflow`: -```typescript title="lib/middleware.ts" -import { WorkflowMiddleware } from "@upstash/workflow"; -import { realtime } from "./realtime"; + -export const realtimeMiddleware = new WorkflowMiddleware({ - name: "realtime-events", - callbacks: { - afterExecution: async ({ context, stepName, result }) => { - const channel = realtime.channel(context.workflowRunId); - await channel.emit("workflow.stepFinish", { - stepName, - result, - }); - }, - runCompleted: async ({ context }) => { - const channel = realtime.channel(context.workflowRunId); - await channel.emit("workflow.runFinish", {}); - }, +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; + +export const { POST } = serve( + async (context) => { + // Your webhook handling logic here }, -}); + { + initialPayloadParser: (payload) => { + return payload; + }, + } +); ``` -**Key points:** - -* The `afterExecution` callback is triggered after each workflow step completes -* The `runCompleted` callback is triggered when the entire workflow finishes -* We use `context.workflowRunId` to create a unique channel for each workflow run -* Events are automatically emitted without needing to manually call emit inside your workflow steps +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext -### 4. Create a Realtime Endpoint +app = FastAPI() +serve = Serve(app) -Create an API route at `app/api/realtime/route.ts` to handle Realtime connections: +def initial_payload_parser(payload): + return payload -```typescript title="app/api/realtime/route.ts" -import { handle } from "@upstash/realtime"; -import { realtime } from "@/lib/realtime"; +@serve.post("/api/example", initial_payload_parser=initial_payload_parser) +async def example(context: AsyncWorkflowContext[str]) -> None: + # Your webhook handling logic here -export const GET = handle({ realtime }); ``` -This endpoint enables Server-Sent Events (SSE) connections for real-time updates. + -### 5. Add the Realtime Provider +#### Request Validation -Wrap your application in the `RealtimeProvider` by updating your root layout at `app/layout.tsx`: +Always validate incoming webhook requests to ensure they're legitimate. This way, no one other than the original webhook source can trigger your workflow. Here's an example using Clerk webhooks with Svix: -```tsx title="app/layout.tsx" -"use client"; + +```typescript Validate and Parse in Workflow - TypeScript +export const { POST } = serve(async (context) => { + const payloadString = context.requestPayload; + const headerPayload = context.headers; -import { RealtimeProvider } from "@upstash/realtime/client"; + let event: WebhookEvent; + try { + event = await validateRequest(payloadString, headerPayload); + } catch { + return + } + + // Next steps based on the event + +}) -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - - {children} - - - ); -} ``` -### 6. Create a Typed Client Hook +```python Validate and Parse in Workflow - Python +async def validate_request(payload_string: str, header_payload: dict): + # Validate the request + pass -Create a typed `useRealtime` hook at `lib/realtime-client.ts`: +@serve.post("/api/example") +async def example(context: AsyncWorkflowContext[str]) -> None: + payload_string = context.request_payload + header_payload = context.headers -```typescript title="lib/realtime-client.ts" -"use client"; + try: + event = await validate_request(payload_string, header_payload) + except: + return -import { createRealtime } from "@upstash/realtime/client"; -import type { RealtimeEvents } from "./realtime"; + # Next steps based on the event -export const { useRealtime } = createRealtime(); ``` -*** +```typescript Validation Function - TypeScript +import { Webhook } from "svix"; +import { WebhookEvent } from "@clerk/nextjs/server"; -## Building the Workflow +const webhookSecret = "YOUR_WEBHOOK_SECRET"; -### 1. Create the Workflow Endpoint +async function validateRequest(payloadString: string, headerPayload: Headers) { + const svixHeaders = { + "svix-id": headerPayload.get("svix-id") as string, + "svix-timestamp": headerPayload.get("svix-timestamp") as string, + "svix-signature": headerPayload.get("svix-signature") as string, + }; + const wh = new Webhook(webhookSecret); + return wh.verify(payloadString, svixHeaders) as WebhookEvent; +} +``` -Create your workflow at `app/api/workflow/route.ts`: + -```typescript title="app/api/workflow/route.ts" -import { serve } from "@upstash/workflow/nextjs"; -import { realtimeMiddleware } from "@/lib/middleware"; +### Handling Webhook events -type WorkflowPayload = { - userId: string; - action: string; -}; +Use the context.run method to process webhook events in discrete, trackable steps: -export const { POST } = serve( - async (context) => { - const { userId, action } = context.requestPayload; + - await context.run("validate-data", async () => { - return { valid: true, userId, action }; - }); +```typescript TypeScript +export const { POST } = serve(async (context) => { + // ... Parse and validate the incoming request - await context.run("process-action", async () => { - // Your business logic here - return { processed: true, userId, action }; - }); + const user = await context.run( + "handle-webhook-event", + async () => { + if (event.type === "user.created") { + const { id: clerkUserId, email_addresses, first_name } = event.data; + const primaryEmail = email_addresses.find( + (email) => (email.id = event.data.primary_email_address_id) + ); + + if (!primaryEmail) { + return false; + } - return { success: true, workflowRunId: context.workflowRunId }; - }, - { - middlewares: [realtimeMiddleware], - } -); + return { + event: event.type, + userId: clerkUserId, + email: primaryEmail.email_address, + firstName: first_name, + } as UserPayload; + } + return false; + } + ); +}); ``` -**Key points:** - -* Import the `realtimeMiddleware` from `@/lib/middleware` -* Pass the middleware in the `middlewares` array to the `serve` function -* The middleware automatically emits events after each step and when the workflow completes +```python Python +@serve.post("/api/example") +async def example(context: AsyncWorkflowContext[str]) -> None: + # ... Parse and validate the incoming request -### 2. Create a Trigger Endpoint + async def _handle_webhook_event(): + if event.type == "user.created": + clerk_user_id = event.data["id"] + email_addresses = event.data["email_addresses"] + first_name = event.data["first_name"] -Create an endpoint to trigger workflows at `app/api/trigger/route.ts`: + primary_email = next( + ( + email + for email in email_addresses + if email.id == event.data["primary_email_address_id"] + ), + None, + ) -```typescript title="app/api/trigger/route.ts" -import { NextRequest, NextResponse } from "next/server"; -import { Client } from "@upstash/workflow"; + if not primary_email: + return False -export const workflowClient = new Client({ - token: process.env.QSTASH_TOKEN, - baseUrl: process.env.QSTASH_URL, -}); + return { + "event": event.type, + "user_id": clerk_user_id, + "email": primary_email["email_address"], + "first_name": first_name, + } -export async function POST(request: NextRequest) { - const workflowUrl = `${request.nextUrl.origin}/api/workflow`; + return False - const { workflowRunId } = await workflowClient.trigger({ - url: workflowUrl, - body: { - userId: "user-123", - action: "process-data", - }, - }); + user = await context.run("handle-webhook-event", _handle_webhook_event) - return NextResponse.json({ workflowRunId }); -} ``` -*** - -## Building the Frontend + -### 1. Create a Custom Hook +After validating the webhook and extracting the initial user data, you'll often need to perform additional operations like creating customer records, sending welcome emails etc. -Create a React hook to manage the Realtime subscription at `hooks/useWorkflow.ts`: + -```typescript -"use client"; +```typescript TypeScript +export const { POST } = serve(async (context) => { + // ... Previous validation and user data extraction -import { useRealtime } from "@/lib/realtime-client"; -import { useState, useCallback } from "react"; + if (!user) { + return; + } -interface WorkflowStep { - stepName: string; - result?: unknown; -} + const customer = await context.run("create-stripe-customer", async () => { + return await stripe.customers.create({ + email: user.email, + name: `${user.firstName} ${user.lastName}`, + metadata: { + userId: user.userId, + }, + }); + }); -export function useWorkflow() { - const [workflowRunId, setWorkflowRunId] = useState(null); - const [steps, setSteps] = useState([]); - const [isRunFinished, setIsRunFinished] = useState(false); + /// ... Additional steps +}); +``` - useRealtime({ - enabled: Boolean(workflowRunId), - channels: workflowRunId ? [workflowRunId] : [], - events: ["workflow.stepFinish", "workflow.runFinish"], - onData({ event, data }) { - if (event === "workflow.stepFinish") { - setSteps((prev) => [...prev, data]); - } +```python Python +@serve.post("/api/example") +async def example(context: AsyncWorkflowContext[str]) -> None: + # ... Previous validation and user data extraction - if (event === "workflow.runFinish") { - setIsRunFinished(true); - } - }, - }); + if not user: + return - const trigger = () => { - setSteps([]); - setIsRunFinished(false); + async def _create_stripe_customer(): + return await stripe.customers.create( + email=user["email"], + name=f"{user['first_name']} {user['last_name']}", + metadata={"user_id": user["user_id"]}, + ) - const response = await fetch("/api/trigger", { - method: "POST", - }); + customer = await context.run("create-stripe-customer", _create_stripe_customer) - const data = await response.json(); - setWorkflowRunId(data.workflowRunId); - }; + # ... Additional steps - return { - trigger, - workflowRunId, - steps, - isRunFinished, - }; -} ``` -**Key features:** + -* Subscribe to multiple events using the `events` array: `["workflow.stepFinish", "workflow.runFinish"]` -* The hook manages both triggering the workflow and subscribing to updates -* Type-safe event handling with TypeScript +You're now ready to perform any operation in the following steps. -### 2. Use the Hook in Your Component +# Vercel AI SDK +Source: https://upstash.com/docs/workflow/integrations/aisdk -```tsx -"use client"; + + This feature is not yet available in + [workflow-py](https://github.com/upstash/workflow-py). See our + [Roadmap](/docs/workflow/roadmap) for feature parity plans and + [Changelog](/docs/workflow/changelog) for updates. + -import { useWorkflow } from "@/hooks/useWorkflow"; + + You can find the project source code which uses real APIs on Github. + -export default function WorkflowPage() { - const { trigger, steps, isRunFinished } = useWorkflow(); +Upstash Workflow integrates with the Vercel AI SDK to provide durable and reliable AI applications. This allows you to: - return ( -
- +* Build resilient AI applications with automatic retries +* Manage AI operations with workflow steps +* Implement tools and function calling with durability +* Handle errors gracefully across your AI operations +* Handle long-running AI operations with extended timeouts - {isRunFinished &&

✅ Workflow Finished!

} +This guide will walk you through setting up and implementing AI features using Upstash Workflow's durability guarantees with Vercel AI SDK's capabilities. -

Workflow Steps:

+## Prerequisites - {steps.map((step, index) => ( -
- {step.stepName} - {Boolean(step.result) && : {JSON.stringify(step.result)}} -
- ))} -
- ); -} -``` +Before getting started, make sure you have: -## How It All Works Together +* An OpenAI API key +* Basic familiarity with Upstash Workflow and Vercel AI SDK +* Vercel AI SDK version 4.0.12 or higher (required for ToolExecutionError handling) -1. **User triggers workflow**: The frontend calls `/api/trigger`, which returns a `workflowRunId` -2. **Frontend subscribes**: Using the `workflowRunId`, the frontend subscribes to the Realtime channel -3. **Workflow executes**: The workflow runs as a background job, emitting events at each step -4. **Real-time updates**: As the workflow emits events, they're instantly delivered to the frontend via Server-Sent Events +## Installation -## Full Example +Install the required packages: -For a complete working example with all steps, error handling, and UI components, check out the [Upstash Realtime example on GitHub](https://github.com/upstash/workflow-js/tree/main/examples/upstash-realtime). + +```bash npm +npm install @ai-sdk/openai ai zod +``` -## Next Steps +```bash pnpm +pnpm install @ai-sdk/openai ai zod +``` -* Learn about [human-in-the-loop workflows with Realtime](./human-in-the-loop) -* Explore [Realtime features](/docs/realtime/overall/quickstart) -* Check out [Workflow configuration options](/docs/workflow/howto/configure) +```bash bun +bun install @ai-sdk/openai ai zod +``` -# Human-in-the-Loop -Source: https://upstash.com/docs/workflow/howto/realtime/human-in-the-loop + -Some workflows require human approval or input before proceeding. When combined with [**Upstash Realtime**](/docs/realtime/overall/quickstart), you can create interactive workflows that pause for user input and provide real-time feedback to your frontend during the entire process. +## Implementation -This guide shows you how to implement a human-in-the-loop workflow pattern with real-time updates using Upstash Workflow and Upstash Realtime. +### Creating OpenAI client -## How It Works +AI SDKs (Vercel AI SDK, OpenAI SDK etc.) uses the client's default fetch implementation to make API requests, but allows you to provide a custom fetch implementation. -In a human-in-the-loop workflow: +In the case of Upstash Workflow, we need to use the `context.call` method to make HTTP requests. We can create a custom fetch implementation that uses `context.call` to make requests. By using `context.call`, Upstash Workflow is the one making the HTTP request and waiting for the response, even if it takes too long to receive response from the LLM. -1. The workflow executes initial steps and emits progress events -2. The workflow pauses at a specific point using [`context.waitForEvent()`](/docs/workflow/features/wait-for-event) -3. A "waiting for input" event is emitted to notify the frontend -4. The user makes a decision in the frontend (approve/reject) -5. The frontend calls an API to notify the workflow using [`client.notify()`](/docs/workflow/basics/client/notify) -6. The workflow resumes with the user's decision -7. An "input resolved" event is emitted so the frontend can update its UI -8. The workflow continues and completes based on the decision +The following code snippet can also be generalized to work with other LLM SDKs, such as Anthropic or Google. -## Prerequisites +```typescript {18-24} +import { createOpenAI } from '@ai-sdk/openai'; +import { HTTPMethods } from '@upstash/qstash'; +import { WorkflowAbort, WorkflowContext } from '@upstash/workflow'; + +export const createWorkflowOpenAI = (context: WorkflowContext) => { + return createOpenAI({ + compatibility: "strict", + fetch: async (input, init) => { + try { + // Prepare headers from init.headers + const headers = init?.headers + ? Object.fromEntries(new Headers(init.headers).entries()) + : {}; -* An Upstash account with: - * A QStash project for workflows - * A Redis database for Realtime -* Next.js application set up -* Completed the [basic real-time workflow setup](./basic) + // Prepare body from init.body + const body = init?.body ? JSON.parse(init.body as string) : undefined; -## Event Types + // Make network call + const responseInfo = await context.call("openai-call-step", { + url: input.toString(), + method: init?.method as HTTPMethods, + headers, + body, + }); -For human-in-the-loop workflows, extend your schema in `lib/realtime.ts` with these additional event types: + // Construct headers for the response + const responseHeaders = new Headers( + Object.entries(responseInfo.header).reduce((acc, [key, values]) => { + acc[key] = values.join(", "); + return acc; + }, {} as Record) + ); -```typescript {8-14} -const schema = { - workflow: { - runFinish: z.object({}), - stepFinish: z.object({ - stepName: z.string(), - result: z.unknown().optional(), - }), - waitingForInput: z.object({ - eventId: z.string(), - message: z.string(), - }), - inputResolved: z.object({ - eventId: z.string(), - }), - }, + // Return the constructed response + return new Response(JSON.stringify(responseInfo.body), { + status: responseInfo.status, + headers: responseHeaders, + }); + } catch (error) { + if (error instanceof WorkflowAbort) { + throw error + } else { + console.error("Error in fetch implementation:", error); + throw error; // Rethrow error for further handling + } + } + }, + }); }; ``` -The new event types are: +### Using OpenAI client to generate text -* **`waitingForInput`**: Emitted when the workflow pauses and needs user input -* **`inputResolved`**: Emitted when the user provides input, so the frontend knows to clear the waiting state +Now that we've created the OpenAI client, we can use it to generate the text. -## Create the Realtime Middleware +For that, we're going to create a new workflow endpoint that uses the payload as prompt to generate text using the OpenAI client. -Create a custom middleware that will emit events to Realtime at `lib/middleware.ts`: +```typescript {8, 16-20} +import { serve } from "@upstash/workflow/nextjs"; +import { WorkflowAbort } from '@upstash/workflow'; +import { generateText, ToolExecutionError } from 'ai'; -```typescript title="lib/middleware.ts" -import { WorkflowMiddleware } from "@upstash/workflow"; -import { realtime } from "./realtime"; +import { createWorkflowOpenAI } from './model'; -export const realtimeMiddleware = new WorkflowMiddleware({ - name: "realtime-events", - callbacks: { - beforeExecution: async ({ context, stepName }) => { - const channel = realtime.channel(context.workflowRunId); +export const { POST } = serve<{ prompt: string }>(async (context) => { + const openai = createWorkflowOpenAI(context); - // Detect wait-for-event steps and emit waitingForInput - if (stepName === "wait-for-approval") { - await channel.emit("workflow.waitingForInput", { - eventId: `approval-${context.workflowRunId}`, - message: `Waiting for approval`, - }); - } - }, - afterExecution: async ({ context, stepName, result }) => { - const channel = realtime.channel(context.workflowRunId); + // Important: Must have a step before generateText + const prompt = await context.run("get prompt", async () => { + return context.requestPayload.prompt; + }); - // Emit inputResolved after wait-for-event steps complete - if (stepName === "wait-for-approval") { - await channel.emit("workflow.inputResolved", { - eventId: `approval-${context.workflowRunId}`, - }); - } + try { + const result = await generateText({ + model: openai('gpt-3.5-turbo'), + maxTokens: 2048, + prompt, + }); - // Emit stepFinish for all steps - await channel.emit("workflow.stepFinish", { - stepName, - result, - }); - }, - runCompleted: async ({ context }) => { - const channel = realtime.channel(context.workflowRunId); - await channel.emit("workflow.runFinish", {}); - }, - }, + await context.run("text", () => { + console.log(`TEXT: ${result.text}`); + return result.text; + }); + + } catch (error) { + if (error instanceof ToolExecutionError && error.cause instanceof WorkflowAbort) { + throw error.cause; + } else { + throw error; + } + } }); ``` -**Key points:** +We can either [run the app locally](/docs/workflow/howto/local-development/development-server) or deploy it. Once the app is running, we can trigger the workflow using the following code: -* The middleware handles all realtime event emissions automatically -* `beforeExecution`: Detects wait-for-event steps by checking the stepName and emits `workflow.waitingForInput` -* `afterExecution`: Emits `workflow.inputResolved` for wait steps and `workflow.stepFinish` for all steps -* `runCompleted`: Emits `workflow.runFinish` when the workflow finishes -* All emission logic is centralized in the middleware, keeping workflow code clean +```ts +import { Client } from "@upstash/workflow"; -## Building the Workflow +const client = new Client({ token: "" }); +const { workflowRunId } = await client.trigger({ + url: "https:///", + body: { "prompt": "How is the weather in San Francisco around this time?" } +}); +``` -### 1. Create the Workflow Endpoint +The workflow will execute, and we can view the logs in `the Workflow dashboard` -Create your workflow at `app/api/workflow/human-in-loop/route.ts`: + Workflow logs in dashboard -```typescript title="app/api/workflow/human-in-loop/route.ts" -import { serve } from "@upstash/workflow/nextjs"; -import { realtimeMiddleware } from "@/lib/middleware"; +### Advanced Implementation with Tools -type WorkflowPayload = { - userId: string; - action: string; -}; +Tools allow the AI model to perform specific actions during text generation. You can learn more about tools in the [Vercel AI SDK documentation](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling). -export const { POST } = serve( - async (context) => { - const { userId, action } = context.requestPayload; +When using tools with Upstash Workflow, each tool execution must be wrapped in a workflow step. - // Step 1: Initial Processing - await context.run("initial-processing", async () => { - // Your processing logic - return { - preprocessed: true, - userId, - action, - requiresApproval: true, - }; - }); + +The `maxSteps` parameter must be greater than 1 when using tools to allow the model to process tool results and generate final responses. See the [tool steps documentation](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#tool-steps) for detailed explanation. + - // Step 2: Wait for Human Approval - const eventId = `approval-${context.workflowRunId}`; +```typescript {24-30, 33} +import { z } from 'zod'; +import { serve } from "@upstash/workflow/nextjs"; +import { WorkflowAbort } from '@upstash/workflow'; +import { generateText, ToolExecutionError, tool } from 'ai'; - const { eventData, timeout } = await context.waitForEvent<{ - approved: boolean; - }>("wait-for-approval", eventId, { timeout: "5m" }); +import { createWorkflowOpenAI } from './model'; - // Handle timeout - if (timeout) { - return { success: false, reason: "timeout" }; - } +export const { POST } = serve<{ prompt: string }>(async (context) => { + const openai = createWorkflowOpenAI(context); - const status = eventData.approved ? "approved" : "rejected"; + const prompt = await context.run("get prompt", async () => { + return context.requestPayload.prompt; + }); - // Step 3: Process based on approval - await context.run(`process-${status}`, async () => { - return { - status, - processedAt: Date.now(), - action, - userId, - }; + try { + const result = await generateText({ + model: openai('gpt-3.5-turbo'), + tools: { + weather: tool({ + description: 'Get the weather in a location', + parameters: z.object({ + location: z.string().describe('The location to get the weather for'), + }), + execute: ({ location }) => context.run("weather tool", () => { + // Mock data, replace with actual weather API call + return { + location, + temperature: 72 + Math.floor(Math.random() * 21) - 10, + }; + }) + }), + }, + maxSteps: 2, + prompt, }); - // Step 4: Finalize (only if approved) - if (eventData.approved) { - // Additional steps... + await context.run("text", () => { + console.log(`TEXT: ${result.text}`); + return result.text; + }); + } catch (error) { + if (error instanceof ToolExecutionError && error.cause instanceof WorkflowAbort) { + throw error.cause; + } else { + throw error; } - - return { - success: true, - approved: eventData.approved, - workflowRunId: context.workflowRunId, - }; - }, - { - middlewares: [realtimeMiddleware], } -); +}); ``` -**Key patterns:** - -1. **Middleware for all events**: The `realtimeMiddleware` automatically handles all realtime event emissions by detecting wait-for-event steps through the stepName -2. **Step name detection**: The middleware checks if `stepName === "wait-for-approval"` to know when to emit `waitingForInput` and `inputResolved` events -3. **Unique event IDs**: Use a unique `eventId` (like `approval-${workflowRunId}`) to identify which approval request this is -4. **Timeout handling**: Always handle the timeout case when waiting for events - -### 2. Create the Notify Endpoint +When called with the same prompt as above, we will see the following logs: -Create an endpoint at `app/api/notify/route.ts` to handle user input: + -```typescript -import { Client } from "@upstash/workflow"; -import { NextRequest, NextResponse } from "next/server"; +## Important Considerations -const workflowClient = new Client({ - baseUrl: process.env.QSTASH_URL!, - token: process.env.QSTASH_TOKEN!, -}); +When using Upstash Workflow with the Vercel AI SDK, there are several critical requirements that must be followed: -export async function POST(request: NextRequest) { - const body = await request.json(); - const { eventId, eventData } = body; +### Step Execution Order - if (!eventId) { - return NextResponse.json( - { success: false, error: "eventId is required" }, - { status: 400 } - ); - } +The most critical requirement is that `generateText` cannot be called before any workflow step. Always have a step before `generateText`. This could be a step which gets the prompt: - // Notify the workflow - await workflowClient.notify({ - eventId, - eventData, - }); + +```typescript ❌ Wrong {4} +export const { POST } = serve<{ prompt: string }>(async (context) => { + const openai = createWorkflowOpenAI(context); - return NextResponse.json({ success: true }); -} + // Will throw "prompt is undefined" + const result = await generateText({ + model: openai('gpt-3.5-turbo'), + prompt: context.requestPayload.prompt + }); +}); ``` - -**Preventing Race Conditions**: If you need to trigger a workflow and immediately send it an event, you can use the `workflowRunId` parameter to enable lookback: +```typescript ✅ Correct {3-7} +export const { POST } = serve<{ prompt: string }>(async (context) => { + const openai = createWorkflowOpenAI(context); -```typescript -await workflowClient.notify({ - eventId, - eventData, - workflowRunId: "wfr_abc123", // Ensures notification is delivered even if sent before waitForEvent + // Get prompt in a step first + const prompt = await context.run("get prompt", async () => { + return context.requestPayload.prompt; + }); + + const result = await generateText({ + model: openai('gpt-3.5-turbo'), + prompt + }); }); ``` + -Learn more in the [notify documentation](/docs/workflow/basics/client/notify). - +### Error Handling Pattern -## Building the Frontend +You must use the following error handling pattern exactly as shown. The conditions and their handling should not be modified: -### 1. Extend the Custom Hook +```typescript {3-9} +try { + // Your generation code +} catch (error) { + if (error instanceof ToolExecutionError && error.cause instanceof WorkflowAbort) { + throw error.cause; + } else { + throw error; + } +} +``` -Extend your hook from the basic example to handle waiting states: +### Tool Implementation +When implementing tools: +* Each tool's `execute` function must be wrapped in a `context.run()` call +* Tool steps should have descriptive names for tracking +* Tools must follow the same error handling pattern as above + +Example: ```typescript -"use client"; +execute: ({ location }) => context.run("weather tool", () => { + // Mock data, replace with actual weather API call + return { + location, + temperature: 72 + Math.floor(Math.random() * 21) - 10, + }; +}) +``` -import { useRealtime } from "@/lib/realtime-client"; -import { useState, useCallback } from "react"; +# Anthropic +Source: https://upstash.com/docs/workflow/integrations/anthropic -interface WorkflowStep { - stepName: string; - result?: unknown; -} +The standard way to call a third-party endpoint in your workflow is by using [`context.call`](/docs/workflow/basics/context#context-call). -interface WaitingState { - eventId: string; - message: string; -} +However, if you need to call the Anthropic endpoint for text generation ([`/v1/messages`](https://docs.anthropic.com/en/api/messages)), you can leverage the type-safe method `context.api.anthropic.call` method: -export function useWorkflowWithRealtime() { - const [workflowRunId, setWorkflowRunId] = useState(null); - const [steps, setSteps] = useState([]); - const [waitingState, setWaitingState] = useState(null); - const [isTriggering, setIsTriggering] = useState(false); - const [isRunFinished, setIsRunFinished] = useState(false); + + `context.api.anthropic.call` is not yet available in + [workflow-py](https://github.com/upstash/workflow-py). You can use `context.call` instead to work with Anthropic. See our + [Roadmap](/docs/workflow/roadmap) for feature parity plans and + [Changelog](/docs/workflow/changelog) for updates. + - useRealtime({ - enabled: !!workflowRunId, - channels: workflowRunId ? [workflowRunId] : [], - events: [ - "workflow.stepFinish", - "workflow.runFinish", - "workflow.waitingForInput", - "workflow.inputResolved", - ], - onData({ event, data }) { - if (event === "workflow.stepFinish") { - setSteps((prev) => [ - ...prev, - { - stepName: data.stepName, - result: data.result, - }, - ]); - } else if (event === "workflow.runFinish") { - setIsRunFinished(true); - } else if (event === "workflow.inputResolved") { - // Clear waiting state if it matches - setWaitingState((prev) => - prev?.eventId === data.eventId ? null : prev - ); - } else if (event === "workflow.waitingForInput") { - setWaitingState({ - eventId: data.eventId, - message: data.message, - }); - } +```ts +const { status, body } = await context.api.anthropic.call( + "Call Anthropic", + { + token: "", + operation: "messages.create", + body: { + model: "claude-3-5-sonnet-20241022", + max_tokens: 1024, + messages: [ + {"role": "user", "content": "Hello, world"} + ] }, - }); + } +); - const trigger = useCallback(async () => { - setIsTriggering(true); - setSteps([]); - setWaitingState(null); - setIsRunFinished(false); +// get text: +console.log(body.content[0].text) +``` - const response = await fetch("/api/trigger", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ workflowType: "human-in-loop" }), - }); +The SDK provides predefined types for the body field in both the request parameters and the response, simplifying common use cases. If you need to customize these types, you can override them as shown below: - const data = await response.json(); - setWorkflowRunId(data.workflowRunId); - setIsTriggering(false); - }, []); +```ts +type ResponseBodyType = { ... }; // Define your response body type +type RequestBodyType = { ... }; // Define your request body type - const continueWorkflow = useCallback( - async (data: { approved: boolean }) => { - if (!waitingState) { - throw new Error("No workflow waiting for input"); - } +const { status, body } = await context.api.anthropic.call< + ResponseBodyType, + RequestBodyType +>( + "Call Anthropic", + { + ... + } +); +``` - const response = await fetch("/api/notify", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - eventId: waitingState.eventId, - eventData: data, - }), - }); +# Datadog - Upstash QStash Integration +Source: https://upstash.com/docs/workflow/integrations/datadog - if (!response.ok) { - throw new Error("Failed to notify workflow"); - } +This guide walks you through connecting your Datadog account with Upstash QStash for monitoring and analytics of your message delivery, retries, DLQ, and schedules. - // The waiting state will be cleared when we receive inputResolved event - }, - [waitingState] - ); + +**Integration Scope** - return { - trigger, - continueWorkflow, - isTriggering, - workflowRunId, - steps, - waitingState, - isRunFinished, - }; -} -``` +Upstash Datadog Integration covers Prod Pack. -**Key additions:** + -* **`waitingState`**: Tracks when the workflow is waiting for input -* **`continueWorkflow`**: Function to submit user decisions back to the workflow -* **Multiple events subscription**: Uses `events` array to subscribe to multiple event types -* **Input resolved handling**: Clears the waiting state when the workflow receives the user's input +## **Step 1: Log in to Your Datadog Account** -### 2. Use the Hook with Approval UI +1. Go to [Datadog](https://www.datadoghq.com/) and sign in. -```typescript -"use client"; +## **Step 2: Install Upstash Application** -import { useWorkflowWithRealtime } from "@/hooks/useWorkflowWithRealtime"; +1. In Datadog, open the Integrations page. +2. Search for "Upstash" and open the integration. + +![integration-tab.png]() + +Click "Install" to add Upstash to your Datadog account. + +![installation.png]() -export default function WorkflowPage() { - const { - trigger, - isTriggering, - steps, - isRunFinished, - waitingState, - continueWorkflow, - } = useWorkflowWithRealtime(); +## **Step 3: Connect Accounts** - return ( -
- +After installing Upstash, click "Connect Accounts". Datadog will redirect you to Upstash to complete account linking. - {isRunFinished && ( -

✅ Workflow Finished!

- )} +![connect-acc.png]() - {/* Show workflow steps */} -

Workflow Steps:

-
- {steps.map((step, index) => ( -
- {step.stepName} - {Boolean(step.result) && ( - : {JSON.stringify(step.result)} - )} -
- ))} -
+## **Step 4: Select Account to Integrate** - {/* Show approval UI when waiting for input */} - {waitingState && ( -
-

{waitingState.message}

-

- -

-

- -

-
- )} -
- ); -} -``` +1. On Upstash, select the Datadog account to integrate. +2. Personal and team accounts are supported. -## How the Pattern Works +**Caveats** -### Timeline of Events +* The integration can be established once at a time. To change the account scope (e.g., add/remove teams), re-establish the integration from scratch. -1. **Initial processing**: `stepFinish` event → Frontend shows completed step -2. **Waiting for approval**: `waitingForInput` event → Frontend shows approval buttons -3. **User clicks approve/reject**: Frontend calls `/api/notify` -4. **Workflow resumes**: `inputResolved` event → Frontend hides approval buttons -5. **Processing continues**: More `stepFinish` events as workflow continues -6. **Workflow completes**: `runFinish` event → Frontend shows "Workflow Finished!" +![personal.png]() -## Benefits +![team.png]() -* **Real-time feedback**: Users see exactly when their approval is needed -* **No polling**: Instant updates via Server-Sent Events -* **Timeout handling**: Workflows don't hang indefinitely waiting for input +## **Step 5: Wait for Metrics Availability** -## Full Example +Once the integration is completed, metrics from QStash (publish counts, success/error rates, retries, DLQ, schedule executions) will start appearing in Datadog dashboards shortly. -For a complete working example with all steps, error handling, and full UI components, check out the [Upstash Realtime example on GitHub](https://github.com/upstash/workflow-js/tree/main/examples/upstash-realtime). +![upstash-dashboard.png]() -## Next Steps +## **Step 6: Datadog Integration Removal Process** -* Review the [basic real-time workflow pattern](./basic) -* Learn about [workflow event handling](/docs/workflow/features/wait-for-event) -* Explore [Realtime features](/docs/realtime/overall/quickstart) -* Check out [workflow failure handling](/docs/workflow/features/failure-callback) +From Datadog → Integrations → Upstash, press "Remove" to break the connection. -# Redact Private Data -Source: https://upstash.com/docs/workflow/howto/redact-fields +### Confirm Removal -Workflow runs can contain private data that you don't want visible in the Upstash Console or API responses. +Upstash will stop publishing metrics after removal. Ensure any Datadog API keys/configurations for this integration are also removed on the Datadog side. -Upstash Workflow allows you to redact specific fields so they appear as `REDACTED:` in the dashboard and API. The original values are still used when delivering requests to your workflow endpoint. The SHA256 hash lets you verify the data without revealing the original values. +## **Conclusion** -To redact fields, pass the `redact` option when triggering a workflow run. +You’ve connected Datadog with Upstash QStash. Explore Datadog dashboards to monitor message delivery performance and reliability. -Available options: -| Option | Description | -|--|--| -| body | Redact the body of the workflow steps | -| headers | Redact the headers of the workflow steps | -| headers[header_name] | Redact a specific header (e.g., `headers[Authorization]`) | +If you need help, contact support. - -Redaction is one-way. Once a field is redacted, the original value cannot be retrieved from the API or dashboard. - +# OpenAI +Source: https://upstash.com/docs/workflow/integrations/openai - -```typescript TypeScript -import { Client } from "@upstash/workflow"; +### Calling OpenAI -const client = new Client({ token: "" }); +The standard way to call a third-party endpoint in your workflow is by using [`context.call`](/docs/workflow/basics/context#context-call). -const { workflowRunId } = await client.trigger({ - url: "https://my-app.com/api/workflow", - body: { hello: "world" }, - redact: { - body: true, - header: ["Authorization"] // or `header: true` to redact all headers - }, -}); -``` +However, if you need to call the OpenAI endpoint for text generation ([`/v1/chat/completions`](https://platform.openai.com/docs/api-reference/chat)), you can leverage the type-safe method `context.api.openai.call` method: -```python Python -from upstash_workflow import Client + + `context.api.openai.call` is not yet available in + [workflow-py](https://github.com/upstash/workflow-py). You can use `context.call` instead to work with OpenAI. See our + [Roadmap](/docs/workflow/roadmap) for feature parity plans and + [Changelog](/docs/workflow/changelog) for updates. + -client = Client("") -client.trigger( - url="https://my-app.com/api/workflow", - body={ - "hello": "world", - }, - redact={ - "body": True, - "header": ["Authorization"] // or `header: True` to redact all headers +```typescript OpenAI +const { status, body } = await context.api.openai.call( + "Call OpenAI", + { + token: "", + operation: "chat.completions.create", + body: { + model: "gpt-4o", + messages: [ + { + role: "system", + content: "Assistant says 'hello!'", + }, + { + role: "user", + content: "User shouts back 'hi!'" + } + ], }, -) -``` + } +); -```bash cURL -curl -XPOST \ - -H 'Authorization: Bearer XXX' \ - -H "Content-Type: application/json" \ - -H "Upstash-Redact-Fields: body, header[Authorization]" \ - -d '{ "hello": "world" }' \ - 'https://qstash.upstash.io/v2/publish/https://my-app.com/api/workflow' +// get text: +console.log(body.content[0].text) ``` - - +The SDK provides predefined types for the body field in both the request parameters and the response, simplifying common use cases. If you need to customize these types, you can override them as shown below: -Redaction is configured per workflow run, so you can redact different fields for different runs. +```ts +type ResponseBodyType = { ... }; // Define your response body type +type RequestBodyType = { ... }; // Define your request body type -When `body` is redacted, the step outputs in the dashboard and API will show `REDACTED:` instead of the actual values. The workflow still executes with the original data. +const { status, body } = await context.api.openai.call< + ResponseBodyType, + RequestBodyType +>( + "Call OpenAI", + { + ... + } +); +``` -If a workflow run fails and moves to the DLQ, the redacted fields remain redacted in the DLQ. -However, when you retry, resume or restart a workflow run from DLQ, Workflow delivers the original values to your endpoint. +### OpenAI Compatible Provider -# Schedule a Workflow -Source: https://upstash.com/docs/workflow/howto/schedule +If you want to call an OpenAI compatible provider, you can do so using the `baseURL` parameter: -You can schedule a workflow to run periodically using a cron definition. +```ts +const { status, body } = await context.api.openai.call( + "Call Deepseek", + { + baseURL: "https://api.deepseek.com", + token: process.env.DEEPSEEK_API_KEY, + operation: "chat.completions.create", + body: { + model: "deepseek-chat", + messages: [ + { + role: "system", + content: "Assistant says 'hello!'", + }, + { + role: "user", + content: "User shouts back 'hi!'" + } + ], + }, + } +); +``` -For this feature, you would need to use Upstash QStash's Schedules feature. +# Prometheus - Upstash QStash Integration +Source: https://upstash.com/docs/workflow/integrations/prometheus -## Scheduling a workflow +To monitor your QStash metrics in Prometheus and visualize in Grafana, follow these steps: -For example, let's suppose that you have a workflow that creates a backup of some important data daily. Our workflow endpoint might look like this: + +**Integration Scope** -To run this endpoint on a schedule, navigate to `Schedules` in your QStash dashboard and click `Create Schedule`: +Upstash Prometheus Integration covers Prod Pack. - + -Enter your live endpoint URL, add a CRON expression to define the interval at which your endpoint is called (i.e. every day, every 15 minutes, ...) and click `Schedule`: +## **Step 1: Enable Prometheus in Upstash Console** - +1. Open the Upstash Console and navigate to QStash. +2. Go to Settings → Monitoring. +3. Enable Prometheus to allow scraping QStash metrics. -Your workflow will now run repeatedly at the interval you have defined. For more details on CRON expressions, see our [QStash scheduling documentation](/docs/qstash/features/schedules). +![configuration.png]() -## Programmatically Schedule +## **Step 2: Copy Monitoring Token** -In order to massively improve the user experience, many applications send weekly summary reports to their users. These could be weekly analytics summaries or SEO statistics to keep users engaged with the platform. +1. After enabling, a monitoring token is generated and displayed. +2. Copy the token. It will be used to authenticate Prometheus requests. -Let's create a user-specific schedule, sending a first report to each user exactly 7 days after they signed up: + +**Header Format** - +Send the token as `Authorization: Bearer `. -```typescript api/sign-up/route.ts -import { signUp } from "@/utils/auth-utils"; -import { Client } from "@upstash/qstash"; + -const client = new Client({ token: process.env.QSTASH_TOKEN! }); +![monitoring-token.png]() -export async function POST(request: Request) { - const userData: UserData = await request.json(); +## **Step 3: Configure Prometheus (via Grafana Data Source)** - // Schedule weekly account summary - await client.schedules.create({ - scheduleId: `user-summary-${user.email}`, - destination: "https:///api/send-weekly-summary", - body: { userId: user.id }, - cron: cron, - }); +1. In Grafana, add a Prometheus data source. +2. Set the address to `https://api.upstash.com/monitoring/prometheus`. +3. In HTTP headers, add the monitoring token. - return NextResponse.json( - { success: true, message: "User registered and summary scheduled" }, - { status: 201 } - ); -} -``` +![datasource.png]() -```python main.py -from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse -from qstash import AsyncQStash -from datetime import datetime, timedelta +![headers.png]() -app = FastAPI() +Click Test and Save. -client = AsyncQStash("") +![datasource-final.png]() -@app.post("/api/sign-up") -async def sign_up(request: Request): - user_data = await request.json() +## **Step 4: Import Dashboard** - # Simulate user registration - user = await sign_up(user_data) +You can use the Upstash Grafana dashboard to visualize QStash metrics. - # Calculate the date for the first summary (7 days from now) - first_summary_date = datetime.now() + timedelta(days=7) +Open the import dialog and use: Upstash QStash Dashboard - # Create cron expression for weekly summaries starting 7 days from signup - cron = f"{first_summary_date.minute} {first_summary_date.hour} * * {first_summary_date.day}" +![grafana-dashboard.png]() - # Schedule weekly account summary - await client.schedule.create_json( - schedule_id=f"user-summary-{user.email}", - destination="https:///api/send-weekly-summary", - body={"userId": user.id}, - cron=cron, - ) +## **Conclusion** - return JSONResponse( - content={"success": True, "message": "User registered and summary scheduled"}, - status_code=201, - ) +You’ve integrated QStash with Prometheus. Use Grafana to explore message throughput, retries, DLQ, schedules, and Upstash Workflows. -``` +If you encounter issues, contact support. - +# Resend +Source: https://upstash.com/docs/workflow/integrations/resend -This code will call our workflow every week, starting exactly seven days after a user signs up. Each call to our workflow will contain the respective user's ID. +The standard way to call a third-party endpoint in your workflow is by using [`context.call`](/docs/workflow/basics/context#context-call). + +However, if you need to call the Resend endpoint to send emails ([`/emails`](https://resend.com/docs/api-reference/emails/send-email) or [`/emails/batch`](https://resend.com/docs/api-reference/emails/send-batch-emails)), you can leverage the type-safe method `context.api.resend.call` method: - When creating a per-user schedule, pass a unique `scheduleId` to identify the schedule for better management and observability. + `context.api.resend.call` is not yet available in + [workflow-py](https://github.com/upstash/workflow-py). You can use `context.call` instead to work with Resend. See our + [Roadmap](/docs/workflow/roadmap) for feature parity plans and + [Changelog](/docs/workflow/changelog) for updates. -# Secure a Workflow -Source: https://upstash.com/docs/workflow/howto/security + -To prevent unauthorized access to your workflow endpoint, you can add an authorization layer. -Upstash Workflow supports two approaches: +```typescript Single Email +const { status, body } = await context.api.resend.call( + "Call Resend", + { + token: "", + body: { + from: "Acme ", + to: ["delivered@resend.dev"], + subject: "Hello World", + html: "

It works!

", + }, + headers: { + "content-type": "application/json", + }, + } +); +``` -* **Built-in request verification** (recommended) -* **Custom authorization method** +```typescript Batch Email {4} +const { status, body } = await context.api.resend.call( + "Call Resend", + { + batch: true, + token: "", + body: [ + { + from: "Acme ", + to: ["delivered@resend.dev"], + subject: "Hello World", + html: "

It works!

", + }, + { + from: "Acme ", + to: ["delivered@resend.dev"], + subject: "Hello World", + html: "

It works!

", + }, + ], + headers: { + "content-type": "application/json", + }, + } +); +``` -### Built-in request verification (recommended) +
-Upstash Workflow provides a built-in mechanism to secure your workflow endpoint by verifying request signatures. -Every request to your endpoint include a valid `Upstash-Signature` header. +The SDK provides predefined types for the body field in both the request parameters and the response, simplifying common use cases. If you need to customize these types, you can override them as shown below: -How it works: +```ts +type IsBatch = true; // Set to either true or false +type ResponseBodyType = { ... }; // Define your response body type +type RequestBodyType = { ... }; // Define your request body type -1. Upstash Workflow automatically adds the `Upstash-Signature` header to every request. - This signature is generated using your signing keys. +const { status, body } = await context.api.resend.call< + IsBatch, + ResponseBodyType, + RequestBodyType +>( + "Call Resend", + { + ... + } +); +``` -2. When this mechanism is enabled, the SDK verifies that the signature is valid before processing the request. +# llms.txt +Source: https://upstash.com/docs/workflow/llms-txt -This ensures that only requests originating from Upstash Workflow are processed. +# Pricing +Source: https://upstash.com/docs/workflow/pricing -To enable this verification, set the following environment variables in your application: +Upstash Workflow is based on QStash and uses a "pay-as-you-go" pricing model. You only incur costs when your app receives traffic, meaning there's no charge when it's not in use. Click [here](https://upstash.com/pricing/workflow) to view the pricing. -```bash .env -QSTASH_CURRENT_SIGNING_KEY=xxxxxxxxx -QSTASH_NEXT_SIGNING_KEY=xxxxxxxxx -``` +A workflow run consists of several QStash messages, with the total cost determined by the number of messages used. -You can find the values in Upstash Workflow dashboard. +You can track your current message usage and associated costs in the [Overview tab of the console](https://console.upstash.com/qstash?tab=details). - - For edge cases where environment variables cannot be used, you can explicitly create and pass a `Receiver` object to verify request signatures: +For detailed pricing information based on different plans, visit our [Workflow pricing page](https://upstash.com/pricing/workflow). - +### Message Usage per Workflow Run - ```typescript TypeScript - import { Receiver } from "@upstash/qstash"; - import { serve } from "@upstash/workflow/nextjs"; +* [context.run](/docs/workflow/basics/context#context-run), [context.sleep](/docs/workflow/basics/context#context-sleep), [context.sleepUntil](/docs/workflow/basics/context#context-sleepuntil), or [context.waitForEvent](/docs/workflow/basics/context#context-waitforevent) commands generate a single message. +* The [context.call](/docs/workflow/basics/context#context-call) command generates two messages. +* Each step in a [parallel run](/docs/workflow/howto/parallel-runs) costs 1 extra message. +* If the workflow endpoint or URL in [context.call](/docs/workflow/basics/context#context-call) returns an error or is unreachable, the workflow SDK will retry the call (up to 3 times by default). Each retry counts as a new message. - export const { POST } = serve( - async (context) => { ... }, - { - receiver: new Receiver({ - currentSigningKey: "", - nextSigningKey: "", - }), - } - ); - ``` +- [Astro](https://upstash.com/docs/workflow/quickstarts/astro.md) +- [Cloudflare Workers](https://upstash.com/docs/workflow/quickstarts/cloudflare-workers.md) +- [Express.js](https://upstash.com/docs/workflow/quickstarts/express.md) +- [FastAPI](https://upstash.com/docs/workflow/quickstarts/fastapi.md) +- [Flask](https://upstash.com/docs/workflow/quickstarts/flask.md) +- [Hono](https://upstash.com/docs/workflow/quickstarts/hono.md) +- [Next.js & FastAPI](https://upstash.com/docs/workflow/quickstarts/nextjs-fastapi.md) +- [Next.js & Flask](https://upstash.com/docs/workflow/quickstarts/nextjs-flask.md) +- [Nuxt](https://upstash.com/docs/workflow/quickstarts/nuxt.md) +- [Supported Platforms](https://upstash.com/docs/workflow/quickstarts/platforms.md) +- [SolidJS](https://upstash.com/docs/workflow/quickstarts/solidjs.md) +- [SvelteKit](https://upstash.com/docs/workflow/quickstarts/svelte.md) +- [TanStack Start](https://upstash.com/docs/workflow/quickstarts/tanstack-start.md) +- [Next.js](https://upstash.com/docs/workflow/quickstarts/vercel-nextjs.md) - ```python Python - from qstash import Receiver +# Roadmap +Source: https://upstash.com/docs/workflow/roadmap - @serve.post( - "/api/example", - receiver=Receiver( - current_signing_key=os.environ["QSTASH_CURRENT_SIGNING_KEY"], - next_signing_key=os.environ["QSTASH_NEXT_SIGNING_KEY"], - ), - ) - async def example(context: AsyncWorkflowContext[str]) -> None: - ... + + We have moved the roadmap and the changelog to [Github Discussions](https://github.com/orgs/upstash/discussions) starting from October 2025.Now you can follow `In Progress` features. You can see that your `Feature Requests` are recorded. You can vote for them and comment your specific use-cases to shape the feature to your needs. + - ``` - - +# JavaScript SDK +Source: https://upstash.com/docs/workflow/sdk/workflow-js -## Custom Authorization Method +# Python SDK +Source: https://upstash.com/docs/workflow/sdk/workflow-py -You can implement your own authorization mechanism with Upstash Workflow. +# Overview +Source: https://upstash.com/docs/workflow/steps -The context object provides access to the initial request headers and payload on every workflow step. -You can use them to pass your custom authentication token to verify the requests. +A workflow's **context** is an object provided by the route function. - +The context object provides: +* **Workflow APIs** – functions for defining workflow steps. +* **Workflow Run Properties** – request payload, request headers, and other metadata. - ```typescript TypeScript + + ```typescript api/workflow/route.ts highlight={4-5} import { serve } from "@upstash/workflow/nextjs"; export const { POST } = serve( - async (context) => { - // 👇 Extract Bearer token form the request headers - const authHeader = context.headers.get("authorization"); - const bearerToken = authHeader?.split(" ")[1]; - - // 👇 Use your authentication function to verify the token - if (!isValid(bearerToken)) { - console.error("Authentication failed."); - return; - } - - // Your workflow steps.. - }, - { - failureFunction: async () => { - // 👇 Same auth check for failure function - const authHeader = context.headers.get("authorization"); - const bearerToken = authHeader?.split(" ")[1]; - - if (!isValid(bearerToken)) { - // ... - } - }, + // 👇 the workflow context + async (context) => { + // ... } ); ``` - ```python Python + ```python main.py from fastapi import FastAPI from upstash_workflow.fastapi import Serve from upstash_workflow import AsyncWorkflowContext @@ -49219,140 +49236,259 @@ You can use them to pass your custom authentication token to verify the requests serve = Serve(app) @serve.post("/api/example") - async def example(context: AsyncWorkflowContext[str]) -> None: - auth_header = context.headers.get("authorization") - bearer_token = auth_header.split(" ")[1] if auth_header else None + async def example(context: AsyncWorkflowContext[str]) -> None: ... - if not is_valid(bearer_token): - print("Authentication failed.") - return + ``` + - # Your workflow steps... +## Context Object Properties - ``` + + The request payload passed to the workflow run via `trigger()` call. + - + + The request headers passed to the workflow run via `trigger()` call. + - - If you implement custom authorization in your workflow route, you should also include the same authorization check in the failure function. + + The unique identifier of the current workflow run. + - The failure function executes independently of the route function, so without this check, unauthorized requests could trigger the failure function - + + The public URL of the workflow endpoint. + -# Start a Run -Source: https://upstash.com/docs/workflow/howto/start + + The URL used for workflow failure callback. -You’ve defined your workflow, and the final step is to trigger the endpoint! + If a failure function is defined, this is the same as the workflow's `url`. + -There are two main ways to start your workflow: + + The environment variables available to the workflow. + -### Using `client.trigger` (Recommended) + + The QStash client instance used by the workflow endpoint. + -We recommend using [`client.trigger`](/docs/workflow/basics/client/trigger) to start your workflow. + + The labels attached to the current workflow run, if set in [client.trigger](/docs/workflow/basics/client/trigger). + Defaults to an empty array when no label was set. + - - ```ts Single Workflow - import { Client } from "@upstash/workflow"; + + Deprecated. Use `labels` instead. When a run has multiple labels, this only + returns the first one. + - const client = new Client({ token: "" }) - const { workflowRunId } = await client.trigger({ - url: "https:///", - body: "hello there!", // optional body - headers: { ... }, // optional headers - workflowRunId: "my-workflow", // optional workflow run id - retries: 3 // optional retries in the initial request - delay: "10s" // optional delay value - failureUrl: "https://", // optional failure url - flowControl: { ... } // optional flow control - }) +## Context Object Functions - console.log(workflowRunId) - // prints wfr_my-workflow - ``` +You can use the functions exposed by context object to define workflow steps. - ```ts Multiple Workflows - import { Client } from "@upstash/workflow"; +* [context.run](/docs/workflow/basics/context/run) +* [context.sleep](/docs/workflow/basics/context/sleep) +* [context.sleepUntil](/docs/workflow/basics/context/sleepUntil) +* [context.waitForEvent](/docs/workflow/basics/context/waitForEvent) +* [context.createWebhook](/docs/workflow/basics/context/createWebhook) +* [context.waitForWebhook](/docs/workflow/basics/context/waitForWebhook) +* [context.notify](/docs/workflow/basics/context/notify) +* [context.invoke](/docs/workflow/basics/context/invoke) +* [context.call](/docs/workflow/basics/context/call) +* [context.cancel](/docs/workflow/basics/context/cancel) +* [context.api](/docs/workflow/basics/context/api) - const client = new Client({ token: "" }) - const results = await client.trigger([ - { - url: "", - // other options... - }, +# context.api +Source: https://upstash.com/docs/workflow/steps/api + +In addition to `context.call`, you can also make third‑party requests using the `context.api` namespace. + +This namespace provides built‑in integrations for **OpenAI**, **Anthropic**, and **Resend**, allowing you to make requests in a **type‑safe** manner. + + + +```typescript OpenAI +const { status, body } = await context.api.openai.call("Call OpenAI", { + token: "", + operation: "chat.completions.create", + body: { + model: "gpt-4o", + messages: [ { - url: "", - // other options... + role: "system", + content: "Assistant says 'hello!'", }, - ]) + { role: "user", content: "User shouts back 'hi!'" }, + ], + }, +}); +``` + +```typescript Anthropic +const { status, body } = await context.api.anthropic.call( + "Call Anthropic", + { + token: "", + operation: "messages.create", + body: { + model: "claude-3-5-sonnet-20241022", + max_tokens: 1024, + messages: [ + {"role": "user", "content": "Hello, world"} + ] + }, + } +); +``` + +```typescript Resend +const { status, body } = await context.api.resend.call("Call Resend", { + token: "", + body: { + from: "Acme ", + to: ["delivered@resend.dev"], + subject: "Hello World", + html: "

It works!

", + }, + headers: { + "content-type": "application/json", + }, +}); +``` - console.log(results[0].workflowRunId) - // prints wfr_my-workflow - ```
-### 2. Sending an HTTP Request +We'll continue adding more integrations over time. If you'd like to see a specific integration, feel free to contribute to the SDK or contact us with your suggestion. -This approach is recommended for quick testing via curl during development. +For detailed guides on usage and configuration, see the [Integrations section](/docs/workflow/integrations/openai). -You should **NOT** start the workflow run in production by direct calls to your endpoint. +# context.call +Source: https://upstash.com/docs/workflow/steps/call -```bash -curl -X POST https:/// \ - -H "my-header: foo" \ - -d '{"foo": "bar"}' -``` +`context.call()` performs an HTTP request as a workflow step, supporting longer response times up to 12 hours. - - If you’ve secured your endpoint with signing keys, only the `trigger` method will work. Direct calls to the endpoint (e.g., via `curl` or `fetch`) will not be possible since `Upstash-Signature` header is missing. +The request is executed by **Upstash on your behalf**, so your application does not consume compute resources during the request duration. - For more information, read [Secure a workflow](/docs/workflow/howto/security) documentation. - +If the endpoint responds with a non‑success status code (anything outside `200–299`), +`context.call()` still returns the response and the workflow continues. +This allows you to inspect the response (via the `status` field) and decide how to handle failure cases in your logic. -# Webhooks -Source: https://upstash.com/docs/workflow/howto/use-webhooks +If you want requests to retry automatically, you can explicitly pass a retry configuration. -This guide explains how to handle webhooks effectively in your Upstash Workflow applications. We'll walk through: +## Arguments -* setting up webhook endpoints -* verifying webhook requests -* and processing webhook events + + The URL of the HTTP endpoint to call. + - - You can find the project source code on GitHub. - + + TThe HTTP method to use (`GET`, `POST`, `PUT`, etc.). Defaults to `GET`. + -## Overview + + The request body as a string. + + + + A map of headers to include in the request. + + + + Number of retry attempts if the request fails. Defaults to `0` (no retries). + + + + Delay between retries (in milliseconds). By default, uses exponential backoff. You can use mathematical expressions and the special variable `retried` (current retry attempt count starting from 0). Examples: `1000`, `pow(2, retried)`, `max(10, pow(2, retried))`. + + + + Throttle outbound requests. + + See [Flow Control](/docs/workflow/features/flow-control) for details. + + + + A logical grouping key that identifies which requests share the same flow control limits. + + + + The maximum number of allowed requests per second. + + + + The maximum number of concurrent requests allowed. + + + + The time window used to enforce the defined rate limit. Default is `1s`. + + + + + + Maximum time (in seconds) to wait for a response. + If retries are enabled, this timeout applies individually to each attempt. + + + + When using [`serveMany`](/docs/workflow/features/invoke/serveMany#using-serve-manyny), you can call another workflow defined in the same `serveMany` by passing it to this parameter. + + +## Response + + + The HTTP response status code. + + + + The response body. + + `context.call()` attempts to parse the body as JSON. + If parsing fails, the raw body string is returned. + -Webhooks allow external services to notify your application when events occur. For example, you can use webhooks to receive notifications when a new order is placed in your e-commerce store, a new user signs up, or a new message is sent in your chat application. + + The response headers. + -Upstash Workflow provides a simple way to receive these events and trigger workflows based on the incoming data autonomously. + + In TypeScript, you can declare the expected result type for strong typing: -### Setting Up Webhook Endpoints + ```typescript + type ResultType = { + field1: string, + field2: number + }; -#### Basic Setup + const result = await context.call( ... ); + ``` + -To create a webhook endpoint, use the `serve` function from `@upstash/workflow`: +## Usage -```typescript TypeScript +```javascript TypeScript import { serve } from "@upstash/workflow/nextjs"; -export const { POST } = serve( - async (context) => { - // Your webhook handling logic here - }, - { - initialPayloadParser: (payload) => { - return payload; - }, - } -); +export const { POST } = serve<{ topic: string }>(async (context) => { + const { userId, name } = context.requestPayload; + + const { status, headers, body } = await context.call("sync-user-data", { + url: "https://my-third-party-app", // Endpoint URL + method: "POST", + body: JSON.stringify({ + userId, + name + }), + headers: { + authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + }, + } + ); +}); + ``` ```python Python @@ -49363,910 +49499,772 @@ from upstash_workflow import AsyncWorkflowContext app = FastAPI() serve = Serve(app) -def initial_payload_parser(payload): - return payload - -@serve.post("/api/example", initial_payload_parser=initial_payload_parser) -async def example(context: AsyncWorkflowContext[str]) -> None: - # Your webhook handling logic here - -``` - - - -#### Request Validation - -Always validate incoming webhook requests to ensure they're legitimate. This way, no one other than the original webhook source can trigger your workflow. Here's an example using Clerk webhooks with Svix: - - -```typescript Validate and Parse in Workflow - TypeScript -export const { POST } = serve(async (context) => { - const payloadString = context.requestPayload; - const headerPayload = context.headers; - - let event: WebhookEvent; - try { - event = await validateRequest(payloadString, headerPayload); - } catch { - return - } - - // Next steps based on the event - -}) - -``` - -```python Validate and Parse in Workflow - Python -async def validate_request(payload_string: str, header_payload: dict): - # Validate the request - pass +@dataclass +class Request: + topic: str @serve.post("/api/example") -async def example(context: AsyncWorkflowContext[str]) -> None: - payload_string = context.request_payload - header_payload = context.headers +async def example(context: AsyncWorkflowContext[Request]) -> None: + request: Request = context.request_payload - try: - event = await validate_request(payload_string, header_payload) - except: - return + result = await context.call( + "generate-long-essay", + url="https://api.openai.com/v1/chat/completions", + method="POST", + body={ + "model": "gpt-4o", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant writing really long essays that would cause a normal serverless function to timeout.", + }, + {"role": "user", "content": request["topic"]}, + ], + }, + headers={ + "authorization": f"Bearer {os.environ['OPENAI_API_KEY']}", + }, + ) - # Next steps based on the event + status, headers, body = result.status, result.headers, result.body ``` -```typescript Validation Function - TypeScript -import { Webhook } from "svix"; -import { WebhookEvent } from "@clerk/nextjs/server"; - -const webhookSecret = "YOUR_WEBHOOK_SECRET"; + -async function validateRequest(payloadString: string, headerPayload: Headers) { - const svixHeaders = { - "svix-id": headerPayload.get("svix-id") as string, - "svix-timestamp": headerPayload.get("svix-timestamp") as string, - "svix-signature": headerPayload.get("svix-signature") as string, - }; - const wh = new Webhook(webhookSecret); - return wh.verify(payloadString, svixHeaders) as WebhookEvent; -} -``` + + We provide integrations for **OpenAI, Anthropic, and Resend**, allowing you to call their APIs with strongly typed request bodies using `context.call`. + See [`context.api`](/docs/workflow/basics/context#context-api) for details. + -
+ + The `context.call()` function can make requests to any public API endpoint. However, it cannot: -### Handling Webhook events + * Make requests to localhost (unless you set up a local tunnel, [here's how](/docs/workflow/howto/local-development/local-tunnel)) + * Make requests to internal Upstash QStash endpoints. + -Use the context.run method to process webhook events in discrete, trackable steps: +# context.cancel +Source: https://upstash.com/docs/workflow/steps/cancel - +All of the methods covered so far are used to define workflow steps. -```typescript TypeScript -export const { POST } = serve(async (context) => { - // ... Parse and validate the incoming request +`context.cancel` is different — it allows you to **explicitly cancel the current workflow run**. - const user = await context.run( - "handle-webhook-event", - async () => { - if (event.type === "user.created") { - const { id: clerkUserId, email_addresses, first_name } = event.data; - const primaryEmail = email_addresses.find( - (email) => (email.id = event.data.primary_email_address_id) - ); +```ts +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload - if (!primaryEmail) { - return false; - } + const result = await context.run("check if canceled", () => { ... }); - return { - event: event.type, - userId: clerkUserId, - email: primaryEmail.email_address, - firstName: first_name, - } as UserPayload; - } - return false; - } - ); -}); + if (result.cancel) { + await context.cancel() // cancel the workflow run + } +}) ``` -```python Python -@serve.post("/api/example") -async def example(context: AsyncWorkflowContext[str]) -> None: - # ... Parse and validate the incoming request - - async def _handle_webhook_event(): - if event.type == "user.created": - clerk_user_id = event.data["id"] - email_addresses = event.data["email_addresses"] - first_name = event.data["first_name"] +When a workflow run is canceled: - primary_email = next( - ( - email - for email in email_addresses - if email.id == event.data["primary_email_address_id"] - ), - None, - ) +* It is labeled as **canceled** (not failed). +* The configured `failureFunction` **is not triggered**. +* No entries are sent to the **dead-letter queue (DLQ)**. - if not primary_email: - return False +# context.createWebhook +Source: https://upstash.com/docs/workflow/steps/createWebhook - return { - "event": event.type, - "user_id": clerk_user_id, - "email": primary_email["email_address"], - "first_name": first_name, - } +`context.createWebhook()` creates a unique webhook that can be called by external services to trigger workflow continuation. - return False +The webhook URL generated can be called multiple times to resume multiple [`context.waitForWebhook`](/docs/workflow/basics/context/waitForWebhook) steps. - user = await context.run("handle-webhook-event", _handle_webhook_event) +## Arguments -``` + + Name of the step. + - +## Response -After validating the webhook and extracting the initial user data, you'll often need to perform additional operations like creating customer records, sending welcome emails etc. + + The unique webhook URL that external services should call to resume the workflow. - + Can be called multiple times to resume multiple [`context.waitForWebhook`](/docs/workflow/basics/context/waitForWebhook) steps. + -```typescript TypeScript -export const { POST } = serve(async (context) => { - // ... Previous validation and user data extraction + + The internal event identifier associated with this webhook. + This is primarily used internally by [`context.waitForWebhook`](/docs/workflow/basics/context/waitForWebhook). + - if (!user) { - return; - } +## Usage - const customer = await context.run("create-stripe-customer", async () => { - return await stripe.customers.create({ - email: user.email, - name: `${user.firstName} ${user.lastName}`, - metadata: { - userId: user.userId, - }, - }); - }); +```typescript highlight={4} +import { serve } from "@upstash/workflow/nextjs"; - /// ... Additional steps +export const { POST } = serve(async (context) => { + const webhook = await context.createWebhook("create webhook"); + + console.log(webhook.webhookUrl); // Use this URL with external services }); ``` -```python Python -@serve.post("/api/example") -async def example(context: AsyncWorkflowContext[str]) -> None: - # ... Previous validation and user data extraction - - if not user: - return +For more complete examples and use cases, see [the page on webhooks](/docs/workflow/features/webhooks). - async def _create_stripe_customer(): - return await stripe.customers.create( - email=user["email"], - name=f"{user['first_name']} {user['last_name']}", - metadata={"user_id": user["user_id"]}, - ) +# context.invoke +Source: https://upstash.com/docs/workflow/steps/invoke - customer = await context.run("create-stripe-customer", _create_stripe_customer) +`context.invoke()` triggers another workflow run and pauses until the invoked workflow finishes. - # ... Additional steps +The calling workflow resumes once the invoked workflow either **succeeds**, **fails**, or is **canceled**. -``` + + Workflows can only invoke other workflows that were served together in the same `serveMany` route. + For details, see [Invoke other workflows](/docs/workflow/features/invoke). + - +## Arguments -You're now ready to perform any operation in the following steps. + + The workflow definition to invoke. + Must be a workflow exposed under the same `serveMany`. + -# Vercel AI SDK -Source: https://upstash.com/docs/workflow/integrations/aisdk + + The payload to send to the invoked workflow. + This value will be set as `context.requestPayload` in the invoked workflow. + - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/docs/workflow/roadmap) for feature parity plans and - [Changelog](/docs/workflow/changelog) for updates. - + + Optional HTTP headers to forward to the invoked workflow. + This value will be set as `context.headers` in the invoked workflow. + - - You can find the project source code which uses real APIs on Github. - + + Override the workflow run ID for the invoked workflow. + Defaults to a new ID if not specified. + -Upstash Workflow integrates with the Vercel AI SDK to provide durable and reliable AI applications. This allows you to: + + Number of retry attempts configuration of the invoked workflow. + Defaults to `3`. Retries use exponential backoff. + -* Build resilient AI applications with automatic retries -* Manage AI operations with workflow steps -* Implement tools and function calling with durability -* Handle errors gracefully across your AI operations -* Handle long-running AI operations with extended timeouts + + Delay between retries of the invoked workflow. + -This guide will walk you through setting up and implementing AI features using Upstash Workflow's durability guarantees with Vercel AI SDK's capabilities. + + Flow control configuration of the invoked workflow. -## Prerequisites + See [Flow Control](/docs/workflow/features/flow-control) for details. -Before getting started, make sure you have: + + + A logical grouping key that identifies which requests share the same flow control limits. + -* An OpenAI API key -* Basic familiarity with Upstash Workflow and Vercel AI SDK -* Vercel AI SDK version 4.0.12 or higher (required for ToolExecutionError handling) + + The maximum number of allowed requests per second. + -## Installation + + The maximum number of concurrent requests allowed. + -Install the required packages: + + The time window used to enforce the defined rate limit. Default is `1s`. + + + - -```bash npm -npm install @ai-sdk/openai ai zod -``` +## Response -```bash pnpm -pnpm install @ai-sdk/openai ai zod -``` + + The response body returned by the invoked workflow. + -```bash bun -bun install @ai-sdk/openai ai zod -``` + + `true` if the invoked workflow completed with failure. + - + + `true` if the invoked workflow was canceled before completion. + -## Implementation +## Usage -### Creating OpenAI client +```ts +const { body, isFailed, isCanceled } = await context.invoke( + "invoke another workflow", + { + workflow: anotherWorkflow, + body: "test", + header: {...}, // headers to pass to anotherWorkflow (optional) + retries, // number of retries (optional, default: 3) + retryDelay, // delay between retries (optional, uses exponential backoff by default) + flowControl, // flow control settings (optional) + workflowRunId // workflowRunId to set (optional) + } +); +``` -AI SDKs (Vercel AI SDK, OpenAI SDK etc.) uses the client's default fetch implementation to make API requests, but allows you to provide a custom fetch implementation. +# context.notify +Source: https://upstash.com/docs/workflow/steps/notify -In the case of Upstash Workflow, we need to use the `context.call` method to make HTTP requests. We can create a custom fetch implementation that uses `context.call` to make requests. By using `context.call`, Upstash Workflow is the one making the HTTP request and waiting for the response, even if it takes too long to receive response from the LLM. +`context.notify()` notifies workflows that are waiting for a specific event, passing along an optional payload. -The following code snippet can also be generalized to work with other LLM SDKs, such as Anthropic or Google. +It is typically used in combination with [`context.waitForEvent`](/docs/workflow/basics/context#context-waitforevent). -```typescript {18-24} -import { createOpenAI } from '@ai-sdk/openai'; -import { HTTPMethods } from '@upstash/qstash'; -import { WorkflowAbort, WorkflowContext } from '@upstash/workflow'; +## Arguments -export const createWorkflowOpenAI = (context: WorkflowContext) => { - return createOpenAI({ - compatibility: "strict", - fetch: async (input, init) => { - try { - // Prepare headers from init.headers - const headers = init?.headers - ? Object.fromEntries(new Headers(init.headers).entries()) - : {}; + + A unique identifier for the step. + - // Prepare body from init.body - const body = init?.body ? JSON.parse(init.body as string) : undefined; + + The identifier of the event to notify. + Must match the `eventId` used in `context.waitForEvent`. + - // Make network call - const responseInfo = await context.call("openai-call-step", { - url: input.toString(), - method: init?.method as HTTPMethods, - headers, - body, - }); + + Data to deliver to the waiting workflow(s). + This value will be returned in `eventData` from the corresponding `waitForEvent` call. + - // Construct headers for the response - const responseHeaders = new Headers( - Object.entries(responseInfo.header).reduce((acc, [key, values]) => { - acc[key] = values.join(", "); - return acc; - }, {} as Record) - ); + + The workflow run ID to notify. When provided, enables **lookback functionality** - the notification will be stored and delivered even if `notify` is called before `waitForEvent`. - // Return the constructed response - return new Response(JSON.stringify(responseInfo.body), { - status: responseInfo.status, - headers: responseHeaders, - }); - } catch (error) { - if (error instanceof WorkflowAbort) { - throw error - } else { - console.error("Error in fetch implementation:", error); - throw error; // Rethrow error for further handling - } - } - }, - }); -}; -``` + This solves race conditions where notifications might be sent before a workflow reaches its wait step. + -### Using OpenAI client to generate text +## Response -Now that we've created the OpenAI client, we can use it to generate the text. +`context.notify()` returns a list of waiters describing the workflows that were notified. -For that, we're going to create a new workflow endpoint that uses the payload as prompt to generate text using the OpenAI client. + + A list of `NotifyResponse` objects describing each workflow that was waiting on the event. -```typescript {8, 16-20} -import { serve } from "@upstash/workflow/nextjs"; -import { WorkflowAbort } from '@upstash/workflow'; -import { generateText, ToolExecutionError } from 'ai'; + + + The ID of the notification message delivered to the workflow. + This is unique to every notification. + -import { createWorkflowOpenAI } from './model'; + + The unique identifier of the workflow run that was notified. + -export const { POST } = serve<{ prompt: string }>(async (context) => { - const openai = createWorkflowOpenAI(context); + + Unix timestamp (in milliseconds) representing when the workflow was created. + - // Important: Must have a step before generateText - const prompt = await context.run("get prompt", async () => { - return context.requestPayload.prompt; - }); + + + + URL to call upon notify + + + Unix timestamp for when the wait will time out + + + Headers sent in case of notify + + + URL to call upon timeout + + + Body used in timeout request + + + Headers sent in case of time out + + + + - try { - const result = await generateText({ - model: openai('gpt-3.5-turbo'), - maxTokens: 2048, - prompt, - }); + - await context.run("text", () => { - console.log(`TEXT: ${result.text}`); - return result.text; - }); +## Usage - } catch (error) { - if (error instanceof ToolExecutionError && error.cause instanceof WorkflowAbort) { - throw error.cause; - } else { - throw error; - } - } -}); -``` +### Basic Notification -We can either [run the app locally](/docs/workflow/howto/local-development/development-server) or deploy it. Once the app is running, we can trigger the workflow using the following code: +```javascript +import { serve } from "@upstash/workflow/nextjs"; -```ts -import { Client } from "@upstash/workflow"; +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload; -const client = new Client({ token: "" }); -const { workflowRunId } = await client.trigger({ - url: "https:///", - body: { "prompt": "How is the weather in San Francisco around this time?" } + const { + notifyResponse, // result of notify, which is a list of notified waiters + } = await context.notify("notify step", "my-event-Id", payload); }); ``` -The workflow will execute, and we can view the logs in `the Workflow dashboard` +### Notification with Lookback - Workflow logs in dashboard +To prevent race conditions, you can provide a `workflowRunId`. This enables lookback - the notification will be stored and delivered even if sent before the target workflow reaches `waitForEvent`: -### Advanced Implementation with Tools +```javascript +import { serve } from "@upstash/workflow/nextjs"; -Tools allow the AI model to perform specific actions during text generation. You can learn more about tools in the [Vercel AI SDK documentation](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling). +export const { POST } = serve<{ orderId: string }>(async (context) => { + const { orderId } = context.requestPayload; -When using tools with Upstash Workflow, each tool execution must be wrapped in a workflow step. + // Process payment + await context.run("process-payment", async () => { + return processPayment(orderId); + }); - -The `maxSteps` parameter must be greater than 1 when using tools to allow the model to process tool results and generate final responses. See the [tool steps documentation](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#tool-steps) for detailed explanation. - + // Notify a specific workflow run with lookback support + const { + notifyResponse, + } = await context.notify( + "notify payment complete", + "payment-processed", + { orderId, status: "success" }, + "wfr_order_processor_123" // Enables lookback for this workflow run + ); +}); +``` -```typescript {24-30, 33} -import { z } from 'zod'; -import { serve } from "@upstash/workflow/nextjs"; -import { WorkflowAbort } from '@upstash/workflow'; -import { generateText, ToolExecutionError, tool } from 'ai'; +# context.run +Source: https://upstash.com/docs/workflow/steps/run -import { createWorkflowOpenAI } from './model'; +`context.run()` executes a piece of custom business logic as a workflow step. -export const { POST } = serve<{ prompt: string }>(async (context) => { - const openai = createWorkflowOpenAI(context); +It returns a `Promise`, so you can decide how steps execute: +* **Sequentially** by awaiting them one by one. +* **In parallel** by awaiting multiple steps together. - const prompt = await context.run("get prompt", async () => { - return context.requestPayload.prompt; - }); +## Arguments - try { - const result = await generateText({ - model: openai('gpt-3.5-turbo'), - tools: { - weather: tool({ - description: 'Get the weather in a location', - parameters: z.object({ - location: z.string().describe('The location to get the weather for'), - }), - execute: ({ location }) => context.run("weather tool", () => { - // Mock data, replace with actual weather API call - return { - location, - temperature: 72 + Math.floor(Math.random() * 21) - 10, - }; - }) - }), - }, - maxSteps: 2, - prompt, - }); + + A unique identifier for the step. + - await context.run("text", () => { - console.log(`TEXT: ${result.text}`); - return result.text; - }); - } catch (error) { - if (error instanceof ToolExecutionError && error.cause instanceof WorkflowAbort) { - throw error.cause; - } else { - throw error; - } - } -}); -``` + + The business logic to run inside this step. + -When called with the same prompt as above, we will see the following logs: +## Response - +Each step can return a JSON-serializable value—anything from simple primitives to complex objects. -## Important Considerations +The value is **JSON-serialized** and automatically restored across requests. -When using Upstash Workflow with the Vercel AI SDK, there are several critical requirements that must be followed: + + Avoid returning stateful resources such as database connections or file handles. -### Step Execution Order + Instead, return plain data (numbers, strings, arrays, objects) so the result can be safely persisted and restored across workflow executions. + -The most critical requirement is that `generateText` cannot be called before any workflow step. Always have a step before `generateText`. This could be a step which gets the prompt: +## Usage -```typescript ❌ Wrong {4} -export const { POST } = serve<{ prompt: string }>(async (context) => { - const openai = createWorkflowOpenAI(context); + ```typescript Serial execution (TypeScript) highlight={6-8, 10-12} + import { serve } from "@upstash/workflow/nextjs"; - // Will throw "prompt is undefined" - const result = await generateText({ - model: openai('gpt-3.5-turbo'), - prompt: context.requestPayload.prompt - }); -}); -``` + export const { POST } = serve(async (context) => { + const input = context.requestPayload; -```typescript ✅ Correct {3-7} -export const { POST } = serve<{ prompt: string }>(async (context) => { - const openai = createWorkflowOpenAI(context); + const result1 = await context.run("step-1", async () => { + return someWork(input); + }); - // Get prompt in a step first - const prompt = await context.run("get prompt", async () => { - return context.requestPayload.prompt; - }); + await context.run("step-2", async () => { + someOtherWork(result1); + }); + }); - const result = await generateText({ - model: openai('gpt-3.5-turbo'), - prompt - }); -}); -``` - + ``` -### Error Handling Pattern + ```typescript Parallel execution (TypeScript) + import { serve } from "@upstash/workflow/nextjs" -You must use the following error handling pattern exactly as shown. The conditions and their handling should not be modified: + export const { POST } = serve( + async (context) => { + const input = context.requestPayload; -```typescript {3-9} -try { - // Your generation code -} catch (error) { - if (error instanceof ToolExecutionError && error.cause instanceof WorkflowAbort) { - throw error.cause; - } else { - throw error; - } -} -``` + const promise1 = context.run("step-1", async () => { + return someWork(input); + }); -### Tool Implementation + const promise2 = context.run("step-2", async () => { + return someOtherWork(input); + }); -When implementing tools: -* Each tool's `execute` function must be wrapped in a `context.run()` call -* Tool steps should have descriptive names for tracking -* Tools must follow the same error handling pattern as above + await Promise.all([promise1, promise2]); + }, + ); + ``` -Example: -```typescript -execute: ({ location }) => context.run("weather tool", () => { - // Mock data, replace with actual weather API call - return { - location, - temperature: 72 + Math.floor(Math.random() * 21) - 10, - }; -}) -``` + ```python Serial execution (Python) + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext -# Anthropic -Source: https://upstash.com/docs/workflow/integrations/anthropic + app = FastAPI() + serve = Serve(app) -The standard way to call a third-party endpoint in your workflow is by using [`context.call`](/docs/workflow/basics/context#context-call). + @serve.post("/api/example") + async def example(context: AsyncWorkflowContext[str]) -> None: + input = context.request_payload -However, if you need to call the Anthropic endpoint for text generation ([`/v1/messages`](https://docs.anthropic.com/en/api/messages)), you can leverage the type-safe method `context.api.anthropic.call` method: + async def _step1(): + return some_work(input) - - `context.api.anthropic.call` is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). You can use `context.call` instead to work with Anthropic. See our - [Roadmap](/docs/workflow/roadmap) for feature parity plans and - [Changelog](/docs/workflow/changelog) for updates. - + result1 = await context.run("step-1", _step1) -```ts -const { status, body } = await context.api.anthropic.call( - "Call Anthropic", - { - token: "", - operation: "messages.create", - body: { - model: "claude-3-5-sonnet-20241022", - max_tokens: 1024, - messages: [ - {"role": "user", "content": "Hello, world"} - ] - }, - } -); + async def _step2(): + return some_other_work(result1) + + await context.run("step-2", _step2) -// get text: -console.log(body.content[0].text) -``` + ``` + -The SDK provides predefined types for the body field in both the request parameters and the response, simplifying common use cases. If you need to customize these types, you can override them as shown below: + + Because results are JSON-serialized, **class instances are restored as plain objects**. + This means instance methods will not be available unless you explicitly rehydrate the object. -```ts -type ResponseBodyType = { ... }; // Define your response body type -type RequestBodyType = { ... }; // Define your request body type + To fix this, you can recreate the instance using Object.assign() or a custom factory: + ```typescript + export const { POST } = serve( + async (context) => { -const { status, body } = await context.api.anthropic.call< - ResponseBodyType, - RequestBodyType ->( - "Call Anthropic", - { - ... - } -); -``` + let user = await context.run("step-1", async () => { + // 👇 Return a class instance from step + return new User("John Doe", "john.doe@example.com"); + }); -# Datadog - Upstash QStash Integration -Source: https://upstash.com/docs/workflow/integrations/datadog + // 👇 Properties are accessible by default + console.log(user.name) -This guide walks you through connecting your Datadog account with Upstash QStash for monitoring and analytics of your message delivery, retries, DLQ, and schedules. + // 👇 Create a Proper Instance with Object.assign() + user = Object.assign(new User(), user); - -**Integration Scope** + await context.run("greet", async () => { + // 👇 Now instance methods are available as well + console.log(user.greet()); + }); + } + ); + ``` + -Upstash Datadog Integration covers Prod Pack. +# context.sleep +Source: https://upstash.com/docs/workflow/steps/sleep - +`context.sleep()` pauses workflow execution for a specified duration. -## **Step 1: Log in to Your Datadog Account** +When a workflow is paused, the current request completes and a new one is automatically scheduled to resume after the delay. +This ensures no compute resources are consumed during the sleep period. -1. Go to [Datadog](https://www.datadoghq.com/) and sign in. +Always `await` a `sleep` step to properly pause execution. -## **Step 2: Install Upstash Application** +## Arguments -1. In Datadog, open the Integrations page. -2. Search for "Upstash" and open the integration. + + A unique identifier for the step. + -![integration-tab.png]() + + The duration to pause workflow execution. -Click "Install" to add Upstash to your Datadog account. + * **Human-readable string format:** -![installation.png]() + | Input | Duration | + |---------|-------------| + | `"10s"` | 10 seconds | + | `"1m"` | 1 minute | + | `"30m"` | 30 minutes | + | `"2h"` | 2 hours | + | `"1d"` | 1 day | + | `"1w"` | 1 week | + | `"1mo"` | 1 month | + | `"1y"` | 1 year | -## **Step 3: Connect Accounts** + * **Numeric format (seconds):** -After installing Upstash, click "Connect Accounts". Datadog will redirect you to Upstash to complete account linking. + | Input | Duration | + |---------|---------------| + | `60` | 60 seconds (1 minute) | + | `3600` | 3600 seconds (1 hour) | + | `86400` | 86400 seconds (1 day) | + -![connect-acc.png]() +## Usage -## **Step 4: Select Account to Integrate** + -1. On Upstash, select the Datadog account to integrate. -2. Personal and team accounts are supported. +```typescript TypeScript highlight={12-13} +import { serve } from "@upstash/workflow/nextjs"; +import { signIn, sendEmail } from "@/utils/onboarding-utils"; -**Caveats** +export const { POST } = serve(async (context) => { + const userData = context.requestPayload; -* The integration can be established once at a time. To change the account scope (e.g., add/remove teams), re-establish the integration from scratch. + const user = await context.run("sign-in", async () => { + const signedInUser = await signIn(userData); + return signedInUser; + }); -![personal.png]() + // 👇 Wait for one day (in seconds) + await context.sleep("wait-until-welcome-email", "1d"); -![team.png]() + await context.run("send-welcome-email", async () => { + return sendEmail(user.name, user.email); + }); +}); +``` -## **Step 5: Wait for Metrics Availability** +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext +from onboarding_utils import sign_in, send_email -Once the integration is completed, metrics from QStash (publish counts, success/error rates, retries, DLQ, schedule executions) will start appearing in Datadog dashboards shortly. +app = FastAPI() +serve = Serve(app) -![upstash-dashboard.png]() +@serve.post("/api/onboarding") +async def onboarding(context: AsyncWorkflowContext[User]) -> None: + user_data = context.request_payload -## **Step 6: Datadog Integration Removal Process** + async def _sign_in(): + return await sign_in(user_data) -From Datadog → Integrations → Upstash, press "Remove" to break the connection. + user = await context.run("sign-in", _sign_in) -### Confirm Removal + # 👇 Wait for one day (in seconds) + await context.sleep("wait-until-welcome-email", "1d") -Upstash will stop publishing metrics after removal. Ensure any Datadog API keys/configurations for this integration are also removed on the Datadog side. + async def _send_email(): + return await send_email(user.name, user.email) -## **Conclusion** + await context.run("send-welcome-email", _send_email) -You’ve connected Datadog with Upstash QStash. Explore Datadog dashboards to monitor message delivery performance and reliability. +``` -If you need help, contact support. + -# OpenAI -Source: https://upstash.com/docs/workflow/integrations/openai +# context.sleepUntil +Source: https://upstash.com/docs/workflow/steps/sleepUntil -### Calling OpenAI +`context.sleepUntil()` pauses workflow execution until a specific timestamp. -The standard way to call a third-party endpoint in your workflow is by using [`context.call`](/docs/workflow/basics/context#context-call). +When a workflow is paused, the current request completes and a new one is automatically scheduled to resume at the target time. +This ensures no compute resources are consumed while sleeping. -However, if you need to call the OpenAI endpoint for text generation ([`/v1/chat/completions`](https://platform.openai.com/docs/api-reference/chat)), you can leverage the type-safe method `context.api.openai.call` method: +Always await a `sleepUntil` step to properly pause execution. - - `context.api.openai.call` is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). You can use `context.call` instead to work with OpenAI. See our - [Roadmap](/docs/workflow/roadmap) for feature parity plans and - [Changelog](/docs/workflow/changelog) for updates. - +## Arguments -```typescript OpenAI -const { status, body } = await context.api.openai.call( - "Call OpenAI", - { - token: "", - operation: "chat.completions.create", - body: { - model: "gpt-4o", - messages: [ - { - role: "system", - content: "Assistant says 'hello!'", - }, - { - role: "user", - content: "User shouts back 'hi!'" - } - ], - }, - } -); + + A unique identifier for the step. + -// get text: -console.log(body.content[0].text) -``` + + The target time when the workflow should resume. + Accepted formats: + * A **number**: Unix timestamp in seconds + * A **Date object** + * A **string** that can be parsed by `new Date(string)` in JavaScript + -The SDK provides predefined types for the body field in both the request parameters and the response, simplifying common use cases. If you need to customize these types, you can override them as shown below: +## Usage -```ts -type ResponseBodyType = { ... }; // Define your response body type -type RequestBodyType = { ... }; // Define your request body type + -const { status, body } = await context.api.openai.call< - ResponseBodyType, - RequestBodyType ->( - "Call OpenAI", - { - ... - } -); -``` +```typescript TypeScript highlight={11-16} +import { serve } from "@upstash/workflow/nextjs"; +import { signIn, sendEmail } from "@/utils/onboarding-utils"; -### OpenAI Compatible Provider +export const { POST } = serve(async (context) => { + const userData = context.requestPayload; -If you want to call an OpenAI compatible provider, you can do so using the `baseURL` parameter: + const user = await context.run("sign-in", async () => { + return signIn(userData); + }); -```ts -const { status, body } = await context.api.openai.call( - "Call Deepseek", - { - baseURL: "https://api.deepseek.com", - token: process.env.DEEPSEEK_API_KEY, - operation: "chat.completions.create", - body: { - model: "deepseek-chat", - messages: [ - { - role: "system", - content: "Assistant says 'hello!'", - }, - { - role: "user", - content: "User shouts back 'hi!'" - } - ], - }, - } -); -``` + // 👇 Calculate the date for one week from now + const oneWeekFromNow = new Date(); + oneWeekFromNow.setDate(oneWeekFromNow.getDate() + 7); -# Prometheus - Upstash QStash Integration -Source: https://upstash.com/docs/workflow/integrations/prometheus + // 👇 Sleep until the calculated date + await context.sleepUntil("wait-for-one-week", oneWeekFromNow); -To monitor your QStash metrics in Prometheus and visualize in Grafana, follow these steps: + await context.run("send-welcome-email", async () => { + return sendEmail(user.name, user.email); + }); +}); +``` - -**Integration Scope** +```python Python +from fastapi import FastAPI +from datetime import datetime, timedelta +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext +from onboarding_utils import sign_in, send_email -Upstash Prometheus Integration covers Prod Pack. +app = FastAPI() +serve = Serve(app) - +@serve.post("/api/onboarding") +async def onboarding(context: AsyncWorkflowContext[User]) -> None: + user_data = context.request_payload -## **Step 1: Enable Prometheus in Upstash Console** + async def _sign_in(): + return await sign_in(user_data) -1. Open the Upstash Console and navigate to QStash. -2. Go to Settings → Monitoring. -3. Enable Prometheus to allow scraping QStash metrics. + user = await context.run("sign-in", _sign_in) -![configuration.png]() + # 👇 Calculate the date for one week from now + one_week_from_now = datetime.now() + timedelta(days=7) -## **Step 2: Copy Monitoring Token** + # 👇 Wait until the calculated date + await context.sleep_until("wait-for-one-week", one_week_from_now) -1. After enabling, a monitoring token is generated and displayed. -2. Copy the token. It will be used to authenticate Prometheus requests. + async def _send_email(): + return await send_email(user.name, user.email) - -**Header Format** + await context.run("send-welcome-email", _send_email) -Send the token as `Authorization: Bearer `. +``` - + -![monitoring-token.png]() +# context.waitForEvent +Source: https://upstash.com/docs/workflow/steps/waitForEvent -## **Step 3: Configure Prometheus (via Grafana Data Source)** +`context.waitForEvent` pauses workflow execution until a given event occurs or a timeout is reached. -1. In Grafana, add a Prometheus data source. -2. Set the address to `https://api.upstash.com/monitoring/prometheus`. -3. In HTTP headers, add the monitoring token. +Default timeout value is 7 days. -![datasource.png]() +## Arguments -![headers.png]() + + A unique identifier for the step. + -Click Test and Save. + + A unique identifier for the event to wait on. + -![datasource-final.png]() + + The maximum time to wait before continuing execution. -## **Step 4: Import Dashboard** + * **String format**: Human‑readable duration (e.g., `"10s"`, `"2h"`, `"1d"`). + * **Number format**: Duration in seconds (e.g., `60`, `3600`). -You can use the Upstash Grafana dashboard to visualize QStash metrics. + Defaults to `7d` (7 days). + -Open the import dialog and use: Upstash QStash Dashboard +## Response -![grafana-dashboard.png]() + + The data passed in when the event is triggered via `notify()`. + -## **Conclusion** + + `true` if execution resumed because the timeout elapsed, + `false` if resumed due to the event being received. + -You’ve integrated QStash with Prometheus. Use Grafana to explore message throughput, retries, DLQ, schedules, and Upstash Workflows. +## Usage -If you encounter issues, contact support. +```javascript highlight={6-11} +import { serve } from "@upstash/workflow/nextjs"; -# Resend -Source: https://upstash.com/docs/workflow/integrations/resend +export const { POST } = serve<{ topic: string }>(async (context) => { + const request = context.requestPayload; -The standard way to call a third-party endpoint in your workflow is by using [`context.call`](/docs/workflow/basics/context#context-call). + const { + eventData, + timeout, + } = await context.waitForEvent("wait for some event", "my-event-id", { + timeout: "1000s", // 1000 second timeout + }); +}); -However, if you need to call the Resend endpoint to send emails ([`/emails`](https://resend.com/docs/api-reference/emails/send-email) or [`/emails/batch`](https://resend.com/docs/api-reference/emails/send-batch-emails)), you can leverage the type-safe method `context.api.resend.call` method: +``` - - `context.api.resend.call` is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). You can use `context.call` instead to work with Resend. See our - [Roadmap](/docs/workflow/roadmap) for feature parity plans and - [Changelog](/docs/workflow/changelog) for updates. - +# context.waitForWebhook +Source: https://upstash.com/docs/workflow/steps/waitForWebhook - +`context.waitForWebhook()` pauses workflow execution until the webhook created by `createWebhook` is called or a timeout is reached. -```typescript Single Email -const { status, body } = await context.api.resend.call( - "Call Resend", - { - token: "", - body: { - from: "Acme ", - to: ["delivered@resend.dev"], - subject: "Hello World", - html: "

It works!

", - }, - headers: { - "content-type": "application/json", - }, - } -); -``` +You can call `context.waitForWebhook` with the same `webhook` object multiple times to wait for multiple calls to the same webhook URL. -```typescript Batch Email {4} -const { status, body } = await context.api.resend.call( - "Call Resend", - { - batch: true, - token: "", - body: [ - { - from: "Acme ", - to: ["delivered@resend.dev"], - subject: "Hello World", - html: "

It works!

", - }, - { - from: "Acme ", - to: ["delivered@resend.dev"], - subject: "Hello World", - html: "

It works!

", - }, - ], - headers: { - "content-type": "application/json", - }, - } -); -``` +## Arguments -
+ + Name of the step. + -The SDK provides predefined types for the body field in both the request parameters and the response, simplifying common use cases. If you need to customize these types, you can override them as shown below: + + The webhook object returned by `context.createWebhook()`. -```ts -type IsBatch = true; // Set to either true or false -type ResponseBodyType = { ... }; // Define your response body type -type RequestBodyType = { ... }; // Define your request body type + + + The webhook URL to wait for. + -const { status, body } = await context.api.resend.call< - IsBatch, - ResponseBodyType, - RequestBodyType ->( - "Call Resend", - { - ... - } -); -``` + + The internal event identifier. + + + -# llms.txt -Source: https://upstash.com/docs/workflow/llms-txt + + The maximum time to wait before continuing execution. -# Pricing -Source: https://upstash.com/docs/workflow/pricing + Should be passed in Human‑readable duration (e.g., `"10s"`, `"2h"`, `"1d"`). + -Upstash Workflow is based on QStash and uses a "pay-as-you-go" pricing model. You only incur costs when your app receives traffic, meaning there's no charge when it's not in use. Click [here](https://upstash.com/pricing/workflow) to view the pricing. +## Response -A workflow run consists of several QStash messages, with the total cost determined by the number of messages used. +The response varies depending on whether the webhook was called before the timeout: -You can track your current message usage and associated costs in the [Overview tab of the console](https://console.upstash.com/qstash?tab=details). + + * `false` if the webhook was called successfully + * `true` if execution resumed because the timeout elapsed + - + + The HTTP request object received by the webhook (only present when `timeout` is `false`). -For detailed pricing information based on different plans, visit our [Workflow pricing page](https://upstash.com/pricing/workflow). + Contains the full request details including method, headers, body, and URL. + -### Message Usage per Workflow Run +## Usage -* [context.run](/docs/workflow/basics/context#context-run), [context.sleep](/docs/workflow/basics/context#context-sleep), [context.sleepUntil](/docs/workflow/basics/context#context-sleepuntil), or [context.waitForEvent](/docs/workflow/basics/context#context-waitforevent) commands generate a single message. -* The [context.call](/docs/workflow/basics/context#context-call) command generates two messages. -* Each step in a [parallel run](/docs/workflow/howto/parallel-runs) costs 1 extra message. -* If the workflow endpoint or URL in [context.call](/docs/workflow/basics/context#context-call) returns an error or is unreachable, the workflow SDK will retry the call (up to 3 times by default). Each retry counts as a new message. +### Basic Example -- [Astro](https://upstash.com/docs/workflow/quickstarts/astro.md) -- [Cloudflare Workers](https://upstash.com/docs/workflow/quickstarts/cloudflare-workers.md) -- [Express.js](https://upstash.com/docs/workflow/quickstarts/express.md) -- [FastAPI](https://upstash.com/docs/workflow/quickstarts/fastapi.md) -- [Flask](https://upstash.com/docs/workflow/quickstarts/flask.md) -- [Hono](https://upstash.com/docs/workflow/quickstarts/hono.md) -- [Next.js & FastAPI](https://upstash.com/docs/workflow/quickstarts/nextjs-fastapi.md) -- [Next.js & Flask](https://upstash.com/docs/workflow/quickstarts/nextjs-flask.md) -- [Nuxt](https://upstash.com/docs/workflow/quickstarts/nuxt.md) -- [Supported Platforms](https://upstash.com/docs/workflow/quickstarts/platforms.md) -- [SolidJS](https://upstash.com/docs/workflow/quickstarts/solidjs.md) -- [SvelteKit](https://upstash.com/docs/workflow/quickstarts/svelte.md) -- [TanStack Start](https://upstash.com/docs/workflow/quickstarts/tanstack-start.md) -- [Next.js](https://upstash.com/docs/workflow/quickstarts/vercel-nextjs.md) +```typescript highlight={8-13} +import { serve } from "@upstash/workflow/nextjs"; -# Roadmap -Source: https://upstash.com/docs/workflow/roadmap +export const { POST } = serve(async (context) => { + // Create webhook + const webhook = await context.createWebhook("create webhook"); - - We have moved the roadmap and the changelog to [Github Discussions](https://github.com/orgs/upstash/discussions) starting from October 2025.Now you can follow `In Progress` features. You can see that your `Feature Requests` are recorded. You can vote for them and comment your specific use-cases to shape the feature to your needs. - + // Wait for webhook to be called with 30 second timeout + const webhookResponse = await context.waitForWebhook( + "wait for webhook", + webhook, + "30s" + ); -# JavaScript SDK -Source: https://upstash.com/docs/workflow/sdk/workflow-js + if (webhookResponse.timeout) { + console.log("Webhook was not called within the timeout period"); + } else { + console.log("Webhook was called successfully"); + console.log("Request body:", webhookResponse.request.body); + console.log("Request headers:", webhookResponse.request.headers); + } +}); +``` -# Python SDK -Source: https://upstash.com/docs/workflow/sdk/workflow-py +For more complete examples and use cases, see [the page on webhooks](/docs/workflow/features/webhooks). # General Source: https://upstash.com/docs/workflow/troubleshooting/general diff --git a/llms.txt b/llms.txt index 5682eb236..2422567c8 100644 --- a/llms.txt +++ b/llms.txt @@ -992,18 +992,6 @@ - [client.notify](https://upstash.com/docs/workflow/basics/client/notify.md) - [client.trigger](https://upstash.com/docs/workflow/basics/client/trigger.md) - [client.getWaiters](https://upstash.com/docs/workflow/basics/client/waiters.md) -- [Overview](https://upstash.com/docs/workflow/basics/context.md) -- [context.api](https://upstash.com/docs/workflow/basics/context/api.md) -- [context.call](https://upstash.com/docs/workflow/basics/context/call.md) -- [context.cancel](https://upstash.com/docs/workflow/basics/context/cancel.md) -- [context.createWebhook](https://upstash.com/docs/workflow/basics/context/createWebhook.md) -- [context.invoke](https://upstash.com/docs/workflow/basics/context/invoke.md) -- [context.notify](https://upstash.com/docs/workflow/basics/context/notify.md) -- [context.run](https://upstash.com/docs/workflow/basics/context/run.md) -- [context.sleep](https://upstash.com/docs/workflow/basics/context/sleep.md) -- [context.sleepUntil](https://upstash.com/docs/workflow/basics/context/sleepUntil.md) -- [context.waitForEvent](https://upstash.com/docs/workflow/basics/context/waitForEvent.md) -- [context.waitForWebhook](https://upstash.com/docs/workflow/basics/context/waitForWebhook.md) - [How Workflow Works](https://upstash.com/docs/workflow/basics/how.md) - [Overview](https://upstash.com/docs/workflow/basics/serve.md) - [Advanced Options](https://upstash.com/docs/workflow/basics/serve/advanced.md) @@ -1039,7 +1027,7 @@ - [Wait](https://upstash.com/docs/workflow/features/wait.md) - [Overview](https://upstash.com/docs/workflow/features/wait-for-event.md) - [Webhooks](https://upstash.com/docs/workflow/features/webhooks.md) -- [Getting Started](https://upstash.com/docs/workflow/getstarted.md) +- [Overview](https://upstash.com/docs/workflow/getstarted.md) - [Cancel a Run](https://upstash.com/docs/workflow/howto/cancel.md) - [Update a Workflow](https://upstash.com/docs/workflow/howto/changes.md) - [Configure a Run](https://upstash.com/docs/workflow/howto/configure.md) @@ -1083,6 +1071,18 @@ - [Roadmap](https://upstash.com/docs/workflow/roadmap.md) - [JavaScript SDK](https://upstash.com/docs/workflow/sdk/workflow-js.md) - [Python SDK](https://upstash.com/docs/workflow/sdk/workflow-py.md) +- [Overview](https://upstash.com/docs/workflow/steps.md) +- [context.api](https://upstash.com/docs/workflow/steps/api.md) +- [context.call](https://upstash.com/docs/workflow/steps/call.md) +- [context.cancel](https://upstash.com/docs/workflow/steps/cancel.md) +- [context.createWebhook](https://upstash.com/docs/workflow/steps/createWebhook.md) +- [context.invoke](https://upstash.com/docs/workflow/steps/invoke.md) +- [context.notify](https://upstash.com/docs/workflow/steps/notify.md) +- [context.run](https://upstash.com/docs/workflow/steps/run.md) +- [context.sleep](https://upstash.com/docs/workflow/steps/sleep.md) +- [context.sleepUntil](https://upstash.com/docs/workflow/steps/sleepUntil.md) +- [context.waitForEvent](https://upstash.com/docs/workflow/steps/waitForEvent.md) +- [context.waitForWebhook](https://upstash.com/docs/workflow/steps/waitForWebhook.md) - [General](https://upstash.com/docs/workflow/troubleshooting/general.md) - [Vercel](https://upstash.com/docs/workflow/troubleshooting/vercel.md) diff --git a/workflow/getstarted.mdx b/workflow/getstarted.mdx index 02389bbf1..3e3b43850 100644 --- a/workflow/getstarted.mdx +++ b/workflow/getstarted.mdx @@ -1,9 +1,7 @@ --- -title: "Getting Started" +title: "Overview" --- -## Overview - Upstash Workflow lets you write **durable, reliable and performant serverless functions**. Get delivery guarantees, automatic retries on failure, scheduling and more without managing any infrastructure.