From fff7e3817a1456e502c8b44cd9d6f3889ac75499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Tokg=C3=B6z?= Date: Wed, 3 Jun 2026 23:28:37 +0300 Subject: [PATCH 1/5] Refactor the Workflow docs navigation. --- docs.json | 279 +++++++++--------- workflow/{basics/context.mdx => steps.mdx} | 0 workflow/{basics/context => steps}/api.mdx | 0 workflow/{basics/context => steps}/call.mdx | 0 workflow/{basics/context => steps}/cancel.mdx | 0 .../context => steps}/createWebhook.mdx | 0 workflow/{basics/context => steps}/invoke.mdx | 0 workflow/{basics/context => steps}/notify.mdx | 0 workflow/{basics/context => steps}/run.mdx | 0 workflow/{basics/context => steps}/sleep.mdx | 0 .../{basics/context => steps}/sleepUntil.mdx | 0 .../context => steps}/waitForEvent.mdx | 0 .../context => steps}/waitForWebhook.mdx | 0 13 files changed, 146 insertions(+), 133 deletions(-) rename workflow/{basics/context.mdx => steps.mdx} (100%) rename workflow/{basics/context => steps}/api.mdx (100%) rename workflow/{basics/context => steps}/call.mdx (100%) rename workflow/{basics/context => steps}/cancel.mdx (100%) rename workflow/{basics/context => steps}/createWebhook.mdx (100%) rename workflow/{basics/context => steps}/invoke.mdx (100%) rename workflow/{basics/context => steps}/notify.mdx (100%) rename workflow/{basics/context => steps}/run.mdx (100%) rename workflow/{basics/context => steps}/sleep.mdx (100%) rename workflow/{basics/context => steps}/sleepUntil.mdx (100%) rename workflow/{basics/context => steps}/waitForEvent.mdx (100%) rename workflow/{basics/context => steps}/waitForWebhook.mdx (100%) diff --git a/docs.json b/docs.json index f9348742b..b3ecb5d62 100644 --- a/docs.json +++ b/docs.json @@ -1252,7 +1252,7 @@ "tab": "Workflow", "groups": [ { - "group": "Workflow", + "group": "Getting Started", "pages": [ "workflow/getstarted", { @@ -1284,120 +1284,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" ] } ] @@ -1405,13 +1351,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", @@ -1431,25 +1370,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": { @@ -1457,17 +1457,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" ] }, { @@ -1892,6 +1897,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/workflow/basics/context.mdx b/workflow/steps.mdx similarity index 100% rename from workflow/basics/context.mdx rename to workflow/steps.mdx diff --git a/workflow/basics/context/api.mdx b/workflow/steps/api.mdx similarity index 100% rename from workflow/basics/context/api.mdx rename to workflow/steps/api.mdx diff --git a/workflow/basics/context/call.mdx b/workflow/steps/call.mdx similarity index 100% rename from workflow/basics/context/call.mdx rename to workflow/steps/call.mdx diff --git a/workflow/basics/context/cancel.mdx b/workflow/steps/cancel.mdx similarity index 100% rename from workflow/basics/context/cancel.mdx rename to workflow/steps/cancel.mdx diff --git a/workflow/basics/context/createWebhook.mdx b/workflow/steps/createWebhook.mdx similarity index 100% rename from workflow/basics/context/createWebhook.mdx rename to workflow/steps/createWebhook.mdx diff --git a/workflow/basics/context/invoke.mdx b/workflow/steps/invoke.mdx similarity index 100% rename from workflow/basics/context/invoke.mdx rename to workflow/steps/invoke.mdx diff --git a/workflow/basics/context/notify.mdx b/workflow/steps/notify.mdx similarity index 100% rename from workflow/basics/context/notify.mdx rename to workflow/steps/notify.mdx diff --git a/workflow/basics/context/run.mdx b/workflow/steps/run.mdx similarity index 100% rename from workflow/basics/context/run.mdx rename to workflow/steps/run.mdx diff --git a/workflow/basics/context/sleep.mdx b/workflow/steps/sleep.mdx similarity index 100% rename from workflow/basics/context/sleep.mdx rename to workflow/steps/sleep.mdx diff --git a/workflow/basics/context/sleepUntil.mdx b/workflow/steps/sleepUntil.mdx similarity index 100% rename from workflow/basics/context/sleepUntil.mdx rename to workflow/steps/sleepUntil.mdx diff --git a/workflow/basics/context/waitForEvent.mdx b/workflow/steps/waitForEvent.mdx similarity index 100% rename from workflow/basics/context/waitForEvent.mdx rename to workflow/steps/waitForEvent.mdx diff --git a/workflow/basics/context/waitForWebhook.mdx b/workflow/steps/waitForWebhook.mdx similarity index 100% rename from workflow/basics/context/waitForWebhook.mdx rename to workflow/steps/waitForWebhook.mdx From 74544da4caa6652933bb8558fa36a02f12c1a0c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:29:38 +0000 Subject: [PATCH 2/5] chore(llms): regenerate llms.txt and llms-full.txt --- llms-full.txt | 10324 ++++++++++++++++++++++++------------------------ llms.txt | 24 +- 2 files changed, 5174 insertions(+), 5174 deletions(-) diff --git a/llms-full.txt b/llms-full.txt index 30bb92cd5..f351917ba 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -47891,5922 +47891,5947 @@ 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 label of the current workflow run, if set in [client.trigger](/docs/workflow/basics/client/trigger). - +* **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. -## Context Object Functions +* **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. -You can use the functions exposed by context object to define workflow steps. +* **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. -* [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) +*** -# context.api -Source: https://upstash.com/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. -In addition to `context.call`, you can also make third‑party requests using the `context.api` namespace. +It's quick and easy to get started: follow the [Quickstarts](/docs/workflow/quickstarts/platforms) to define your first workflow in minutes. -This namespace provides built‑in integrations for **OpenAI**, **Anthropic**, and **Resend**, allowing you to make requests in a **type‑safe** manner. +# Overview +Source: https://upstash.com/docs/workflow/basics/serve + +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](http://localhost:3000/workflow/howto/local-development)) - * 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). - + ```typescript TypeScript + type InitialPayload = { + foo: string; + bar: number; + }; -## Usage + // 👇 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; + }, + } + ); + ``` -```typescript highlight={4} -import { serve } from "@upstash/workflow/nextjs"; + ```python Python + @dataclass + class InitialPayload: + foo: str + bar: int -export const { POST } = serve(async (context) => { - const webhook = await context.createWebhook("create webhook"); + def initial_payload_parser(initial_payload: str) -> InitialPayload: + return parse_payload(initial_payload) - console.log(webhook.webhookUrl); // Use this URL with external services -}); -``` + @serve.post("/api/example", initial_payload_parser=initial_payload_parser) + async def example(context: AsyncWorkflowContext[InitialPayload]) -> None: + payload: InitialPayload = context.request_payload -For more complete examples and use cases, see [the page on webhooks](/docs/workflow/features/webhooks). + ``` -# 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**. + + Alternative to `initialPayloadParser`, you can pass a `schema` in the TypeScript SDK. - - 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). - + The schema is used to validate and parse the initial request payload automatically using [Zod](https://zod.dev/). -## Arguments + - - The workflow definition to invoke. - Must be a workflow exposed under the same `serveMany`. - + ```typescript TypeScript - - The payload to send to the invoked workflow. - This value will be set as `context.requestPayload` in the invoked workflow. - + import { z } from "zod"; - - Optional HTTP headers to forward to the invoked workflow. - This value will be set as `context.headers` in the invoked workflow. - + const parameters = z.object({ expression: z.string() }); - - Override the workflow run ID for the invoked workflow. - Defaults to a new ID if not specified. - + export const { POST } = serve( + async (context) => { + // context.requestPayload is typed as `{ expression: string }` + const payload = context.requestPayload; + }, + { + schema: parameters, + } + ); + ``` - - Number of retry attempts configuration of the invoked workflow. - Defaults to `3`. Retries use exponential backoff. + - - Delay between retries of the invoked workflow. + + Specifies the full endpoint URL of the workflow, including the route path. + + 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. + + Use this option when running behind a proxy, reverse proxy, or local tunnel during development where `request.url` cannot be used directly. + + + + ```typescript TypeScript + export const { POST } = serve( + async (context) => { ... }, + { + url: "https://.com/api/workflow" + } + ); + ``` + + ```python Python + @serve.post("/api/example", url="https://.com/api/workflow") + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` + - - Flow control configuration of the invoked workflow. + - See [Flow Control](/docs/workflow/features/flow-control) for details. + 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. - - - A logical grouping key that identifies which requests share the same flow control limits. - + + 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. + - - The maximum number of allowed requests per second. - + - - The maximum number of concurrent requests allowed. - + ```typescript TypeScript + export const { POST } = serve( + async (context) => { + ... + }, + // options: + { + baseUrl: "" + } + ); + ``` - - The time window used to enforce the defined rate limit. Default is `1s`. - - - + ```python Python + @serve.post("/api/example", base_url="") + async def example(context: AsyncWorkflowContext[str]) -> None: ... -## Response + ``` - - The response body returned by the invoked workflow. - + + - - `true` if the invoked workflow completed with failure. - + - - `true` if the invoked workflow was canceled before completion. - + Use `qstashClient` if you want to provide your own QStash client instead of letting Workflow use the default from environment variables. -## Usage + This is useful if you're working with multiple QStash projects in the same app. -```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) - } -); -``` + -# context.notify -Source: https://upstash.com/docs/workflow/basics/context/notify + ```typescript TypeScript + import { Client } from "@upstash/qstash"; + import { serve } from "@upstash/workflow/nextjs"; -`context.notify()` notifies workflows that are waiting for a specific event, passing along an optional payload. + export const { POST } = serve( + async (context) => { ... }, + { + qstashClient: new Client({ token: "" }) + } + ); + ``` -It is typically used in combination with [`context.waitForEvent`](/docs/workflow/basics/context#context-waitforevent). + ```python Python + from qstash import AsyncQStash -## Arguments + @serve.post("/api/example", qstash_client=AsyncQStash(os.environ["QSTASH_TOKEN"])) + async def example(context: AsyncWorkflowContext[str]) -> None: ... - - A unique identifier for the step. - + ``` - - 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. - - 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`. + - This solves race conditions where notifications might be sent before a workflow reaches its wait step. - + The `Receiver` verifies that every request to your endpoint actually comes from QStash, blocking anyone else from triggering your workflow. -## Response + The `receiver` option allows you to pass a QStash Receiver explicitly. -`context.notify()` returns a list of waiters describing the workflows that were notified. + By default, Workflow initializes the Receiver automatically using the environment variables `QSTASH_CURRENT_SIGNING_KEY` and `QSTASH_NEXT_SIGNING_KEY`. - - A list of `NotifyResponse` objects describing each workflow that was waiting on the event. + This is useful if you're working with multiple QStash projects in the same app. - - - The ID of the notification message delivered to the workflow. - This is unique to every notification. - + - - The unique identifier of the workflow run that was notified. - + ```typescript TypeScript + import { Receiver } from "@upstash/qstash"; + import { serve } from "@upstash/workflow/nextjs"; - - Unix timestamp (in milliseconds) representing when the workflow was created. - + export const { POST } = serve( + async (context) => { ... }, + { + receiver: new Receiver({ + currentSigningKey: "", + nextSigningKey: "", + }) + } + ); + ``` - - - - 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 - - - - - - + ```python Python + from qstash import Receiver -## Usage + @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: + ... + ``` -### Basic Notification + -```javascript -import { serve } from "@upstash/workflow/nextjs"; + -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); -}); -``` +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`. -### Notification with Lookback +Inside your workflow, these values are also exposed on `context.env`. -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`: + -```javascript +```typescript TypeScript +import { Receiver } from "@upstash/qstash"; import { serve } from "@upstash/workflow/nextjs"; -export const { POST } = serve<{ orderId: string }>(async (context) => { - const { orderId } = context.requestPayload; - - // Process payment - await context.run("process-payment", async () => { - return processPayment(orderId); - }); - - // 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`):** + * 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. + - // 👇 Create a Proper Instance with Object.assign() - user = Object.assign(new User(), user); + +* **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. + - await context.run("greet", async () => { - // 👇 Now instance methods are available as well - console.log(user.greet()); - }); - } - ); - ``` - + +* **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 -Source: https://upstash.com/docs/workflow/basics/context/sleep + +* **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). + -`context.sleep()` pauses workflow execution for a specified duration. + +* **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. + -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 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. + -Always `await` a `sleep` step to properly pause execution. + +* **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. + -## Arguments + +* **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). + - - A unique identifier for the step. - + +* **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). + - - The duration to pause workflow execution. + +* **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. + - * **Human-readable string format:** + +* **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). - | 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 | +* **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). + - * **Numeric format (seconds):** + +* **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. + - | Input | Duration | - |---------|---------------| - | `60` | 60 seconds (1 minute) | - | `3600` | 3600 seconds (1 hour) | - | `86400` | 86400 seconds (1 day) | - + +* 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. + -## Usage + +* Bug fixes and internal logging improvements. + - + +* Released [Upstash Workflow](/docs/workflow/getstarted). + -```typescript TypeScript highlight={12-13} -import { serve } from "@upstash/workflow/nextjs"; -import { signIn, sendEmail } from "@/utils/onboarding-utils"; +- [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) -export const { POST } = serve(async (context) => { - const userData = context.requestPayload; +# Overview +Source: https://upstash.com/docs/workflow/features/dlq - const user = await context.run("sign-in", async () => { - const signedInUser = await signIn(userData); - return signedInUser; - }); +The Dead Letter Queue (DLQ) automatically captures failed workflow runs that have exhausted all retry attempts. - // 👇 Wait for one day (in seconds) - await context.sleep("wait-until-welcome-email", "1d"); +This ensures that no workflow execution is lost and provides multiple options for recovering from failures gracefully. - await context.run("send-welcome-email", async () => { - return sendEmail(user.name, user.email); - }); -}); -``` +## How it works? -```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 +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. -app = FastAPI() -serve = Serve(app) + -@serve.post("/api/onboarding") -async def onboarding(context: AsyncWorkflowContext[User]) -> None: - user_data = context.request_payload +The DLQ serves as a safety net, preserving failed workflow runs with their complete execution context. - async def _sign_in(): - return await sign_in(user_data) + + 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 - user = await context.run("sign-in", _sign_in) + After the retention duration expires, DLQ items are automatically removed and cannot be recovered. + - # 👇 Wait for one day (in seconds) - await context.sleep("wait-until-welcome-email", "1d") +## Recovery Actions - async def _send_email(): - return await send_email(user.name, user.email) +Once a workflow run is in the DLQ, you can take the following actions: - await context.run("send-welcome-email", _send_email) +* **[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. - +# Rerun Failure Function +Source: https://upstash.com/docs/workflow/features/dlq/callback -# context.sleepUntil -Source: https://upstash.com/docs/workflow/basics/context/sleepUntil +The **Rerun Failure Function** action allows you to retry the failure function that executes when a workflow run enters the Dead Letter Queue (DLQ). -`context.sleepUntil()` pauses workflow execution until a specific timestamp. +The failure function is typically a cleanup or notification operation that runs automatically whenever a workflow is moved to the 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. +This feature is particularly helpful for: -Always await a `sleepUntil` step to properly pause execution. +* 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. -## Arguments +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. - - A unique identifier for the step. - + - - 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: +* 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. + + + +You can perform this action programmatically as well: + + + ```typescript TypeScript + import { Client } from "@upstash/workflow"; + + const client = new Client({ token: "" }); + + await client.dlq.restart({ + dlqId: "dlq-12345", + retries: 3, + }); + ``` -# context.waitForEvent -Source: https://upstash.com/docs/workflow/basics/context/waitForEvent +# Resume +Source: https://upstash.com/docs/workflow/features/dlq/resume -`context.waitForEvent` pauses workflow execution until a given event occurs or a timeout is reached. +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. -Default timeout value is 7 days. +This approach is ideal when: -## Arguments +* 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. - - A unique identifier for the step. - + - - A unique identifier for the event to wait on. - +You can perform this action programmatically as well: - - The maximum time to wait before continuing execution. + + ```typescript TypeScript + import { Client } from "@upstash/workflow"; - * **String format**: Human‑readable duration (e.g., `"10s"`, `"2h"`, `"1d"`). - * **Number format**: Duration in seconds (e.g., `60`, `3600`). + const client = new Client({ token: "" }); - Defaults to `7d` (7 days). - + await client.dlq.resume({ + dlqId: "dlq-12345", + retries: 3, + }); + ``` + -## Response + + 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. - - The data passed in when the event is triggered via `notify()`. - + For more details, check out the [Handle workflow route code changes](/docs/workflow/howto/changes) page. + - - `true` if execution resumed because the timeout elapsed, - `false` if resumed due to the event being received. - +# Overview +Source: https://upstash.com/docs/workflow/features/failure-callback -## Usage +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. -```javascript highlight={6-11} +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). + + + +The failure function automatically receives the workflow run context and the reason for the failure, so you can decide how to handle it. + + +```typescript TypeScript import { serve } from "@upstash/workflow/nextjs"; -export const { POST } = serve<{ topic: string }>(async (context) => { - const request = context.requestPayload; +export const { POST } = serve( + async (context) => { + // Your workflow logic... + }, + { + failureFunction: async ({ + context, + failStatus, + failResponse, + failHeaders, + }) => { - const { - eventData, - timeout, - } = await context.waitForEvent("wait for some event", "my-event-id", { - timeout: "1000s", // 1000 second timeout - }); -}); + // 👇 Log error to monitoring system + await logToSentry(...); + + // 👇 Send alert to team + await sendSlackAlert(...); + // 👇 Perform cleanup operations + await cleanupWorkflowResources(...); + }, + } ``` +); -# context.waitForWebhook -Source: https://upstash.com/docs/workflow/basics/context/waitForWebhook + -`context.waitForWebhook()` pauses workflow execution until the webhook created by `createWebhook` is called or a timeout is reached. +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. -You can call `context.waitForWebhook` with the same `webhook` object multiple times to wait for multiple calls to the same webhook URL. + + 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. -## Arguments + Read more here: [securing your workflow endpoint](/docs/workflow/howto/security). + - - Name of the step. - +## Parameters - - The webhook object returned by `context.createWebhook()`. +The `failureFunction` receives an object with the following parameters: - - - The webhook URL to wait for. + + The workflow context object containing: + + + + The ID of the failed workflow run - - The internal event identifier. + + The publicly accessible workflow endpoint URL + + + + The original request payload that triggered the workflow + + + + The original request headers + + + + Environment variables - - The maximum time to wait before continuing execution. + + The HTTP status code returned by the failed workflow step. + - Should be passed in Human‑readable duration (e.g., `"10s"`, `"2h"`, `"1d"`). + + The response body returned by the failed workflow step. -## Response + + The response headers returned by the failed workflow step. + -The response varies depending on whether the webhook was called before the timeout: +# Advanced failureUrl Option +Source: https://upstash.com/docs/workflow/features/failureFunction/advanced - - * `false` if the webhook was called successfully - * `true` if execution resumed because the timeout elapsed - +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. - - The HTTP request object received by the webhook (only present when `timeout` is `false`). +You can use either `failureFunction` or `failureUrl`, but not both. These options are mutually exclusive. - Contains the full request details including method, headers, body, and URL. - +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. -## Usage +If you think this advanced option fits your need, you can configure it by passing `failureUrl` configuration. -### Basic Example + + ```typescript + import { Client } from "@upstash/workflow"; -```typescript highlight={8-13} -import { serve } from "@upstash/workflow/nextjs"; + const client = new Client({ token: "" }) -export const { POST } = serve(async (context) => { - // Create webhook - const webhook = await context.createWebhook("create webhook"); + const { workflowRunId } = await client.trigger({ + url: "https:///workflow" + failureUrl: "https:///workflow-failure" + }) + ``` - // Wait for webhook to be called with 30 second timeout - const webhookResponse = await context.waitForWebhook( - "wait for webhook", - webhook, - "30s" - ); + ```python Python + @serve.post("/api/example", failure_url="https:///workflow-failure") + async def example(context: AsyncWorkflowContext[str]) -> None: + # Your workflow logic... + pass + ``` + - 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); - } -}); -``` +# Reliability of Failure Function +Source: https://upstash.com/docs/workflow/features/failureFunction/reliability -For more complete examples and use cases, see [the page on webhooks](/docs/workflow/features/webhooks). +The failure function is executed whenever a workflow run fails. -# How Workflow Works -Source: https://upstash.com/docs/workflow/basics/how +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 is an orchestration layer that allows you to write **multi‑step workflows** which are: +You can view and filter workflow runs with failed failure function executions in the DLQ dashboard. -* **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 + -Upstash Workflow is built on top of Upstash QStash, our serverless messaging and scheduling solution, to achieve these features. +From the DLQ dashboard, you can retry the failure function. -## The Core Idea + -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. +You can perform this action programmatically as well: -These approaches can work, but they often fail to handle production load reliably or become increasingly difficult to maintain over time: +```ts +import { Client } from "@upstash/workflow"; -* **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. +const client = new Client({ token: "" }); -*** +const response = await client.dlq.retryFailureFunction({ + dlqId: "dlq-12345" // The ID of the DLQ message to retry +}); +``` -## How Upstash Workflow Solves This +# Overview +Source: https://upstash.com/docs/workflow/features/flow-control -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. +Flow Control allows you to limit how many workflow steps are executed by delaying and queuing their delivery. -* 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. +This feature helps to: +* Manage resource consumption +* Prevent violations of external API rate limits +* Ensure workflows run within defined system constraints -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.** +## How Flow Control Works - +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. -*** +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. -## Extended Features +There are two main parameters to configure: -Upstash Workflow extends the basic step model with additional primitives: +* [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 -* **Parallel Steps** - Define multiple steps (e.g. inside a `Promise.all()`). The engine detects independent work and runs steps concurrently as separate HTTP executions. +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. -* **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. +## Example -* **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. +Suppose you have the following workflow: -* **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. +```typescript +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload -*** + await context.run("step-1", () => { ... }); -This architecture makes your serverless functions durable, reliable, and performance‑optimized, even in the face of runtime errors or temporary service outages. + await context.run("step-2", () => { ... }); -It's quick and easy to get started: follow the [Quickstarts](/docs/workflow/quickstarts/platforms) to define your first workflow in minutes. + await context.run("step-3", () => { ... }); +}) +``` -# Overview -Source: https://upstash.com/docs/workflow/basics/serve +Now imagine you trigger **N workflow runs** for this workflow with the following configuration: -Use the `serve()` function to define an endpoint that runs a workflow. -It accepts two arguments: +```typescript +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "fw_example", + parallelism: 7, + rate: 3, + period: "1m", + } +}) +``` -1. **Route Function**: an async function that receives the workflow context and defines the workflow steps. -2. **Options**: configuration options for the workflow. +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. - - ```typescript TypeScript - import { serve } from "@upstash/workflow/nextjs"; + - export const { POST } = serve(async (context) => { - // Route function - }, { - // Options - }); - ``` +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. - ```python Python - from fastapi import FastAPI - from upstash_workflow.fastapi import Serve - from upstash_workflow import AsyncWorkflowContext +Steps that exceed these limits are automatically queued and executed later. - 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" +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. - result = await context.run("step-1", _step1) +Parallelism slots are consumed by running steps. +If no slots are available, new steps enter the **waitlist** until resources free up: - async def _step2() -> None: - # define another piece of business logic as step 2 - pass + - await context.run("step-2", _step2) - ``` - + +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. -## Route Function +The context.call and context.invoke steps are exception this to this rule and accept their own flow control configuration: -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. +* [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. -The context object provides: -* **Workflow APIs** – functions for defining workflow steps. -* **Workflow Run Properties** – request payload, request headers, and other metadata. +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. + -For a full list of available APIs and properties, see the [Workflow Context](/docs/workflow/basics/context) documentation. +## Configuration - - ```typescript TypeScript highlight={4-9} - import { serve } from "@upstash/workflow/nextjs"; +You can configure flow control when starting a workflow run: - export const { POST } = serve( - async (context) => { - // 👇 Access context properties - const { userId } = context.requestPayload; - // 👇 Define a workflow step - await context.run("step-1", async () => {}) - } - ); - ``` +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; - ```python Python - from fastapi import FastAPI - from upstash_workflow.fastapi import Serve - from upstash_workflow import AsyncWorkflowContext +const client = new Client({ token: "" }) - app = FastAPI() - serve = Serve(app) +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "user-signup", + parallelism: 1, + rate: 10, + period: 100, + } +}) +``` - @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" +All steps within a workflow run will adhere to the specified flow control configuration. - result = await context.run("step-1", _step1) + +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. - async def _step2() -> None: - # define another piece of business logic as step 2 - pass +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. + - await context.run("step-2", _step2) +# Monitor & Manage +Source: https://upstash.com/docs/workflow/features/flow-control/monitor - ``` - +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. -## 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. +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). - +## Manage - ```typescript TypeScript highlight={5-8} - import { serve } from "@upstash/workflow/nextjs"; +You can manage flow control keys by pausing/resuming delivery, pinning/unpinning configurations, and resetting rate counts. - export const { POST } = serve( - async (context) => { ... }, - // 👇 Workflow options - { - failureFunction: async ({ ... }) => {} - } - ); - ``` +These operations are available via the SDK, REST API, or directly from the **Flow Control** tab in the [Upstash Console](https://console.upstash.com). - ```python Python - from fastapi import FastAPI - from upstash_workflow.fastapi import Serve - from upstash_workflow import AsyncWorkflowContext +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. - app = FastAPI() - serve = Serve(app) +# Parallelism +Source: https://upstash.com/docs/workflow/features/flow-control/parallelism - @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 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. - result = await context.run("step-1", _step1) +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; - async def _step2() -> None: - # define another piece of business logic as step 2 - pass +const client = new Client({ token: "" }) - await context.run("step-2", _step2) +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "user-signup", + parallelism: 10, + } +}) +``` - ``` - +**Example**: +If `parallelism = 3`, at most 3 requests can run concurrently. -# Advanced Options -Source: https://upstash.com/docs/workflow/basics/serve/advanced +When tokens are available, requests acquire one and start execution: + -Advanced Options are intended to support edge cases or testing pipelines and are **not required for regular use**. +When all tokens are in use, additional requests are not failed — they’re queued in a **waitlist**: + - - Defines a function that executes if the workflow fails after all retries are exhausted. +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 [failureFunction](/docs/workflow/features/failure-callback). + + Token handoff does not guarantee strict ordering. + A later request in the waitlist may acquire a token before an earlier one. + - - ```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 - } - } - ); - ``` + - ```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 +# Rate and Period +Source: https://upstash.com/docs/workflow/features/flow-control/rate-period - @serve.post("/api/example", failure_function=failure_function) - async def example(context: AsyncWorkflowContext[str]) -> None: ... - ``` +The rate specifies the maximum number of requests allowed in a given period (time window). - - +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; - +const client = new Client({ token: "" }) - - This parameter is only available in Python SDK. In Javascript SDK, you can pass this value when triggering the workflow. - +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "user-signup", + rate: 10, + period: 100, + } +}) +``` - The `failureUrl` option defines an external endpoint that will be called if the workflow fails after all retries are exhausted. +**Example**: +If `rate = 2` and `period = 1 minute`, then **a maximum of 2 steps** can be executed per minute. - This option is an advanced alternative to `failureFunction`. - For more details, see [Advanced failureUrl Option](/docs/workflow/features/failureFunction/advanced). +The first 2 requests within the minute are executed immediately: - + - ```python Python - @serve.post("/api/example", failureUrl="https:///...") - async def example(context: AsyncWorkflowContext[str]) -> None: ... - ``` +The 3rd request in the same minute is not executed immediately: - - + - +Instead of rejecting it, Workflow schedules the request in the next available time window: - - This parameter is only available in Python SDK. In Javascript SDK, you can pass this value when triggering the workflow. - + - Defines the number of retry attempts if a workflow step fails. - The default value is 3. +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. - For details, see [retry configuration](/docs/workflow/features/retries#configuration). +# Overview +Source: https://upstash.com/docs/workflow/features/invoke - +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. - ```python Python - @serve.post("/api/example", retries=3) - async def example(context: AsyncWorkflowContext[str]) -> None: ... - ``` - - +When you use `context.invoke`, invoking workflow will wait until the invoked workflow finishes before running the next step. - - An array of middleware instances that intercept workflow lifecycle and debug events. +```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) + } +) +``` - Middlewares allow you to hook into various stages of workflow execution (before/after steps, run start/completion) - and debug events (errors, warnings, info logs). +You can return a response from a workflow, which will be delivered to invoker workflow run. - For details and examples, see [Middlewares](/docs/workflow/howto/middlewares). + - + + 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. + - ```typescript TypeScript - import { serve } from "@upstash/workflow/nextjs"; - import { loggingMiddleware } from "@upstash/workflow"; +# Using Serve Many +Source: https://upstash.com/docs/workflow/features/invoke/serveMany - export const { POST } = serve( - async (context) => { ... }, - { - middlewares: [loggingMiddleware] - } - ); - ``` +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 - - Enables custom parsing of the initial request payload. +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. - 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. + + + Use `createWorkflow()` to define workflows as objects. - + 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 TypeScript - type InitialPayload = { - foo: string; - bar: number; - }; + ```typescript + const workflowOne = createWorkflow( + // 👇 Request Payload Type + async (context: WorkflowContext) => { - // 👇 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; - }, + await context.sleep("wait 1 second", 1) + + // 👇 Workflow Response Type + return { message: "This is the data returned by the workflow" }; } ); - ``` - - ```python Python - @dataclass - class InitialPayload: - foo: str - bar: int - def initial_payload_parser(initial_payload: str) -> InitialPayload: - return parse_payload(initial_payload) + 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. - @serve.post("/api/example", initial_payload_parser=initial_payload_parser) - async def example(context: AsyncWorkflowContext[InitialPayload]) -> None: - payload: InitialPayload = context.request_payload + 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. + ```typescript app/serve-many/[...any]/route.ts + export const { POST } = serveMany( + { + "workflow-one-route": workflowOne, + "workflow-two-route": workflowTwo, + } + ) ``` - + + 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. - - Alternative to `initialPayloadParser`, you can pass a `schema` in the TypeScript SDK. + ```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" + } + ), + }); + ``` - The schema is used to validate and parse the initial request payload automatically using [Zod](https://zod.dev/). + - + + In this example, both `workflowOne` and `workflowTwo` are exposed through `serveMany`, sharing the same parent path. - ```typescript TypeScript + You can start `workflowOne` by sending a trigger request to: + `https://your-app/serve-many/workflow-one-route`. - import { z } from "zod"; + ```typescript + import { Client } from "@upstash/workflow"; - const parameters = z.object({ expression: z.string() }); + const client = new Client({ token: "" }) - export const { POST } = serve( - async (context) => { - // context.requestPayload is typed as `{ expression: string }` - const payload = context.requestPayload; - }, - { - schema: parameters, - } - ); + const { workflowRunId } = await client.trigger({ + // 👇 URL of workflow one + url: "https://your-app/serve-many/workflow-one-route" + }) ``` - - - - - Specifies the full endpoint URL of the workflow, including the route path. + 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`. + + - 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. +# Notify +Source: https://upstash.com/docs/workflow/features/notify - Use this option when running behind a proxy, reverse proxy, or local tunnel during development where `request.url` cannot be used directly. +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 - ```typescript TypeScript - export const { POST } = serve( - async (context) => { ... }, - { - url: "https://.com/api/workflow" - } - ); - ``` +Notifies other workflows waiting for a specific event from within a workflow. - ```python Python - @serve.post("/api/example", url="https://.com/api/workflow") - async def example(context: AsyncWorkflowContext[str]) -> None: ... - ``` - - + +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; - +export const { POST } = serve(async (context) => { + const { orderId, processingResult } = context.requestPayload; - 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. + await context.run("process-order", async () => { + // ... + }) - - 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. - + const { notifyResponse } = await context.notify( + "notify-processing-complete", + `order-${orderId}`, + { + orderId, + status: "completed", + result: processingResult, + completedAt: new Date().toISOString() + } + ); - +}); +``` - ```typescript TypeScript - export const { POST } = serve( - async (context) => { - ... - }, - // options: - { - baseUrl: "" - } - ); - ``` +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext +from datetime import datetime - ```python Python - @serve.post("/api/example", base_url="") - async def example(context: AsyncWorkflowContext[str]) -> None: ... +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) - Use `qstashClient` if you want to provide your own QStash client instead of letting Workflow use the default from environment variables. + # 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() + } + ) - This is useful if you're working with multiple QStash projects in the same app. + # Log notification results + async def _log_notification(): + print(f"Notified {len(notify_response)} waiting workflows") + return notify_response - + await context.run("log-notification", _log_notification) +``` + - ```typescript TypeScript - import { Client } from "@upstash/qstash"; - import { serve } from "@upstash/workflow/nextjs"; +## External Notification - export const { POST } = serve( - async (context) => { ... }, - { - qstashClient: new Client({ token: "" }) - } - ); - ``` +You can also notify workflows from external systems using the Workflow Client: - ```python Python - from qstash import AsyncQStash + +```typescript TypeScript +import { Client } from "@upstash/workflow"; - @serve.post("/api/example", qstash_client=AsyncQStash(os.environ["QSTASH_TOKEN"])) - async def example(context: AsyncWorkflowContext[str]) -> None: ... +const client = new Client({ token: "" }); - ``` +await client.notify({ + eventId: "order-completed-123", + eventData: { + orderId: "123", + status: "completed", + deliveryTime: "2 days", + trackingNumber: "TRK123456" + } +}); +``` - +```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" + } +) +``` + - The `Receiver` verifies that every request to your endpoint actually comes from QStash, blocking anyone else from triggering your workflow. +## Lookback Functionality - The `receiver` option allows you to pass a QStash Receiver explicitly. +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. - By default, Workflow initializes the Receiver automatically using the environment variables `QSTASH_CURRENT_SIGNING_KEY` and `QSTASH_NEXT_SIGNING_KEY`. - - This is useful if you're working with multiple QStash projects in the same app. - - +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"; + +```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 +// Trigger a workflow and get its run ID +const { workflowRunId } = await client.trigger({ + url: "https://your-app.com/api/process-order", + body: { orderId: "123" } +}); - @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: - ... - ``` +// 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 +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"} +) -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`. +# 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 +) +``` + -Inside your workflow, these values are also exposed on `context.env`. +The same also applies to `context.notify` - ```typescript TypeScript -import { Receiver } from "@upstash/qstash"; import { serve } from "@upstash/workflow/nextjs"; -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: "", +export const { POST } = serve(async (context) => { + const { orderId, processingResult } = context.requestPayload; + + await context.run("process-order", async () => { + // ... + }) + + const { notifyResponse } = await context.notify( + "notify-processing-complete", + `order-${orderId}`, + { + orderId, + status: "completed", + result: processingResult, + completedAt: new Date().toISOString(), + workflowRunId: "targetWorkflowRunId" // 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"], - }, -) -async def example(context: AsyncWorkflowContext[str]) -> None: - ... +}); ``` - - - - + +When using lookback with `workflowRunId`, the notification is targeted to a specific workflow run rather than all waiters with that event ID. + - Enables verbose mode to print detailed logs of workflow execution to the application's `stdout`. +# Parallel Steps +Source: https://upstash.com/docs/workflow/features/parallel-steps - Verbose mode is disabled by default. +Upstash Workflow supports executing multiple steps in parallel. - ```typescript - export const { POST } = serve( - async (context) => { ... }, - { - verbose: true - } - ); - ``` +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"; - - 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. +export const { POST } = serve(async (context) => { - By default, the Upstash Workflow SDK collects anonymous telemetry data to help improve the service. - The collected data includes: + // 👇 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")), + ]); - * SDK version - * Platform (Vercel, AWS, etc.) - * Runtime version (Node.js, Python, etc.) +}); +``` - Set `disableTelemetry` to `true` to opt out of telemetry for this specific workflow endpoint. +The results of the parallel steps are available as usual once awaited. - +The dashboard visualizes parallel execution as shown below: - ```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: ... - ``` - +You can also await different step types together. For example, you can run a `context.call()` and a `context.run()` in parallel. - - You should also - set [`disableTelemetry` when triggering workflow runs via `client.trigger()`](/docs/workflow/basics/client/trigger#param-disable-telemetry) to fully disable telemetry - - + + Whether executing sequentially or in parallel, you should always + await all promises in a workflow. + Leaving promises unawaited may cause unexpected behavior. + -# Changelog -Source: https://upstash.com/docs/workflow/changelog +# Overview +Source: https://upstash.com/docs/workflow/features/retries - - 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. - +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 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. - +## How Retries Work - -* **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. - +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 `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. - + - -* **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). - +By default, the retry count is set to **3**, and an **exponential backoff** delay strategy is used. - -* **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. - +```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`):** - * 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. - - - -* **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. - +| Retry Attempt | Algorithm | Delay | +|---------------|--------------|--------| +| 1 | $$e^{2.5}$$ | 12s | +| 2 | $$e^5$$ | 2m28s | +| 3 | $$e^{7.5}$$ | 30m8s | +| 4+ | $$86400$$ | 24h | - -* **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). - +## Configuration - -* **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). - +You can configure retry behavior when starting a new workflow run. - -* **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. - +### Configure Retry Attempt Count - -* **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). +You can specify how many times a step should be retried upon failure. -* **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). - +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; - -* **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. - +const client = new Client({ token: "" }) - -* 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. - +const { workflowRunId } = await client.trigger({ + url: "https:///", + retries: 3 +}) +``` - -* Bug fixes and internal logging improvements. - +### Configure Retry Delay Strategy - -* Released [Upstash Workflow](/docs/workflow/getstarted). - +Retry delay is the time to wait before trying again after a failure. You can define a custom retry delay strategy. -- [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) +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). -# Overview -Source: https://upstash.com/docs/workflow/features/dlq +To apply a constant delay, you can simply provide a fixed value. -The Dead Letter Queue (DLQ) automatically captures failed workflow runs that have exhausted all retry attempts. +The expression must return the delay in **milliseconds**. -This ensures that no workflow execution is lost and provides multiple options for recovering from failures gracefully. +```typescript Configure Retry Delay Strategy +import { Client } from "@upstash/workflow"; -## How it works? +const client = new Client({ token: "" }) -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. +const { workflowRunId } = await client.trigger({ + url: "https:///", + retries: 3, + retryDelay: "(1 + retried) * 1000" +}) +``` - +# Prevent Retries +Source: https://upstash.com/docs/workflow/features/retries/prevent-retries -The DLQ serves as a safety net, preserving failed workflow runs with their complete execution context. +It is recommended to enable retries for workflow runs to improve reliability. - - 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 +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. - After the retention duration expires, DLQ items are automatically removed and cannot be recovered. - +## Using `WorkflowNonRetryableError` -## Recovery Actions +`WorkflowNonRetryableError` lets you explicitly fail a workflow without entering the retry cycle. -Once a workflow run is in the DLQ, you can take the following actions: +When thrown, the workflow run is marked as failed, which: +* Triggers the failure function (if defined) +* Sends the workflow run to the DLQ -* **[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. +```ts TypeScript highlight={7} +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload -You can apply these actions in bulk to multiple DLQ entries. Check the individual action pages for more details. + const isExists = await context.run("is-user-exists", () => { ... }); -# Rerun Failure Function -Source: https://upstash.com/docs/workflow/features/dlq/callback + if (!isExists) { + throw new WorkflowNonRetryableError("The user does not exists!") + } +}) +``` -The **Rerun Failure Function** action allows you to retry the failure function that executes when a workflow run enters the Dead Letter Queue (DLQ). +## Using `context.cancel()` -The failure function is typically a cleanup or notification operation that runs automatically whenever a workflow is moved to the DLQ. +You can cancel a workflow run explicitly from inside the workflow. -This feature is particularly helpful for: +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 -* 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. + +```typescript highlight={10-11} TypeScript +export const { POST } = serve<{ orderId: string }>(async (context) => { + const { orderId } = context.requestPayload; -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. + // Check if order is still valid + const orderStatus = await context.run("check-order-status", async () => { + return await getOrderStatus(orderId); + }); - + if (orderStatus === "cancelled") { + // Stop execution gracefully without error + await context.cancel(); + return; + } -You can perform this action programmatically as well: + // Continue processing if order is valid + await context.run("process-order", async () => { + return await processOrder(orderId); + }); +}); +``` - - ```typescript TypeScript - import { Client } from "@upstash/workflow"; +```python Python +@serve.post("/graceful-cancellation") +async def graceful_cancellation(context: AsyncWorkflowContext[dict]) -> None: + order_id = context.request_payload["order_id"] - const client = new Client({ token: "" }); + async def _check_order_status(): + return await get_order_status(order_id) - await client.dlq.retryFailureFunction({ - dlqId: "dlq-12345", - }); - ``` - + # Check if order is still valid + order_status = await context.run("check-order-status", _check_order_status) - - 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 order_status == "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 + async def _process_order(): + return await process_order(order_id) -# Delete -Source: https://upstash.com/docs/workflow/features/dlq/delete + await context.run("process-order", _process_order) +``` + -The **Delete** action allows you to remove failed workflow runs from the Dead Letter Queue when no recovery action is needed. +## Using conditional execution -You can delete entries individually, in bulk by IDs, or by using filters such as label, URL, or date range. +You can also use guard conditions to skip certain steps and exit early, without throwing errors or canceling the workflow. +In this case, the workflow run completes successfully because no error was raised. - ```typescript TypeScript - import { Client } from "@upstash/workflow"; - - const client = new Client({ token: "" }); - - // delete a single entry - await client.dlq.delete("dlq-12345"); + ```typescript TypeScript highlight={10-11} + export const { POST } = serve<{ data: any }>(async (context) => { + const { data } = context.requestPayload; - // delete multiple entries - await client.dlq.delete(["dlq-12345", "dlq-67890"]); + // Check if order is still valid + const orderStatus = await context.run("check-order-status", async () => { + return await getOrderStatus(orderId); + }); - // delete by filters - await client.dlq.delete({ label: "my-label" }); + if (orderStatus === "not-found") { + // Stop execution without error + return; + } - // delete all - await client.dlq.delete({ all: true }); + // Continue processing if order is valid + await context.run("process-order", async () => { + return await processOrder(orderId); + }); + }); ``` - - -For the full API reference, see [client.dlq.delete](/docs/workflow/basics/client/dlq/delete). - -# 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. + ```python Python + @serve.post("/conditional-execution") + async def conditional_execution(context: AsyncWorkflowContext[dict]) -> None: + data = context.request_payload["data"] -This approach is ideal when: + async def _validate_data(): + return validate_input_data(data) -* 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. + # Validate data first + validation_result = await context.run("validate-data", _validate_data) - + if not validation_result["is_valid"]: + # Log the validation failure + async def _log_validation_failure(): + await log_validation_error(validation_result["errors"]) -You can perform this action programmatically as well: + await context.run("log-validation-failure", _log_validation_failure) - - ```typescript TypeScript - import { Client } from "@upstash/workflow"; + # Stop execution without error + return - const client = new Client({ token: "" }); + # Only execute if validation passes + async def _process_valid_data(): + return await process_data(data) - await client.dlq.restart({ - dlqId: "dlq-12345", - retries: 3, - }); + await context.run("process-valid-data", _process_valid_data) ``` -# Resume -Source: https://upstash.com/docs/workflow/features/dlq/resume - -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. - -This approach is ideal when: - -* 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. - - +# 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. + // Execute the scheduled task + await context.run("execute-scheduled-task", async () => { + return await executeTask(userId); + }); +}); +``` -If you think this advanced option fits your need, you can configure it by passing `failureUrl` configuration. +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext +from datetime import datetime - - ```typescript - import { Client } from "@upstash/workflow"; - - 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. - -The first 2 requests within the minute are executed immediately: +### Waiting for Multiple Calls - +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: -The 3rd request in the same minute is not executed immediately: +```typescript +while (true) { + const webhookResponse = await context.waitForWebhook( + `wait for progress update ${stepCount}`, + webhook, + "5m" // 5 minute timeout between updates + ); - + 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()); -Instead of rejecting it, Workflow schedules the request in the next available time window: + if (request.headers.get("x-task-finished") === "true") { + console.log("Task finished, exiting loop"); + break; + } + } +} +``` - +## Race Condition Safety -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. +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. -# Overview -Source: https://upstash.com/docs/workflow/features/invoke +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. -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. +## Comparison with Wait for Event -When you use `context.invoke`, invoking workflow will wait until the invoked workflow finishes before running the next step. +Webhooks and [Wait for Event](/docs/workflow/features/wait-for-event) serve similar purposes but with different approaches: -```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) - } -) -``` +| 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 | -You can return a response from a workflow, which will be delivered to invoker workflow run. +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. - +## API Reference - - 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. - + + + Create a unique webhook URL for external services to call + + + Wait for the webhook to be called or timeout + + -# Using Serve Many -Source: https://upstash.com/docs/workflow/features/invoke/serveMany +# Getting Started +Source: https://upstash.com/docs/workflow/getstarted -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: +## Overview -* You'd have to provide the URL explicitly like a trigger request -* You'd lose type safety for request and response payloads +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. -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. + - - - Use `createWorkflow()` to define workflows as objects. +## Quickstarts - 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. +Upstash Workflow supports Next.js, Cloudflare Workers and [many other frameworks](/docs/workflow/quickstarts/platforms) in TypeScript and Python. - ```typescript - const workflowOne = createWorkflow( - // 👇 Request Payload Type - async (context: WorkflowContext) => { + + + 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 + + - await context.sleep("wait 1 second", 1) +## Key Features - // 👇 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. - - 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: - - -```typescript TypeScript -import { Client } from "@upstash/workflow"; +## Safe changes -const client = new Client({ token: "" }); +Updating workflow code is safe in the following cases: -await client.notify({ - eventId: "order-completed-123", - eventData: { - orderId: "123", - status: "completed", - deliveryTime: "2 days", - trackingNumber: "TRK123456" - } -}); -``` +* No active workflow runs exist +* Only new steps are added to the end of the workflow -```python Python -from upstash_workflow import Client +## Guidelines for updating workflows -client = Client("") +Consider the following approaches when updating your workflow code: -# 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" - } -) -``` - +* **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. -## Lookback Functionality +For a deeper understanding of these limitations, see our [how workflows work](/docs/workflow/basics/how) section. -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. +# Configure a Run +Source: https://upstash.com/docs/workflow/howto/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 +You can configure a workflow run when starting it. The following are the options you can configure: - -```typescript TypeScript +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. + +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"; + +function handler(req, res) { + // responses from qstash are base64-encoded + const decoded = atob(req.body.body); + console.log(decoded); + + return res.status(200).end(); +} + +export default verifySignature(handler); + +export const config = { + api: { + bodyParser: false, + }, +}; +``` -### Configure Retry Attempt Count +`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. -You can specify how many times a step should be retried upon failure. +## 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 } + ``` + -}); -``` - -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext +### Debug Events -app = FastAPI() -serve = Serve(app) + + Called when an error occurs. -@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"] + **Parameters:** + * `workflowRunId`: The workflow run ID (optional) + * `error`: The error object - # Send order processing request - async def _request_order_processing(): - return await request_order_processing(order_id) + ```typescript + onError: async ({ workflowRunId, error }) => { + // Handle error + } + ``` + - await context.run("request-order-processing", _request_order_processing) + + Called when a warning is logged. - # Wait for order processing completion - result = await context.wait_for_event( - "wait-for-order-processing", - f"order-{order_id}", - timeout="10m" # 10 minutes timeout - ) + **Parameters:** + * `workflowRunId`: The workflow run ID (optional) + * `warning`: The warning message - if result["timeout"]: - # Handle timeout scenario - async def _handle_timeout(): - return await handle_order_timeout(order_id, user_email) + ```typescript + onWarning: async ({ workflowRunId, warning }) => { + // Handle warning + } + ``` + - await context.run("handle-timeout", _handle_timeout) - return + + Called when an info message is logged. - # Process the completed order - async def _process_completed_order(): - return await process_completed_order(order_id, result["event_data"]) + **Parameters:** + * `workflowRunId`: The workflow run ID (optional) + * `info`: The info message - await context.run("process-completed-order", _process_completed_order) -``` - + ```typescript + onInfo: async ({ workflowRunId, info }) => { + // Handle info + } + ``` + -# Overview -Source: https://upstash.com/docs/workflow/features/wait-for-event +## Examples -Wait for Event feature that allows you to pause workflow execution until an external event occurs. +### Error Tracking Middleware -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. +Send errors to an external monitoring service: -## How Wait for Event Works +```typescript +import { WorkflowMiddleware } from "@upstash/workflow"; -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. +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() + }) + }); + } + } +}); +``` -Each waiter has a timeout duration to wait for the event and then fires automatically. +### Multiple Middlewares - - 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) - +You can use multiple middlewares together: -## Race Condition Between Wait and Notify +```typescript +import { serve } from "@upstash/workflow/nextjs"; +import { loggingMiddleware } from "@upstash/workflow"; -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. +export const { POST } = serve( + async (context) => { + // Your workflow logic + }, + { + middlewares: [ + loggingMiddleware, + errorTrackingMiddleware, + performanceMiddleware + ] + } +); +``` -### Solutions +Middlewares are executed in the order they're provided in the array. -There are three ways to handle race conditions: +# Migration Guide +Source: https://upstash.com/docs/workflow/howto/migrations -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) +This guide covers migration between different versions of Upstash Workflow. -#### Option 1: Lookback with workflowRunId + + + 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. -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`: + ## Agents API → Separate Package - -```typescript TypeScript -import { Client } from "@upstash/workflow"; + The Agents API has been moved to a separate package to remove the AI SDK dependency from the core workflow package. -const client = new Client({ token: "" }); + ### Migration Steps -// Trigger a workflow -const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/process-order", - body: { orderId: "123" } -}); + 1. Install the new package: -// Immediately notify with lookback - no race condition! -await client.notify({ - eventId: "payment-verified", - eventData: { verified: true }, - workflowRunId: workflowRunId, // Enables lookback -}); -``` - + ```bash + npm install @upstash/workflow-agents + ``` -#### Option 2: Use Webhooks + 2. Update your imports: -[Webhooks](/docs/workflow/features/webhooks) have built-in lookback and are safer against timing issues for general event handling. + ```typescript + // Old + import { serve } from "@upstash/workflow/nextjs"; -#### Option 3: Check and Retry + 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({ ... }); + }); -Alternatively, you can check the response of the `notify` operation and retry if needed: + // New + import { serve } from "@upstash/workflow/nextjs"; + import { agentWorkflow } from "@upstash/workflow-agents"; - -```typescript TypeScript -import { Client } from "@upstash/workflow"; + export const { POST } = serve(async (context) => { + const agents = agentWorkflow(context) -const client = new Client({ token: "" }); + const model = agents.openai('gpt-3.5-turbo'); + const agent = agents.agent({ ... }); + const task = agents.task({ ... }); + }); + ``` -const result = await client.notify({ - eventId, - eventData -}); + See [Agents documentation](/docs/workflow/agents/overview) for more details. -// Check if any workflows were notified -if (result.waiters && result.waiters.length > 0) { - console.log(`Notified ${result.waiters.length} workflows`); - return result; -} + ## Removed `keepTriggerConfig` and `useFailureFunction` -// 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 parameters are no longer needed in `client.trigger()` as both are now `true` by default. -return await client.notify({ - eventId, - eventData -}); -``` - + ### Migration Steps -## Selecting an Event ID + Simply remove these parameters from your trigger calls: -When a workflow run waits on an event ID, it's appended to a list of waiters for the event ID. + ```typescript + // Old + const { workflowRunId } = await client.trigger({ + url: "https://your-app.com/api/workflow", + retries: 3, + keepTriggerConfig: true, + useFailureFunction: true + }); -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. + // New + const { workflowRunId } = await client.trigger({ + url: "https://your-app.com/api/workflow", + retries: 3 + }); + ``` -# Webhooks -Source: https://upstash.com/docs/workflow/features/webhooks + Configuration passed to `trigger()` now automatically applies to the entire workflow. -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. + ## Configuration Moved from `serve` to `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 + The `retries`, `flowControl`, `retryDelay`, and `failureUrl` options have been removed from `serve()` and should now be passed in `client.trigger()`. -## How Webhooks Work + ### Migration Steps -When you use webhooks in your workflow, Upstash Workflow: + Move configuration from serve options to trigger: -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 + ```typescript + // Old + export const { POST } = serve( + async (context) => { ... }, + { + retries: 3, + retryDelay: "1000 * (1 + retried)", + flowControl: { key: "my-key", rate: 10 } + } + ); -This happens without keeping your serverless function running, making it cost-effective for long-running integrations. + // Trigger call + await client.trigger({ url: "..." }); -## Examples + // New + export const { POST } = serve( + async (context) => { ... } + // No configuration here anymore + ); -### Basic Usage + // Configuration in trigger call + await client.trigger({ + url: "...", + retries: 3, + retryDelay: "1000 * (1 + retried)", + flowControl: { key: "my-key", rate: 10 } + }); + ``` -```typescript -import { serve } from "@upstash/workflow/nextjs"; + This change makes it easier to configure different behavior for different workflow runs of the same endpoint. -export const { POST } = serve(async (context) => { - // Step 1: Create webhook - const webhook = await context.createWebhook("create webhook"); + + In the Python SDK, these options remain in the `serve` decorator as they were before. + - // 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, - }), - }); + ## Removed `stringifyBody` from `context.call` and `context.invoke` - // Step 3: Wait for the webhook to be called - const webhookResponse = await context.waitForWebhook( - "wait for webhook", - webhook, - "30s" // timeout - ); + The `stringifyBody` parameter has been removed. The `body` parameter now expects a string. - 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 - } -}); -``` + ### Migration Steps -### Waiting for Multiple Calls + Update your call and invoke methods to use `JSON.stringify()`: -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 + // Old + const result = await context.call("call-api", { + url: "https://api.example.com/endpoint", + method: "POST", + body: { key: "value" }, + stringifyBody: true + }); -```typescript -while (true) { - const webhookResponse = await context.waitForWebhook( - `wait for progress update ${stepCount}`, - webhook, - "5m" // 5 minute timeout between updates - ); + // New + const result = await context.call("call-api", { + url: "https://api.example.com/endpoint", + method: "POST", + body: JSON.stringify({ key: "value" }) + }); + ``` - 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()); + The same applies to `context.invoke()`: - if (request.headers.get("x-task-finished") === "true") { - console.log("Task finished, exiting loop"); - break; - } - } -} -``` + ```typescript + // Old + await context.invoke("invoke-workflow", { + workflow: otherWorkflow, + body: { key: "value" }, + stringifyBody: true + }); -## Race Condition Safety + // New + await context.invoke("invoke-workflow", { + workflow: otherWorkflow, + body: JSON.stringify({ key: "value" }) + }); + ``` -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. + ## Logger → 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. + The logging system has been replaced with a more flexible middleware system. -## Comparison with Wait for Event + ### Migration Steps -Webhooks and [Wait for Event](/docs/workflow/features/wait-for-event) serve similar purposes but with different approaches: + Replace the old logger with the new middleware: -| 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 + // Old + // Logging was automatic or controlled via verbose option + export const { POST } = serve( + async (context) => { ... }, + { verbose: true } + ); -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. + // New + import { loggingMiddleware } from "@upstash/workflow"; -## API Reference + export const { POST } = serve( + async (context) => { ... }, + { + middlewares: [loggingMiddleware] + } + ); + ``` - - - Create a unique webhook URL for external services to call - - - Wait for the webhook to be called or timeout - - + You can also create custom middlewares for more control. See [Middlewares documentation](/docs/workflow/howto/middlewares) for details. -# Getting Started -Source: https://upstash.com/docs/workflow/getstarted + ## Removed `onStepFinish` -## Overview + The `onStepFinish` callback has been removed. Use middlewares instead. -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. + ### Migration Steps - + Replace `onStepFinish` with a custom middleware: -## Quickstarts + ```typescript + // Old + export const { POST } = serve( + async (context) => { ... }, + { + onStepFinish: (stepName, result) => { + console.log(`Step ${stepName} finished with:`, result); + } + } + ); -Upstash Workflow supports Next.js, Cloudflare Workers and [many other frameworks](/docs/workflow/quickstarts/platforms) in TypeScript and Python. + // New + import { WorkflowMiddleware } from "@upstash/workflow"; - - - 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 - - + const stepFinishMiddleware = new WorkflowMiddleware({ + name: "step-finish", + callbacks: { + afterExecution: async ({ stepName, result }) => { + console.log(`Step ${stepName} finished with:`, result); + } + } + }); -## Key Features + export const { POST } = serve( + async (context) => { ... }, + { + middlewares: [stepFinishMiddleware] + } + ); + ``` - - - 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. - - + See [Middlewares documentation](/docs/workflow/howto/middlewares) for more details. -## Example Use Cases + -Here are some example real world use-cases for Upstash Workflow: + + 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. - - - 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. - - + 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. -## How it works + ### Install `@upstash/workflow` -Upstash Workflow builds on the principle of steps. Instead of defining a single, complex piece of business logic, workflows contain multiple individual steps. + First, we will need to install the new package with: -Each of the steps are executed by a separate request to your application, by preserving the output of previous steps. + + + ```bash + npm install @upstash/workflow + ``` + + + ```bash + pnpm install @upstash/workflow + ``` + + + ```bash + bun add @upstash/workflow + ``` + + -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. + If you were using `@upstash/qstash` only for workflow, you can uninstall it from your project. - + ### Serve methods -## Support + You will need to change the imports from `@upstash/qstash` to @upstash/workflow: -Need help or have questions? We're here to support you: + ```ts + // old + import { serve } from "@upstash/qstash/nextjs" -* Join our Discord community to ask questions and share feedback -* Open a ticket through the Intercom chatbox in the dashboard for any issue + // new + import { serve } from "@upstash/workflow/nextjs" + ``` -# Cancel a Run -Source: https://upstash.com/docs/workflow/howto/cancel + We have updated what our `serve` methods return. We made this change to make it + easier to extend the API in the future. -You can cancel a running workflow both programatically and from your Upstash Workflow console. + For instance, Next.js method changed like this: -## Cancelling via console + ```javascript + // old + 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: + // new + export const { POST } = serve(...); + ``` - + 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. -## Cancelling programatically + 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`. - - 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. - + ### Updating `context.call` -```javascript -import { Client } from "@upstash/workflow"; + 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: -const client = new Client({ token: "" }); -await client.cancel({ ids: "" }); -``` + ```javascript + // old + const result = await context.call("call step", "", "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). + // new + const { + status, // response status + headers, // response headers + body // response body + } = await context.call("call step", { + url: "", + method: "POST", + ... + }) + ``` -You can also use the [Upstash Workflow REST API](/docs/workflow/api-reference/runs/cancel-workflow-run) to cancel a run programatically. + 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). -# Update a Workflow -Source: https://upstash.com/docs/workflow/howto/changes + 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. -Workflows are composed of multiple steps. When you modify workflow code, it's important to consider how these changes might affect in-progress workflows. + 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. -## Issues + ### Renaming Errors -You cannot change the step order of an existing workflow. + The errors in Workflow were renamed from `QStashWorkflowError` and `QStashWorkflowAbort` to `WorkflowError` and `WorkflowAbort`. + + + + +# Select a Region +Source: https://upstash.com/docs/workflow/howto/multi-region + +## 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` -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: +## Key Concepts -```bash -HTTP status 400. Incompatible step name. Expected , got -``` +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 - -@serve.post("/api/example", failure_function=failure_function) -async def example(context: AsyncWorkflowContext[str]) -> None: ... -``` +In a complete code example, your workflow could look like this: - +```typescript app/api/workflow/route.ts +import { serve } from "@upstash/workflow/nextjs"; +import { checkInventory, brewCoffee, printReceipt } from "@/utils"; -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). +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")), + ]); -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. + // If all ingedients available, brew coffee + if (coffeeBeansAvailable && cupsAvailable && milkAvailable) { + const price = await ctx.run("brew-coffee", async () => { + return await brewCoffee({ style: "cappuccino" }); + }); -## Using a `failureUrl` + await printReceipt(price); + } +}); +``` -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. +After running your workflow, your dashboard shows each step in detail: -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 -```typescript TypeScript -export const { POST } = serve( - async (context) => { - // Your workflow logic... - }, - { - failureUrl: "https:///workflow-failure", - } -); -``` +[**Upstash Realtime**](/docs/realtime/overall/quickstart) lets you emit events from your workflow and subscribe to them in real-time on your frontend. -```python Python -@serve.post("/api/example", failureUrl="https:///...") -async def example(context: AsyncWorkflowContext[str]) -> None: ... -``` +## How It Works - +Upstash Realtime is powered by Upstash Redis and provides a clean, 100% type-safe API for publishing and subscribing to events: -The callback body sent to you will be a JSON object with the following fields: +* 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 -```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 -} -``` +This guide shows you how to integrate Upstash Workflow with Upstash Realtime to display real-time progress updates in your frontend. -In Next.js you can use the following code to handle the callback: +## Setup -```javascript JavaScript -// pages/api/callback.js +### 1. Install Packages -import { verifySignature } from "@upstash/qstash/nextjs"; +```bash +npm install @upstash/workflow @upstash/realtime @upstash/redis zod +``` -function handler(req, res) { - // responses from qstash are base64-encoded - const decoded = atob(req.body.body); - console.log(decoded); +### 2. Configure Upstash Realtime - return res.status(200).end(); -} +Create a Realtime instance in `lib/realtime.ts`: -export default verifySignature(handler); +```typescript title="lib/realtime.ts" +import { InferRealtimeEvents, Realtime } from "@upstash/realtime"; +import { Redis } from "@upstash/redis"; +import z from "zod/v4"; -export const config = { - api: { - bodyParser: false, +const redis = Redis.fromEnv(); + +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"; + +interface WorkflowStep { + stepName: string; + result?: unknown; +} + +export function useWorkflow() { + const [workflowRunId, setWorkflowRunId] = useState(null); + const [steps, setSteps] = useState([]); + const [isRunFinished, setIsRunFinished] = useState(false); - 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: + 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(); + afterExecution: async ({ context, stepName, result }) => { + const channel = realtime.channel(context.workflowRunId); - // 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 }); + // 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 +**Key points:** -### Lifecycle Events +* 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 - - Called when a workflow run begins. +## Building the Workflow - **Parameters:** - * `context`: The workflow context +### 1. Create the Workflow Endpoint - ```typescript - runStarted: async ({ context }) => { - // Handle run start - } - ``` - +Create your workflow at `app/api/workflow/human-in-loop/route.ts`: - - Called before each step executes. +```typescript title="app/api/workflow/human-in-loop/route.ts" +import { serve } from "@upstash/workflow/nextjs"; +import { realtimeMiddleware } from "@/lib/middleware"; - **Parameters:** - * `context`: The workflow context - * `stepName`: Name of the step about to execute +type WorkflowPayload = { + userId: string; + action: string; +}; - ```typescript - beforeExecution: async ({ context, stepName }) => { - // Handle step start - } - ``` - +export const { POST } = serve( + async (context) => { + const { userId, action } = context.requestPayload; - - Called after each step completes. + // Step 1: Initial Processing + await context.run("initial-processing", async () => { + // Your processing logic + return { + preprocessed: true, + userId, + action, + requiresApproval: true, + }; + }); - **Parameters:** - * `context`: The workflow context - * `stepName`: Name of the completed step - * `result`: The result returned by the step + // Step 2: Wait for Human Approval + const eventId = `approval-${context.workflowRunId}`; - ```typescript - afterExecution: async ({ context, stepName, result }) => { - // Handle step completion - } - ``` - + const { eventData, timeout } = await context.waitForEvent<{ + approved: boolean; + }>("wait-for-approval", eventId, { timeout: "5m" }); - - Called when the entire workflow run finishes. + // Handle timeout + if (timeout) { + return { success: false, reason: "timeout" }; + } - **Parameters:** - * `context`: The workflow context - * `result`: The final result of the workflow + const status = eventData.approved ? "approved" : "rejected"; - ```typescript - runCompleted: async ({ context, result }) => { - // Handle run completion - } - ``` - + // Step 3: Process based on approval + await context.run(`process-${status}`, async () => { + return { + status, + processedAt: Date.now(), + action, + userId, + }; + }); -### Debug Events + // Step 4: Finalize (only if approved) + if (eventData.approved) { + // Additional steps... + } - - Called when an error occurs. + return { + success: true, + approved: eventData.approved, + workflowRunId: context.workflowRunId, + }; + }, + { + middlewares: [realtimeMiddleware], + } +); +``` - **Parameters:** - * `workflowRunId`: The workflow run ID (optional) - * `error`: The error object +**Key patterns:** - ```typescript - onError: async ({ workflowRunId, error }) => { - // Handle error - } - ``` - +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 a warning is logged. +### 2. Create the Notify Endpoint - **Parameters:** - * `workflowRunId`: The workflow run ID (optional) - * `warning`: The warning message +Create an endpoint at `app/api/notify/route.ts` to handle user input: - ```typescript - onWarning: async ({ workflowRunId, warning }) => { - // Handle warning - } - ``` - +```typescript +import { Client } from "@upstash/workflow"; +import { NextRequest, NextResponse } from "next/server"; - - Called when an info message is logged. +const workflowClient = new Client({ + baseUrl: process.env.QSTASH_URL!, + token: process.env.QSTASH_TOKEN!, +}); - **Parameters:** - * `workflowRunId`: The workflow run ID (optional) - * `info`: The info message +export async function POST(request: NextRequest) { + const body = await request.json(); + const { eventId, eventData } = body; - ```typescript - onInfo: async ({ workflowRunId, info }) => { - // Handle info + if (!eventId) { + return NextResponse.json( + { success: false, error: "eventId is required" }, + { status: 400 } + ); } - ``` - -## Examples + // Notify the workflow + await workflowClient.notify({ + eventId, + eventData, + }); -### Error Tracking Middleware + return NextResponse.json({ success: true }); +} +``` -Send errors to an external monitoring service: + +**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 { WorkflowMiddleware } from "@upstash/workflow"; - -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() - }) - }); - } - } +await workflowClient.notify({ + eventId, + eventData, + workflowRunId: "wfr_abc123", // Ensures notification is delivered even if sent before waitForEvent }); ``` -### Multiple Middlewares - -You can use multiple middlewares together: +Learn more in the [notify documentation](/docs/workflow/basics/client/notify). + -```typescript -import { serve } from "@upstash/workflow/nextjs"; -import { loggingMiddleware } from "@upstash/workflow"; +## Building the Frontend -export const { POST } = serve( - async (context) => { - // Your workflow logic - }, - { - middlewares: [ - loggingMiddleware, - errorTrackingMiddleware, - performanceMiddleware - ] - } -); -``` +### 1. Extend the Custom Hook -Middlewares are executed in the order they're provided in the array. +Extend your hook from the basic example to handle waiting states: -# Migration Guide -Source: https://upstash.com/docs/workflow/howto/migrations +```typescript +"use client"; -This guide covers migration between different versions of Upstash Workflow. +import { useRealtime } from "@/lib/realtime-client"; +import { useState, useCallback } from "react"; - - - 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. +interface WorkflowStep { + stepName: string; + result?: unknown; +} - ## Agents API → Separate Package +interface WaitingState { + eventId: string; + message: string; +} - The Agents API has been moved to a separate package to remove the AI SDK dependency from the core workflow package. +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); - ### Migration Steps + 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, + }); + } + }, + }); - 1. Install the new package: + const trigger = useCallback(async () => { + setIsTriggering(true); + setSteps([]); + setWaitingState(null); + setIsRunFinished(false); - ```bash - npm install @upstash/workflow-agents - ``` + const response = await fetch("/api/trigger", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ workflowType: "human-in-loop" }), + }); - 2. Update your imports: + const data = await response.json(); + setWorkflowRunId(data.workflowRunId); + setIsTriggering(false); + }, []); - ```typescript - // Old - import { serve } from "@upstash/workflow/nextjs"; + const continueWorkflow = useCallback( + async (data: { approved: boolean }) => { + if (!waitingState) { + throw new Error("No workflow waiting for input"); + } - 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/notify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + eventId: waitingState.eventId, + eventData: data, + }), + }); - // New - import { serve } from "@upstash/workflow/nextjs"; - import { agentWorkflow } from "@upstash/workflow-agents"; + if (!response.ok) { + throw new Error("Failed to notify workflow"); + } - export const { POST } = serve(async (context) => { - const agents = agentWorkflow(context) + // The waiting state will be cleared when we receive inputResolved event + }, + [waitingState] + ); - const model = agents.openai('gpt-3.5-turbo'); - const agent = agents.agent({ ... }); - const task = agents.task({ ... }); - }); - ``` + return { + trigger, + continueWorkflow, + isTriggering, + workflowRunId, + steps, + waitingState, + isRunFinished, + }; +} +``` - See [Agents documentation](/docs/workflow/agents/overview) for more details. +**Key additions:** - ## Removed `keepTriggerConfig` and `useFailureFunction` +* **`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 - These parameters are no longer needed in `client.trigger()` as both are now `true` by default. +### 2. Use the Hook with Approval UI - ### Migration Steps +```typescript +"use client"; - Simply remove these parameters from your trigger calls: +import { useWorkflowWithRealtime } from "@/hooks/useWorkflowWithRealtime"; - ```typescript - // Old - const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/workflow", - retries: 3, - keepTriggerConfig: true, - useFailureFunction: true - }); +export default function WorkflowPage() { + const { + trigger, + isTriggering, + steps, + isRunFinished, + waitingState, + continueWorkflow, + } = useWorkflowWithRealtime(); - // New - const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/workflow", - retries: 3 - }); - ``` + return ( +
+ - Configuration passed to `trigger()` now automatically applies to the entire workflow. + {isRunFinished && ( +

✅ Workflow Finished!

+ )} - ## Configuration Moved from `serve` to `trigger` + {/* Show workflow steps */} +

Workflow Steps:

+
+ {steps.map((step, index) => ( +
+ {step.stepName} + {Boolean(step.result) && ( + : {JSON.stringify(step.result)} + )} +
+ ))} +
- The `retries`, `flowControl`, `retryDelay`, and `failureUrl` options have been removed from `serve()` and should now be passed in `client.trigger()`. + {/* Show approval UI when waiting for input */} + {waitingState && ( +
+

{waitingState.message}

+

+ +

+

+ +

+
+ )} +
+ ); +} +``` - ### Migration Steps +## How the Pattern Works - Move configuration from serve options to trigger: +### Timeline of Events - ```typescript - // Old - export const { POST } = serve( - async (context) => { ... }, - { - 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!" - // Trigger call - await client.trigger({ url: "..." }); +## Benefits - // New - export const { POST } = serve( - async (context) => { ... } - // No configuration here anymore - ); +* **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 - // Configuration in trigger call - await client.trigger({ - url: "...", - retries: 3, - retryDelay: "1000 * (1 + retried)", - flowControl: { key: "my-key", rate: 10 } - }); - ``` +## Full Example - This change makes it easier to configure different behavior for different workflow runs of the same endpoint. +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). - - In the Python SDK, these options remain in the `serve` decorator as they were before. - +## Next Steps - ## Removed `stringifyBody` from `context.call` and `context.invoke` +* 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) - The `stringifyBody` parameter has been removed. The `body` parameter now expects a string. +# Redact Private Data +Source: https://upstash.com/docs/workflow/howto/redact-fields - ### Migration Steps +Workflow runs can contain private data that you don't want visible in the Upstash Console or API responses. - Update your call and invoke methods to use `JSON.stringify()`: +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. - ```typescript - // Old - const result = await context.call("call-api", { - url: "https://api.example.com/endpoint", - method: "POST", - body: { key: "value" }, - stringifyBody: true - }); +To redact fields, pass the `redact` option when triggering a workflow run. - // New - const result = await context.call("call-api", { - url: "https://api.example.com/endpoint", - method: "POST", - body: JSON.stringify({ key: "value" }) - }); - ``` +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]`) | - The same applies to `context.invoke()`: + +Redaction is one-way. Once a field is redacted, the original value cannot be retrieved from the API or dashboard. + - ```typescript - // Old - await context.invoke("invoke-workflow", { - workflow: otherWorkflow, - body: { key: "value" }, - stringifyBody: true - }); + +```typescript TypeScript +import { Client } from "@upstash/workflow"; - // New - await context.invoke("invoke-workflow", { - workflow: otherWorkflow, - body: JSON.stringify({ key: "value" }) - }); - ``` +const client = new Client({ token: "" }); - ## Logger → 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 + }, +}); +``` - The logging system has been replaced with a more flexible middleware system. +```python Python +from upstash_workflow import Client - ### Migration Steps +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 + }, +) +``` - Replace the old logger with the new middleware: +```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' +``` + - ```typescript - // Old - // Logging was automatic or controlled via verbose option - export const { POST } = serve( - async (context) => { ... }, - { verbose: true } - ); + - // New - import { loggingMiddleware } from "@upstash/workflow"; +Redaction is configured per workflow run, so you can redact different fields for different runs. - export const { POST } = serve( - async (context) => { ... }, - { - middlewares: [loggingMiddleware] - } - ); - ``` +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. - You can also create custom middlewares for more control. See [Middlewares documentation](/docs/workflow/howto/middlewares) for details. +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. - ## Removed `onStepFinish` +# Schedule a Workflow +Source: https://upstash.com/docs/workflow/howto/schedule - The `onStepFinish` callback has been removed. Use middlewares instead. +You can schedule a workflow to run periodically using a cron definition. - ### Migration Steps +For this feature, you would need to use Upstash QStash's Schedules feature. - Replace `onStepFinish` with a custom middleware: +## Scheduling a workflow - ```typescript - // Old - export const { POST } = serve( - async (context) => { ... }, - { - onStepFinish: (stepName, result) => { - console.log(`Step ${stepName} finished with:`, result); - } - } - ); +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: - // New - import { WorkflowMiddleware } from "@upstash/workflow"; +To run this endpoint on a schedule, navigate to `Schedules` in your QStash dashboard and click `Create Schedule`: - const stepFinishMiddleware = new WorkflowMiddleware({ - name: "step-finish", - callbacks: { - afterExecution: async ({ stepName, result }) => { - console.log(`Step ${stepName} finished with:`, result); - } - } - }); + - export const { POST } = serve( - async (context) => { ... }, - { - middlewares: [stepFinishMiddleware] - } - ); - ``` +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`: - See [Middlewares documentation](/docs/workflow/howto/middlewares) for more details. + -
+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). - - 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. +## Programmatically Schedule - 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. +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. - ### Install `@upstash/workflow` +Let's create a user-specific schedule, sending a first report to each user exactly 7 days after they signed up: - First, we will need to install the new package with: + - - - ```bash - npm install @upstash/workflow - ``` - - - ```bash - pnpm install @upstash/workflow - ``` - - - ```bash - bun add @upstash/workflow - ``` - - +```typescript api/sign-up/route.ts +import { signUp } from "@/utils/auth-utils"; +import { Client } from "@upstash/qstash"; - If you were using `@upstash/qstash` only for workflow, you can uninstall it from your project. +const client = new Client({ token: process.env.QSTASH_TOKEN! }); - ### Serve methods +export async function POST(request: Request) { + const userData: UserData = await request.json(); - You will need to change the imports from `@upstash/qstash` to @upstash/workflow: + // 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, + }); - ```ts - // old - import { serve } from "@upstash/qstash/nextjs" + return NextResponse.json( + { success: true, message: "User registered and summary scheduled" }, + { status: 201 } + ); +} +``` - // new - import { serve } from "@upstash/workflow/nextjs" - ``` +```python main.py +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from qstash import AsyncQStash +from datetime import datetime, timedelta - We have updated what our `serve` methods return. We made this change to make it - easier to extend the API in the future. +app = FastAPI() - For instance, Next.js method changed like this: +client = AsyncQStash("") - ```javascript - // old - export const POST = serve(...); +@app.post("/api/sign-up") +async def sign_up(request: Request): + user_data = await request.json() - // new - export const { POST } = serve(...); - ``` + # Simulate user registration + user = await sign_up(user_data) - 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. + # Calculate the date for the first summary (7 days from now) + first_summary_date = datetime.now() + timedelta(days=7) - 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`. + # 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}" - ### Updating `context.call` + # 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, + ) + + return JSONResponse( + content={"success": True, "message": "User registered and summary scheduled"}, + status_code=201, + ) - 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 - // old - const result = await context.call("call step", "", "POST", ...) + - // new - const { - status, // response status - headers, // response headers - body // response body - } = await context.call("call step", { - url: "", - method: "POST", - ... - }) - ``` +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. - 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). + + When creating a per-user schedule, pass a unique `scheduleId` to identify the schedule for better management and observability. + - 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. +# Secure a Workflow +Source: https://upstash.com/docs/workflow/howto/security - 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. +To prevent unauthorized access to your workflow endpoint, you can add an authorization layer. +Upstash Workflow supports two approaches: - ### Renaming Errors +* **Built-in request verification** (recommended) +* **Custom authorization method** - The errors in Workflow were renamed from `QStashWorkflowError` and `QStashWorkflowAbort` to `WorkflowError` and `WorkflowAbort`. +### 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. -# Select a Region -Source: https://upstash.com/docs/workflow/howto/multi-region +How it works: -## Overview +1. Upstash Workflow automatically adds the `Upstash-Signature` header to every request. + This signature is generated using your signing keys. -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. +2. When this mechanism is enabled, the SDK verifies that the signature is valid before processing the request. -## Regional URLs +This ensures that only requests originating from Upstash Workflow are processed. -* **EU Region**: `https://qstash-eu-central-1.upstash.io`, or `https://qstash.upstash.io` -* **US Region**: `https://qstash-us-east-1.upstash.io` +To enable this verification, set the following environment variables in your application: -## Key Concepts +```bash .env +QSTASH_CURRENT_SIGNING_KEY=xxxxxxxxx +QSTASH_NEXT_SIGNING_KEY=xxxxxxxxx +``` -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 +You can find the values in Upstash Workflow dashboard. -### Migration Between Regions + -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. + + For edge cases where environment variables cannot be used, you can explicitly create and pass a `Receiver` object to verify request signatures: -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 + ```typescript TypeScript + import { Receiver } from "@upstash/qstash"; + import { serve } from "@upstash/workflow/nextjs"; - + export const { POST } = serve( + async (context) => { ... }, + { + receiver: new Receiver({ + currentSigningKey: "", + nextSigningKey: "", + }), + } + ); + ``` -The migration tool will: -* Help you set up migration-mode environment variables -* Copy and update your QStash resources (schedules, url groups, queues) + ```python Python + from qstash import Receiver -Your workflow logs or DLQ aren't part of the migration. They will remain in the old region. + @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: + ... - -After migration, your app will be able to handle requests from both regions simultaneously to ensure a smooth transition. + ``` + -## Operating Modes - -Workflow SDK supports two modes of operation: +## Custom Authorization Method -### Single-Region Mode (Default) +You can implement your own authorization mechanism with Upstash Workflow. -When `QSTASH_REGION` environment variable is **not set**, the SDK operates in single-region mode: +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. -* 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 + -```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" -``` + ```typescript TypeScript + import { serve } from "@upstash/workflow/nextjs"; -### 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]; -When `QSTASH_REGION` is set to `US_EAST_1` or `EU_CENTRAL_1`, the SDK enables migration mode: + // 👇 Use your authentication function to verify the token + if (!isValid(bearerToken)) { + console.error("Authentication failed."); + return; + } -* 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 + // Your workflow steps.. + }, + { + failureFunction: async () => { + // 👇 Same auth check for failure function + const authHeader = context.headers.get("authorization"); + const bearerToken = authHeader?.split(" ")[1]; - - If a workflow run was started in one region, all its steps will execute in that region. - + if (!isValid(bearerToken)) { + // ... + } + }, + } + ); + ``` -Environment variables: + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext -```bash -# Migration mode configuration with US as primary -QSTASH_REGION="US_EAST_1" + app = FastAPI() + serve = Serve(app) -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" + @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 -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" -``` + if not is_valid(bearer_token): + print("Authentication failed.") + return - -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. - + # Your workflow steps... -## SDK Requirements + ``` -Migration support requires: -* `@upstash/workflow` >= 1.1.0 -* `@upstash/qstash` >= 2.9.0 + -Update your dependencies: + + If you implement custom authorization in your workflow route, you should also include the same authorization check in the failure function. -```bash -npm install @upstash/workflow@latest @upstash/qstash@latest -``` + The failure function executes independently of the route function, so without this check, unauthorized requests could trigger the failure function + -# Parallel Runs -Source: https://upstash.com/docs/workflow/howto/parallel-runs +# Start a Run +Source: https://upstash.com/docs/workflow/howto/start - - 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. - +You’ve defined your workflow, and the final step is to trigger the endpoint! -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: +There are two main ways to start your workflow: -```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 () => { ... }), - ]) -``` +### Using `client.trigger` (Recommended) -In a complete code example, your workflow could look like this: +We recommend using [`client.trigger`](/docs/workflow/basics/client/trigger) to start your workflow. -```typescript app/api/workflow/route.ts -import { serve } from "@upstash/workflow/nextjs"; -import { checkInventory, brewCoffee, printReceipt } from "@/utils"; + + ```ts Single Workflow + import { Client } from "@upstash/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")), - ]); + 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 + }) - // If all ingedients available, brew coffee - if (coffeeBeansAvailable && cupsAvailable && milkAvailable) { - const price = await ctx.run("brew-coffee", async () => { - return await brewCoffee({ style: "cappuccino" }); - }); + console.log(workflowRunId) + // prints wfr_my-workflow + ``` - await printReceipt(price); - } -}); -``` + ```ts Multiple Workflows + import { Client } from "@upstash/workflow"; -After running your workflow, your dashboard shows each step in detail: + const client = new Client({ token: "" }) + const results = await client.trigger([ + { + url: "", + // other options... + }, + { + url: "", + // other options... + }, + ]) - + console.log(results[0].workflowRunId) + // prints wfr_my-workflow + ``` + -# Realtime Quickstart -Source: https://upstash.com/docs/workflow/howto/realtime/basic +### 2. Sending an HTTP Request -[**Upstash Realtime**](/docs/realtime/overall/quickstart) lets you emit events from your workflow and subscribe to them in real-time on your frontend. +This approach is recommended for quick testing via curl during development. -## How It Works +You should **NOT** start the workflow run in production by direct calls to your endpoint. -Upstash Realtime is powered by Upstash Redis and provides a clean, 100% type-safe API for publishing and subscribing to events: +```bash +curl -X POST https:/// \ + -H "my-header: foo" \ + -d '{"foo": "bar"}' +``` -* 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 + + 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. -This guide shows you how to integrate Upstash Workflow with Upstash Realtime to display real-time progress updates in your frontend. + For more information, read [Secure a workflow](/docs/workflow/howto/security) documentation. + -## Setup +# Webhooks +Source: https://upstash.com/docs/workflow/howto/use-webhooks -### 1. Install Packages +This guide explains how to handle webhooks effectively in your Upstash Workflow applications. We'll walk through: -```bash -npm install @upstash/workflow @upstash/realtime @upstash/redis zod -``` +* setting up webhook endpoints +* verifying webhook requests +* and processing webhook events -### 2. Configure Upstash Realtime + + You can find the project source code on GitHub. + -Create a Realtime instance in `lib/realtime.ts`: +## Overview -```typescript title="lib/realtime.ts" -import { InferRealtimeEvents, Realtime } from "@upstash/realtime"; -import { Redis } from "@upstash/redis"; -import z from "zod/v4"; +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 redis = Redis.fromEnv(); +Upstash Workflow provides a simple way to receive these events and trigger workflows based on the incoming data autonomously. -const schema = { - workflow: { - runFinish: z.object({}), - stepFinish: z.object({ - stepName: z.string(), - result: z.unknown().optional(), - }), - }, -}; +### Setting Up Webhook Endpoints -export const realtime = new Realtime({ schema, redis }); -export type RealtimeEvents = InferRealtimeEvents; -``` +#### Basic Setup -### 3. Create a Realtime Middleware +To create a webhook endpoint, use the `serve` function from `@upstash/workflow`: -Create a custom middleware that will emit events to Realtime at `lib/middleware.ts`: + -```typescript title="lib/middleware.ts" -import { WorkflowMiddleware } from "@upstash/workflow"; -import { realtime } from "./realtime"; +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; -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", {}); - }, +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 + + 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) + ); - await context.run("process-action", async () => { - // Your business logic here - return { processed: true, userId, action }; - }); + 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) 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 +When called with the same prompt as above, we will see the following logs: -### 2. Create the Notify Endpoint + -Create an endpoint at `app/api/notify/route.ts` to handle user input: +## Important Considerations -```typescript -import { Client } from "@upstash/workflow"; -import { NextRequest, NextResponse } from "next/server"; +When using Upstash Workflow with the Vercel AI SDK, there are several critical requirements that must be followed: -const workflowClient = new Client({ - baseUrl: process.env.QSTASH_URL!, - token: process.env.QSTASH_TOKEN!, -}); +### Step Execution Order -export async function POST(request: NextRequest) { - const body = await request.json(); - const { eventId, eventData } = body; +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: - if (!eventId) { - return NextResponse.json( - { success: false, error: "eventId is required" }, - { status: 400 } - ); - } + +```typescript ❌ Wrong {4} +export const { POST } = serve<{ prompt: string }>(async (context) => { + const openai = createWorkflowOpenAI(context); - // Notify the workflow - await workflowClient.notify({ - eventId, - eventData, + // Will throw "prompt is undefined" + const result = await generateText({ + model: openai('gpt-3.5-turbo'), + prompt: context.requestPayload.prompt }); - - return NextResponse.json({ success: true }); -} +}); ``` - -**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** + +1. In Datadog, open the Integrations page. +2. Search for "Upstash" and open the integration. + +![integration-tab.png]() -import { useWorkflowWithRealtime } from "@/hooks/useWorkflowWithRealtime"; +Click "Install" to add Upstash to your Datadog account. -export default function WorkflowPage() { - const { - trigger, - isTriggering, - steps, - isRunFinished, - waitingState, - continueWorkflow, - } = useWorkflowWithRealtime(); +![installation.png]() - return ( -
- +## **Step 3: Connect Accounts** - {isRunFinished && ( -

✅ Workflow Finished!

- )} +After installing Upstash, click "Connect Accounts". Datadog will redirect you to Upstash to complete account linking. - {/* Show workflow steps */} -

Workflow Steps:

-
- {steps.map((step, index) => ( -
- {step.stepName} - {Boolean(step.result) && ( - : {JSON.stringify(step.result)} - )} -
- ))} -
+![connect-acc.png]() - {/* Show approval UI when waiting for input */} - {waitingState && ( -
-

{waitingState.message}

-

- -

-

- -

-
- )} -
- ); -} -``` +## **Step 4: Select Account to Integrate** -## How the Pattern Works +1. On Upstash, select the Datadog account to integrate. +2. Personal and team accounts are supported. -### Timeline of Events +**Caveats** -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!" +* 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. -## Benefits +![personal.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 +![team.png]() -## Full Example +## **Step 5: Wait for Metrics Availability** -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). +Once the integration is completed, metrics from QStash (publish counts, success/error rates, retries, DLQ, schedule executions) will start appearing in Datadog dashboards shortly. -## Next Steps +![upstash-dashboard.png]() -* 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) +## **Step 6: Datadog Integration Removal Process** -# Redact Private Data -Source: https://upstash.com/docs/workflow/howto/redact-fields +From Datadog → Integrations → Upstash, press "Remove" to break the connection. -Workflow runs can contain private data that you don't want visible in the Upstash Console or API responses. +### Confirm Removal -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. +Upstash will stop publishing metrics after removal. Ensure any Datadog API keys/configurations for this integration are also removed on the Datadog side. -To redact fields, pass the `redact` option when triggering a workflow run. +## **Conclusion** -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]`) | +You’ve connected Datadog with Upstash QStash. Explore Datadog dashboards to monitor message delivery performance and reliability. - -Redaction is one-way. Once a field is redacted, the original value cannot be retrieved from the API or dashboard. - +If you need help, contact support. - -```typescript TypeScript -import { Client } from "@upstash/workflow"; +# OpenAI +Source: https://upstash.com/docs/workflow/integrations/openai -const client = new Client({ token: "" }); +### Calling OpenAI -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 - }, -}); -``` +The standard way to call a third-party endpoint in your workflow is by using [`context.call`](/docs/workflow/basics/context#context-call). -```python Python -from upstash_workflow import Client +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: -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 + + `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. + + +```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"; +![monitoring-token.png]() -const client = new Client({ token: process.env.QSTASH_TOKEN! }); +## **Step 3: Configure Prometheus (via Grafana Data Source)** -export async function POST(request: Request) { - const userData: UserData = await request.json(); +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. - // 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, - }); +![datasource.png]() - return NextResponse.json( - { success: true, message: "User registered and summary scheduled" }, - { status: 201 } - ); -} -``` +![headers.png]() -```python main.py -from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse -from qstash import AsyncQStash -from datetime import datetime, timedelta +Click Test and Save. -app = FastAPI() +![datasource-final.png]() -client = AsyncQStash("") +## **Step 4: Import Dashboard** -@app.post("/api/sign-up") -async def sign_up(request: Request): - user_data = await request.json() +You can use the Upstash Grafana dashboard to visualize QStash metrics. - # Simulate user registration - user = await sign_up(user_data) +Open the import dialog and use: Upstash QStash Dashboard - # Calculate the date for the first summary (7 days from now) - first_summary_date = datetime.now() + timedelta(days=7) +![grafana-dashboard.png]() - # 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}" +## **Conclusion** - # 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, - ) +You’ve integrated QStash with Prometheus. Use Grafana to explore message throughput, retries, DLQ, schedules, and Upstash Workflows. - return JSONResponse( - content={"success": True, "message": "User registered and summary scheduled"}, - status_code=201, - ) +If you encounter issues, contact support. -``` +# Resend +Source: https://upstash.com/docs/workflow/integrations/resend - +The standard way to call a third-party endpoint in your workflow is by using [`context.call`](/docs/workflow/basics/context#context-call). -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. +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)) { - // ... - } - }, + export const { POST } = serve( + // 👇 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 @@ -53815,140 +53840,253 @@ 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 label of the current workflow run, if set in [client.trigger](/docs/workflow/basics/client/trigger). + - - ```ts Single Workflow - import { Client } from "@upstash/workflow"; +## Context Object Functions - 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 - }) +You can use the functions exposed by context object to define workflow steps. - console.log(workflowRunId) - // prints wfr_my-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) - ```ts Multiple Workflows - import { Client } from "@upstash/workflow"; +# context.api +Source: https://upstash.com/docs/workflow/steps/api - const client = new Client({ token: "" }) - const results = await client.trigger([ - { - url: "", - // other options... - }, +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 @@ -53959,910 +54097,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](http://localhost:3000/workflow/howto/local-development)) + * 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) 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 af527dbbc..d457221d9 100644 --- a/llms.txt +++ b/llms.txt @@ -983,18 +983,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) @@ -1074,6 +1062,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) From e0d8c948de53f76a2042b1332b259329d3fdb3fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Tokg=C3=B6z?= Date: Wed, 3 Jun 2026 23:37:19 +0300 Subject: [PATCH 3/5] Rename "Getting Started" to "Overview" --- workflow/getstarted.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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. -* 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. +## Key Features - ```typescript - const workflowOne = createWorkflow( - // 👇 Request Payload Type - async (context: WorkflowContext) => { + + + 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. + + - await context.sleep("wait 1 second", 1) +## Example Use Cases - // 👇 Workflow Response Type - return { message: "This is the data returned by the workflow" }; - } - ); +Here are some example real world use-cases for Upstash 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. + + + 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. + + - 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. +## How it works - ```typescript app/serve-many/[...any]/route.ts - export const { POST } = serveMany( - { - "workflow-one-route": workflowOne, - "workflow-two-route": workflowTwo, - } - ) - ``` +Upstash Workflow builds on the principle of steps. Instead of defining a single, complex piece of business logic, workflows contain multiple individual steps. - - 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`. +Each of the steps are executed by a separate request to your application, by preserving the output of previous steps. - 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. +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. - ```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" - } - ), - }); - ``` + - +## Support - - In this example, both `workflowOne` and `workflowTwo` are exposed through `serveMany`, sharing the same parent path. +Need help or have questions? We're here to support you: - You can start `workflowOne` by sending a trigger request to: - `https://your-app/serve-many/workflow-one-route`. +* Join our Discord community to ask questions and share feedback +* Open a ticket through the Intercom chatbox in the dashboard for any issue - ```typescript - import { Client } from "@upstash/workflow"; +# Cancel a Run +Source: https://upstash.com/docs/workflow/howto/cancel - const client = new Client({ token: "" }) +You can cancel a running workflow both programatically and from your Upstash Workflow console. - const { workflowRunId } = await client.trigger({ - // 👇 URL of workflow one - url: "https://your-app/serve-many/workflow-one-route" - }) - ``` +## Cancelling via console - 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`. - - +In your Upstash Workflow console, find the run you'd like to cancel and press the `Cancel Workflow` button on the right side: -# 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. +## Cancelling programatically -## Notify within 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. + -Notifies other workflows waiting for a specific event from within a workflow. +```javascript +import { Client } from "@upstash/workflow"; - -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; +const client = new Client({ token: "" }); +await client.cancel({ ids: "" }); +``` -export const { POST } = serve(async (context) => { - const { orderId, processingResult } = context.requestPayload; +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). - await context.run("process-order", async () => { - // ... - }) +You can also use the [Upstash Workflow REST API](/docs/workflow/api-reference/runs/cancel-workflow-run) to cancel a run programatically. - const { notifyResponse } = await context.notify( - "notify-processing-complete", - `order-${orderId}`, - { - orderId, - status: "completed", - result: processingResult, - completedAt: new Date().toISOString() - } - ); +# Update a Workflow +Source: https://upstash.com/docs/workflow/howto/changes -}); +Workflows are composed of multiple steps. When you modify workflow code, it's important to consider how these changes might affect in-progress workflows. + +## Issues + +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: + +```bash +HTTP status 400. Incompatible step name. Expected , got ``` -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext -from datetime import datetime +## Safe changes -app = FastAPI() -serve = Serve(app) +Updating workflow code is safe in the following cases: -@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"] +* No active workflow runs exist +* Only new steps are added to the end of the workflow - # Process the order - async def _process_order(): - return await process_order(order_id) +## Guidelines for updating workflows - result = await context.run("process-order", _process_order) +Consider the following approaches when updating your workflow code: - # 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() - } - ) +* **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. - # Log notification results - async def _log_notification(): - print(f"Notified {len(notify_response)} waiting workflows") - return notify_response +For a deeper understanding of these limitations, see our [how workflows work](/docs/workflow/basics/how) section. - await context.run("log-notification", _log_notification) -``` - +# Configure a Run +Source: https://upstash.com/docs/workflow/howto/configure -## External Notification +You can configure a workflow run when starting it. The following are the options you can configure: -You can also notify workflows from external systems using the Workflow Client: +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() -await client.notify({ - eventId: "order-completed-123", - eventData: { - orderId: "123", - status: "completed", - deliveryTime: "2 days", - trackingNumber: "TRK123456" - } +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 + } }); ``` -```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) -# 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" - } -) -``` - + +Upstash Workflow does not support step level configuration. The configuration applies to all steps executed by a workflow run. -## Lookback Functionality +If you want to specifically throttle a step, there is a workaround by splitting step to another workflow and using `context.invoke()`. + -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. +# Handle Failed Runs +Source: https://upstash.com/docs/workflow/howto/failures -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 +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. - -```typescript TypeScript -import { Client } from "@upstash/workflow"; +## Why a workflow might fail -const client = new Client({ token: "" }); +* 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. -// Trigger a workflow and get its run ID -const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/process-order", - body: { orderId: "123" } -}); +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. -// 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 -from upstash_workflow import Client +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. -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"} -) +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. -# 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 -) -``` - +## Using a `failureFunction` (recommended) -The same also applies to `context.notify` +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. ```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; - -export const { POST } = serve(async (context) => { - const { orderId, processingResult } = context.requestPayload; +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); - await context.run("process-order", async () => { - // ... - }) + // 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}`; + }, + } +); +``` - const { notifyResponse } = await context.notify( - "notify-processing-complete", - `order-${orderId}`, - { - orderId, - status: "completed", - result: processingResult, - completedAt: new Date().toISOString(), - workflowRunId: "targetWorkflowRunId" // Enables lookback - } - ); +```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: ... ``` + - -When using lookback with `workflowRunId`, the notification is targeted to a specific workflow run rather than all waiters with that event ID. - +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). -# Parallel Steps -Source: https://upstash.com/docs/workflow/features/parallel-steps +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. -Upstash Workflow supports executing multiple steps in parallel. +## Using a `failureUrl` -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. +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. -```typescript app/api/workflow/route.ts -import { serve } from "@upstash/workflow/nextjs"; -import { checkInventory, brewCoffee, printReceipt } from "@/utils"; +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. -export const { POST } = serve(async (context) => { + - // 👇 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")), - ]); +```typescript TypeScript +export const { POST } = serve( + async (context) => { + // Your workflow logic... + }, + { + failureUrl: "https:///workflow-failure", + } +); +``` -}); +```python Python +@serve.post("/api/example", failureUrl="https:///...") +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: +The callback body sent to you will be a JSON object with the following fields: - - -You can also await different step types together. For example, you can run a `context.call()` and a `context.run()` in parallel. - - - Whether executing sequentially or in parallel, you should always - await all promises in a workflow. - Leaving promises unawaited may cause unexpected behavior. - +```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 +} +``` -# Overview -Source: https://upstash.com/docs/workflow/features/retries +In Next.js you can use the following code to handle the callback: -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. +```javascript JavaScript +// pages/api/callback.js -## How Retries Work +import { verifySignature } from "@upstash/qstash/nextjs"; -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. +function handler(req, res) { + // responses from qstash are base64-encoded + const decoded = atob(req.body.body); + console.log(decoded); - + return res.status(200).end(); +} -By default, the retry count is set to **3**, and an **exponential backoff** delay strategy is used. +export default verifySignature(handler); -```javascript Default Backoff Algorithm -// n = how many times this request has been retried -delay = min(86400, e ** (2.5*n)) // in seconds +export const config = { + api: { + bodyParser: false, + }, +}; ``` -| Retry Attempt | Algorithm | Delay | -|---------------|--------------|--------| -| 1 | $$e^{2.5}$$ | 12s | -| 2 | $$e^5$$ | 2m28s | -| 3 | $$e^{7.5}$$ | 30m8s | -| 4+ | $$86400$$ | 24h | +`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. -## Configuration +## Manually Handling Failed Workflow Runs -You can configure retry behavior when starting a new workflow run. +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: -### Configure Retry Attempt Count +### [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. -You can specify how many times a step should be retried upon failure. +### [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. -```typescript Configure Retry Attempt Count -import { Client } from "@upstash/workflow"; +### [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. -const client = new Client({ token: "" }) +## Debugging failed runs -const { workflowRunId } = await client.trigger({ - url: "https:///", - retries: 3 -}) -``` +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. -### Configure Retry Delay Strategy +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. -Retry delay is the time to wait before trying again after a failure. You can define a custom retry delay strategy. + -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). +# Flow control +Source: https://upstash.com/docs/workflow/howto/flow-control -To apply a constant delay, you can simply provide a fixed value. +# Development Server +Source: https://upstash.com/docs/workflow/howto/local-development/development-server -The expression must return the delay in **milliseconds**. +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. -```typescript Configure Retry Delay Strategy -import { Client } from "@upstash/workflow"; +## Automatic dev server (recommended) -const client = new Client({ token: "" }) +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. -const { workflowRunId } = await client.trigger({ - url: "https:///", - retries: 3, - retryDelay: "(1 + retried) * 1000" -}) +```bash .env +QSTASH_DEV=true ``` -# Prevent Retries -Source: https://upstash.com/docs/workflow/features/retries/prevent-retries - -It is recommended to enable retries for workflow runs to improve reliability. +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. -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. +```typescript +// app/api/workflow/route.ts +import { serve } from "@upstash/workflow/nextjs"; -## Using `WorkflowNonRetryableError` +export const { POST } = serve(async (context) => { + await context.run("step-1", () => console.log("running locally")); +}); +``` -`WorkflowNonRetryableError` lets you explicitly fail a workflow without entering the retry cycle. +```typescript +import { Client } from "@upstash/workflow"; -When thrown, the workflow run is marked as failed, which: -* Triggers the failure function (if defined) -* Sends the workflow run to the DLQ +const client = new Client({ token: process.env.QSTASH_TOKEN ?? "" }); -```ts TypeScript highlight={7} -export const { POST } = serve<{ topic: string }>(async (context) => { - const payload = context.requestPayload +await client.trigger({ + url: "http://localhost:3000/api/workflow", +}); +``` - const isExists = await context.run("is-user-exists", () => { ... }); +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). - if (!isExists) { - throw new WorkflowNonRetryableError("The user does not exists!") - } -}) -``` +## Manual setup -## Using `context.cancel()` +If you would rather start and manage the QStash dev server yourself, follow the steps below. -You can cancel a workflow run explicitly from inside the workflow. + + + Start the development server using the QStash CLI: -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 + ```javascript + npx @upstash/qstash-cli dev + ``` - -```typescript highlight={10-11} TypeScript -export const { POST } = serve<{ orderId: string }>(async (context) => { - const { orderId } = context.requestPayload; + The QStash CLI output will look something like this: - // Check if order is still valid - const orderStatus = await context.run("check-order-status", async () => { - return await getOrderStatus(orderId); - }); + ```plaintext QStash CLI Output + Upstash QStash development server is runnning at - if (orderStatus === "cancelled") { - // Stop execution gracefully without error - await context.cancel(); - return; - } + 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 - // Continue processing if order is valid - await context.run("process-order", async () => { - return await processOrder(orderId); - }); -}); -``` + Sample cURL request: + curl -X POST http://127.0.0.1:8080/v2/publish/https://example.com -H "Authorization: Bearer eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" -```python Python -@serve.post("/graceful-cancellation") -async def graceful_cancellation(context: AsyncWorkflowContext[dict]) -> None: - order_id = context.request_payload["order_id"] + Check out documentation for more details: + https://upstash.com/docs/qstash/howto/local-development + ``` - async def _check_order_status(): - return await get_order_status(order_id) + For detailed instructions on setting up the development server, see our [QStash Local Development Guide](/docs/qstash/howto/local-development). + - # Check if order is still valid - order_status = await context.run("check-order-status", _check_order_status) + + 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. - if order_status == "cancelled": - # Stop execution gracefully without error - await context.cancel() - return + + - # Continue processing if order is valid - async def _process_order(): - return await process_order(order_id) + + Once your development server is running, update your environment variables to route QStash requests to your local server. - await context.run("process-order", _process_order) -``` - + ```env + QSTASH_URL="http://127.0.0.1:8080" + QSTASH_TOKEN="eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" + QSTASH_CURRENT_SIGNING_KEY="sig_7RvLjqfZBvP5KEUimQCE1pvpLuou" + QSTASH_NEXT_SIGNING_KEY="sig_7W3ZNbfKWk5NWwEs3U4ixuQ7fxwE" + ``` -## Using conditional execution + -You can also use guard conditions to skip certain steps and exit early, without throwing errors or canceling the workflow. + + It's all set up 🎉 -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; + Now, you can use your local address when triggering the workflow runs. - // Check if order is still valid - const orderStatus = await context.run("check-order-status", async () => { - return await getOrderStatus(orderId); - }); + ```javascript + import { Client } from "@upstash/workflow"; - if (orderStatus === "not-found") { - // Stop execution without error - return; - } + const client = Client() - // Continue processing if order is valid - await context.run("process-order", async () => { - return await processOrder(orderId); - }); - }); - ``` + 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: - ```python Python - @serve.post("/conditional-execution") - async def conditional_execution(context: AsyncWorkflowContext[dict]) -> None: - data = context.request_payload["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 - async def _validate_data(): - return validate_input_data(data) + 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: - # Validate data first - validation_result = await context.run("validate-data", _validate_data) + ```javascript + const BASE_URL = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : `http://localhost:3000` - if not validation_result["is_valid"]: - # Log the validation failure - async def _log_validation_failure(): - await log_validation_error(validation_result["errors"]) + const { workflowRunId } = await client.trigger({ + url: `${BASE_URL}/api/workflow`, + retries: 3 + }); + ``` + - await context.run("log-validation-failure", _log_validation_failure) + - # Stop execution without error - return + - # Only execute if validation passes - async def _process_valid_data(): - return await process_data(data) +# Local Tunnel +Source: https://upstash.com/docs/workflow/howto/local-development/local-tunnel - await context.run("process-valid-data", _process_valid_data) - ``` - +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. -# Sleep -Source: https://upstash.com/docs/workflow/features/sleep +The easiest way to make a local URL publically available is [ngrok](https://ngrok.com), a free tunneling service. -Upstash Workflow provides a **Sleep** feature that allows you to pause workflow execution for specified durations without consuming compute resources. +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. -This feature enables you to build time-based workflows, implement delays between steps, and create scheduled operations without the limitations of traditional serverless timeouts. +You can connect your account like this: -## How Sleep Works + + + + -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. + + + + - - **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) - +Once you have installed the ngrok CLI, add your ngrok-issued auth token like this: -## Sleep Methods +```bash Terminal +ngrok config add-authtoken +``` -Upstash Workflow provides two methods for implementing delays in your workflows: +and replace `` with your actual auth token. -### 1. context.sleep +### Start the tunnel -Pauses workflow execution for a specified duration relative to the current time. +Make your local server available publically by running the following command: - -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; +```bash +ngrok http +``` -export const { POST } = serve(async (context) => { - const { userId } = context.requestPayload; +for example, if your Next.js server is running on port `3000`, the command is: - // Send welcome email immediately - await context.run("send-welcome-email", async () => { - return await sendWelcomeEmail(userId); - }); +```bash +ngrok http 3000 +``` - // Wait for 3 days before sending follow-up - await context.sleep("wait-for-follow-up", "3d"); +The output will look something like this: - // Send follow-up email - await context.run("send-follow-up-email", async () => { - return await sendFollowUpEmail(userId); - }); -}); +```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 +### Using Init Function -app = FastAPI() -serve = Serve(app) +For middlewares that need to initialize resources (like database connections or external clients), use the `init` pattern: -@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"] +```typescript +import { WorkflowMiddleware } from "@upstash/workflow"; - # Calculate the scheduled time - scheduled_date = datetime.fromisoformat(scheduled_time) +const databaseMiddleware = new WorkflowMiddleware({ + name: "database-logger", + init: async () => { + // Initialize your resources + const db = await connectToDatabase(); - # Wait until the scheduled time - await context.sleep_until("wait-until-scheduled", scheduled_date) + // 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 }); + } + }; + } +}); +``` - # Execute the scheduled task - async def _execute_task(): - return await execute_task(user_id) +## Event Types - await context.run("execute-scheduled-task", _execute_task) -``` - +### Lifecycle Events -For `context.sleepUntil`, you can use: + + Called when a workflow run begins. -* `Date` objects (JavaScript/TypeScript) -* Unix timestamps (Python) -* ISO string dates + **Parameters:** + * `context`: The workflow context - - Sleep operations have a precision of approximately 1 second. Very short delays (less than 1 second) may not be exact. - + ```typescript + runStarted: async ({ context }) => { + // Handle run start + } + ``` + -The sleep feature in Upstash Workflow provides a powerful way to create time-based, reliable workflows without the limitations of traditional serverless timeouts. + + Called before each step executes. - By leveraging this feature, you can build sophisticated business logic that spans hours, days, or even months while maintaining cost efficiency and reliability. + **Parameters:** + * `context`: The workflow context + * `stepName`: Name of the step about to execute -# Wait -Source: https://upstash.com/docs/workflow/features/wait + ```typescript + beforeExecution: async ({ context, stepName }) => { + // Handle step start + } + ``` + -You can pause a workflow run with the `waitForEvent` step. An event is uniquely identified by event ID. + + Called after each step completes. -The workflow will resume when the matching event is published. + **Parameters:** + * `context`: The workflow context + * `stepName`: Name of the completed step + * `result`: The result returned by the step -`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). + ```typescript + afterExecution: async ({ context, stepName, result }) => { + // Handle step completion + } + ``` + - -If no timeout is specified, the default is **7 days**. - + + Called when the entire workflow run finishes. - -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; + **Parameters:** + * `context`: The workflow context + * `result`: The final result of the workflow -export const { POST } = serve(async (context) => { - const { orderId, userEmail } = context.requestPayload; + ```typescript + runCompleted: async ({ context, result }) => { + // Handle run completion + } + ``` + - // Wait for order processing completion - const { eventData, timeout } = await context.waitForEvent( - "wait-for-order-processing", - `order-${orderId}`, - { - timeout: "1d" // 1 day timeout - } - ); +### Debug Events - if (timeout) { - // Handle timeout scenario - await context.run("handle-timeout", async () => { - return await handleOrderTimeout(orderId, userEmail); - }); - return; + + Called when an error occurs. + + **Parameters:** + * `workflowRunId`: The workflow run ID (optional) + * `error`: The error object + + ```typescript + onError: async ({ workflowRunId, error }) => { + // Handle error } + ``` + -}); -``` + + Called when a warning is logged. -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext + **Parameters:** + * `workflowRunId`: The workflow run ID (optional) + * `warning`: The warning message -app = FastAPI() -serve = Serve(app) + ```typescript + onWarning: async ({ workflowRunId, warning }) => { + // Handle warning + } + ``` + -@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"] + + Called when an info message is logged. - # Send order processing request - async def _request_order_processing(): - return await request_order_processing(order_id) + **Parameters:** + * `workflowRunId`: The workflow run ID (optional) + * `info`: The info message - await context.run("request-order-processing", _request_order_processing) + ```typescript + onInfo: async ({ workflowRunId, info }) => { + // Handle info + } + ``` + - # Wait for order processing completion - result = await context.wait_for_event( - "wait-for-order-processing", - f"order-{order_id}", - timeout="10m" # 10 minutes timeout - ) +## Examples - if result["timeout"]: - # Handle timeout scenario - async def _handle_timeout(): - return await handle_order_timeout(order_id, user_email) +### Error Tracking Middleware - await context.run("handle-timeout", _handle_timeout) - return +Send errors to an external monitoring service: - # Process the completed order - async def _process_completed_order(): - return await process_completed_order(order_id, result["event_data"]) +```typescript +import { WorkflowMiddleware } from "@upstash/workflow"; - await context.run("process-completed-order", _process_completed_order) +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() + }) + }); + } + } +}); ``` - -# Overview -Source: https://upstash.com/docs/workflow/features/wait-for-event +### Multiple Middlewares -Wait for Event feature that allows you to pause workflow execution until an external event occurs. +You can use multiple middlewares together: -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 { serve } from "@upstash/workflow/nextjs"; +import { loggingMiddleware } from "@upstash/workflow"; -## How Wait for Event Works +export const { POST } = serve( + async (context) => { + // Your workflow logic + }, + { + middlewares: [ + loggingMiddleware, + errorTrackingMiddleware, + performanceMiddleware + ] + } +); +``` -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. +Middlewares are executed in the order they're provided in the array. -Each waiter has a timeout duration to wait for the event and then fires automatically. +# Migration Guide +Source: https://upstash.com/docs/workflow/howto/migrations - - 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) - +This guide covers migration between different versions of Upstash Workflow. -## Race Condition Between Wait and Notify + + + 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. -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. + ## Agents API → Separate Package -### Solutions + The Agents API has been moved to a separate package to remove the AI SDK dependency from the core workflow package. -There are three ways to handle race conditions: + ### Migration Steps -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) + 1. Install the new package: -#### Option 1: Lookback with workflowRunId + ```bash + npm install @upstash/workflow-agents + ``` -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`: + 2. Update your imports: - -```typescript TypeScript -import { Client } from "@upstash/workflow"; + ```typescript + // Old + import { serve } from "@upstash/workflow/nextjs"; -const client = new Client({ token: "" }); + 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({ ... }); + }); -// Trigger a workflow -const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/process-order", - body: { orderId: "123" } -}); + // New + import { serve } from "@upstash/workflow/nextjs"; + import { agentWorkflow } from "@upstash/workflow-agents"; -// Immediately notify with lookback - no race condition! -await client.notify({ - eventId: "payment-verified", - eventData: { verified: true }, - workflowRunId: workflowRunId, // Enables lookback -}); -``` - + export const { POST } = serve(async (context) => { + const agents = agentWorkflow(context) -#### Option 2: Use Webhooks + const model = agents.openai('gpt-3.5-turbo'); + const agent = agents.agent({ ... }); + const task = agents.task({ ... }); + }); + ``` -[Webhooks](/docs/workflow/features/webhooks) have built-in lookback and are safer against timing issues for general event handling. + See [Agents documentation](/docs/workflow/agents/overview) for more details. -#### Option 3: Check and Retry + ## Removed `keepTriggerConfig` and `useFailureFunction` -Alternatively, you can check the response of the `notify` operation and retry if needed: + These parameters are no longer needed in `client.trigger()` as both are now `true` by default. - -```typescript TypeScript -import { Client } from "@upstash/workflow"; + ### Migration Steps -const client = new Client({ token: "" }); + Simply remove these parameters from your trigger calls: -const result = await client.notify({ - eventId, - eventData -}); + ```typescript + // Old + const { workflowRunId } = await client.trigger({ + url: "https://your-app.com/api/workflow", + retries: 3, + keepTriggerConfig: true, + useFailureFunction: true + }); -// Check if any workflows were notified -if (result.waiters && result.waiters.length > 0) { - console.log(`Notified ${result.waiters.length} workflows`); - return result; -} + // New + const { workflowRunId } = await client.trigger({ + url: "https://your-app.com/api/workflow", + retries: 3 + }); + ``` -// 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)); + Configuration passed to `trigger()` now automatically applies to the entire workflow. -return await client.notify({ - eventId, - eventData -}); -``` - + ## Configuration Moved from `serve` to `trigger` -## Selecting an Event ID + The `retries`, `flowControl`, `retryDelay`, and `failureUrl` options have been removed from `serve()` and should now be passed in `client.trigger()`. -When a workflow run waits on an event ID, it's appended to a list of waiters for the event ID. + ### Migration Steps -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. + Move configuration from serve options to trigger: -# Webhooks -Source: https://upstash.com/docs/workflow/features/webhooks + ```typescript + // Old + export const { POST } = serve( + async (context) => { ... }, + { + retries: 3, + retryDelay: "1000 * (1 + retried)", + flowControl: { key: "my-key", rate: 10 } + } + ); -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. + // Trigger call + await client.trigger({ url: "..." }); -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 + // New + export const { POST } = serve( + async (context) => { ... } + // No configuration here anymore + ); -## How Webhooks Work + // Configuration in trigger call + await client.trigger({ + url: "...", + retries: 3, + retryDelay: "1000 * (1 + retried)", + flowControl: { key: "my-key", rate: 10 } + }); + ``` -When you use webhooks in your workflow, Upstash Workflow: + This change makes it easier to configure different behavior for different workflow runs of the same endpoint. -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 + + In the Python SDK, these options remain in the `serve` decorator as they were before. + -This happens without keeping your serverless function running, making it cost-effective for long-running integrations. + ## Removed `stringifyBody` from `context.call` and `context.invoke` -## Examples + The `stringifyBody` parameter has been removed. The `body` parameter now expects a string. -### Basic Usage + ### Migration Steps -```typescript -import { serve } from "@upstash/workflow/nextjs"; + Update your call and invoke methods to use `JSON.stringify()`: -export const { POST } = serve(async (context) => { - // Step 1: Create webhook - const webhook = await context.createWebhook("create webhook"); + ```typescript + // Old + const result = await context.call("call-api", { + url: "https://api.example.com/endpoint", + method: "POST", + body: { key: "value" }, + stringifyBody: true + }); - // 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, - }), - }); + // New + const result = await context.call("call-api", { + url: "https://api.example.com/endpoint", + method: "POST", + body: JSON.stringify({ key: "value" }) + }); + ``` - // Step 3: Wait for the webhook to be called - const webhookResponse = await context.waitForWebhook( - "wait for webhook", - webhook, - "30s" // timeout - ); + The same applies to `context.invoke()`: - 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 - } -}); -``` + ```typescript + // Old + await context.invoke("invoke-workflow", { + workflow: otherWorkflow, + body: { key: "value" }, + stringifyBody: true + }); -### Waiting for Multiple Calls + // New + await context.invoke("invoke-workflow", { + workflow: otherWorkflow, + body: JSON.stringify({ key: "value" }) + }); + ``` -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: + ## Logger → Middleware System -```typescript -while (true) { - const webhookResponse = await context.waitForWebhook( - `wait for progress update ${stepCount}`, - webhook, - "5m" // 5 minute timeout between updates - ); + The logging system has been replaced with a more flexible middleware system. - 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()); + ### Migration Steps - if (request.headers.get("x-task-finished") === "true") { - console.log("Task finished, exiting loop"); - break; - } - } -} -``` + Replace the old logger with the new middleware: -## Race Condition Safety + ```typescript + // Old + // Logging was automatic or controlled via verbose option + export const { POST } = serve( + async (context) => { ... }, + { verbose: true } + ); -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. + // New + import { loggingMiddleware } from "@upstash/workflow"; -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. + export const { POST } = serve( + async (context) => { ... }, + { + middlewares: [loggingMiddleware] + } + ); + ``` -## Comparison with Wait for Event + You can also create custom middlewares for more control. See [Middlewares documentation](/docs/workflow/howto/middlewares) for details. -Webhooks and [Wait for Event](/docs/workflow/features/wait-for-event) serve similar purposes but with different approaches: + ## Removed `onStepFinish` -| 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 | + The `onStepFinish` callback has been removed. Use middlewares instead. -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. + ### Migration Steps -## API Reference + Replace `onStepFinish` with a custom middleware: - - - Create a unique webhook URL for external services to call - - - Wait for the webhook to be called or timeout - - + ```typescript + // Old + export const { POST } = serve( + async (context) => { ... }, + { + onStepFinish: (stepName, result) => { + console.log(`Step ${stepName} finished with:`, result); + } + } + ); -# Getting Started -Source: https://upstash.com/docs/workflow/getstarted + // New + import { WorkflowMiddleware } from "@upstash/workflow"; -## Overview + const stepFinishMiddleware = new WorkflowMiddleware({ + name: "step-finish", + callbacks: { + afterExecution: async ({ stepName, result }) => { + console.log(`Step ${stepName} finished with:`, result); + } + } + }); -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. + export const { POST } = serve( + async (context) => { ... }, + { + middlewares: [stepFinishMiddleware] + } + ); + ``` - + See [Middlewares documentation](/docs/workflow/howto/middlewares) for more details. -## Quickstarts + -Upstash Workflow supports Next.js, Cloudflare Workers and [many other frameworks](/docs/workflow/quickstarts/platforms) in TypeScript and Python. + + 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. - - - 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 - - + 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. -## Key Features + ### Install `@upstash/workflow` - - - 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. - - + First, we will need to install the new package with: -## Example Use Cases + + + ```bash + npm install @upstash/workflow + ``` + + + ```bash + pnpm install @upstash/workflow + ``` + + + ```bash + bun add @upstash/workflow + ``` + + -Here are some example real world use-cases for Upstash Workflow: + If you were using `@upstash/qstash` only for workflow, you can uninstall it from your project. - - - 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. - - + ### Serve methods -## How it works + You will need to change the imports from `@upstash/qstash` to @upstash/workflow: -Upstash Workflow builds on the principle of steps. Instead of defining a single, complex piece of business logic, workflows contain multiple individual steps. + ```ts + // old + import { serve } from "@upstash/qstash/nextjs" -Each of the steps are executed by a separate request to your application, by preserving the output of previous steps. + // new + import { serve } from "@upstash/workflow/nextjs" + ``` -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. + We have updated what our `serve` methods return. We made this change to make it + easier to extend the API in the future. - + For instance, Next.js method changed like this: -## Support + ```javascript + // old + export const POST = serve(...); -Need help or have questions? We're here to support you: + // new + export const { POST } = serve(...); + ``` -* Join our Discord community to ask questions and share feedback -* Open a ticket through the Intercom chatbox in the dashboard for any issue + 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. -# Cancel a Run -Source: https://upstash.com/docs/workflow/howto/cancel + 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`. -You can cancel a running workflow both programatically and from your Upstash Workflow console. + ### Updating `context.call` -## Cancelling via console + 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: -In your Upstash Workflow console, find the run you'd like to cancel and press the `Cancel Workflow` button on the right side: + ```javascript + // old + const result = await context.call("call step", "", "POST", ...) - + // new + const { + status, // response status + headers, // response headers + body // response body + } = await context.call("call step", { + url: "", + method: "POST", + ... + }) + ``` -## Cancelling programatically + 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). - - 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. - + 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. -```javascript -import { Client } from "@upstash/workflow"; + 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. -const client = new Client({ token: "" }); -await client.cancel({ ids: "" }); -``` + ### Renaming Errors -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). + The errors in Workflow were renamed from `QStashWorkflowError` and `QStashWorkflowAbort` to `WorkflowError` and `WorkflowAbort`. -You can also use the [Upstash Workflow REST API](/docs/workflow/api-reference/runs/cancel-workflow-run) to cancel a run programatically. + + -# Update a Workflow -Source: https://upstash.com/docs/workflow/howto/changes +# Select a Region +Source: https://upstash.com/docs/workflow/howto/multi-region -Workflows are composed of multiple steps. When you modify workflow code, it's important to consider how these changes might affect in-progress workflows. +## Overview -## Issues +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. -You cannot change the step order of an existing workflow. +## Regional URLs -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: +* **EU Region**: `https://qstash-eu-central-1.upstash.io`, or `https://qstash.upstash.io` +* **US Region**: `https://qstash-us-east-1.upstash.io` -```bash -HTTP status 400. Incompatible step name. Expected , got -``` +## Key Concepts -## Safe changes +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 -Updating workflow code is safe in the following cases: +### Migration Between Regions -* No active workflow runs exist -* Only new steps are added to the end of the workflow +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. -## Guidelines for updating workflows +You can migrate your Workflow resources from one region to another using the Upstash Console: -Consider the following approaches when updating your workflow code: +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 -* **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. + -For a deeper understanding of these limitations, see our [how workflows work](/docs/workflow/basics/how) section. +The migration tool will: +* Help you set up migration-mode environment variables +* Copy and update your QStash resources (schedules, url groups, queues) -# Configure a Run -Source: https://upstash.com/docs/workflow/howto/configure +Your workflow logs or DLQ aren't part of the migration. They will remain in the old region. -You can configure a workflow run when starting it. The following are the options you can configure: + +After migration, your app will be able to handle requests from both regions simultaneously to ensure a smooth transition. + -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. +## Operating Modes -You can pass these configuration options when starting a workflow run: +Workflow SDK supports two modes of operation: -```typescript -import { Client } from "@upstash/workflow"; +### Single-Region Mode (Default) -const client = Client() +When `QSTASH_REGION` environment variable is **not set**, the SDK operates in single-region mode: -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 - } -}); +* 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 + +```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; +} - ```javascript - const BASE_URL = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : `http://localhost:3000` +export function useWorkflow() { + const [workflowRunId, setWorkflowRunId] = useState(null); + const [steps, setSteps] = useState([]); + const [isRunFinished, setIsRunFinished] = useState(false); - const { workflowRunId } = await client.trigger({ - url: `${BASE_URL}/api/workflow`, - retries: 3 - }); - ``` - + useRealtime({ + enabled: Boolean(workflowRunId), + channels: workflowRunId ? [workflowRunId] : [], + events: ["workflow.stepFinish", "workflow.runFinish"], + onData({ event, data }) { + if (event === "workflow.stepFinish") { + setSteps((prev) => [...prev, data]); + } - + if (event === "workflow.runFinish") { + setIsRunFinished(true); + } + }, + }); - + const trigger = () => { + setSteps([]); + setIsRunFinished(false); -# Local Tunnel -Source: https://upstash.com/docs/workflow/howto/local-development/local-tunnel + const response = await fetch("/api/trigger", { + method: "POST", + }); -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. + const data = await response.json(); + setWorkflowRunId(data.workflowRunId); + }; -The easiest way to make a local URL publically available is [ngrok](https://ngrok.com), a free tunneling service. + return { + trigger, + workflowRunId, + steps, + isRunFinished, + }; +} +``` -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. +**Key features:** -You can connect your account like this: +* 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 - - - - +### 2. Use the Hook in Your Component - - - - +```tsx +"use client"; -Once you have installed the ngrok CLI, add your ngrok-issued auth token like this: +import { useWorkflow } from "@/hooks/useWorkflow"; -```bash Terminal -ngrok config add-authtoken -``` +export default function WorkflowPage() { + const { trigger, steps, isRunFinished } = useWorkflow(); -and replace `` with your actual auth token. + return ( +
+ -### Start the tunnel + {isRunFinished &&

✅ Workflow Finished!

} -Make your local server available publically by running the following command: +

Workflow Steps:

-```bash -ngrok http + {steps.map((step, index) => ( +
+ {step.stepName} + {Boolean(step.result) && : {JSON.stringify(step.result)}} +
+ ))} +
+ ); +} ``` -for example, if your Next.js server is running on port `3000`, the command is: +## How It All Works Together -```bash -ngrok http 3000 -``` +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 -The output will look something like this: +## Full Example -```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 -``` +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 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). +## Next Steps -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. +* 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) - - 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`. - +# Human-in-the-Loop +Source: https://upstash.com/docs/workflow/howto/realtime/human-in-the-loop -# Middlewares -Source: https://upstash.com/docs/workflow/howto/middlewares +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. -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. +This guide shows you how to implement a human-in-the-loop workflow pattern with real-time updates using Upstash Workflow and Upstash Realtime. -## Overview +## How It Works -A middleware can hook into: -* **Lifecycle Events**: Run started, run completed, before/after step execution -* **Debug Events**: Errors, warnings, and info messages +In a human-in-the-loop workflow: -## Built-in Middleware +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 -Upstash Workflow provides a built-in logging middleware that you can use out of the box: +## Prerequisites -```typescript -import { serve } from "@upstash/workflow/nextjs"; -import { loggingMiddleware } from "@upstash/workflow"; +* 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) -export const { POST } = serve( - async (context) => { - await context.run("step-1", () => { - return "Hello World"; - }); +## Event Types + +For human-in-the-loop workflows, extend your schema in `lib/realtime.ts` with these additional event types: + +```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}`); + const channel = realtime.channel(context.workflowRunId); + + // 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 }) => { - 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); + // 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, + }); }, - onWarning: async ({ workflowRunId, warning }) => { - console.warn(`Warning in ${workflowRunId}:`, warning); + runCompleted: async ({ context }) => { + const channel = realtime.channel(context.workflowRunId); + await channel.emit("workflow.runFinish", {}); }, - 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(); +**Key points:** - // 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 }); - } - }; - } -}); -``` +* 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 -## Event Types +## Building the Workflow -### Lifecycle Events +### 1. Create the Workflow Endpoint - - Called when a workflow run begins. +Create your workflow at `app/api/workflow/human-in-loop/route.ts`: - **Parameters:** - * `context`: The workflow context +```typescript title="app/api/workflow/human-in-loop/route.ts" +import { serve } from "@upstash/workflow/nextjs"; +import { realtimeMiddleware } from "@/lib/middleware"; - ```typescript - runStarted: async ({ context }) => { - // Handle run start - } - ``` - +type WorkflowPayload = { + userId: string; + action: string; +}; - - Called before each step executes. +export const { POST } = serve( + async (context) => { + const { userId, action } = context.requestPayload; - **Parameters:** - * `context`: The workflow context - * `stepName`: Name of the step about to execute + // Step 1: Initial Processing + await context.run("initial-processing", async () => { + // Your processing logic + return { + preprocessed: true, + userId, + action, + requiresApproval: true, + }; + }); - ```typescript - beforeExecution: async ({ context, stepName }) => { - // Handle step start - } - ``` - + // Step 2: Wait for Human Approval + const eventId = `approval-${context.workflowRunId}`; - - Called after each step completes. + const { eventData, timeout } = await context.waitForEvent<{ + approved: boolean; + }>("wait-for-approval", eventId, { timeout: "5m" }); - **Parameters:** - * `context`: The workflow context - * `stepName`: Name of the completed step - * `result`: The result returned by the step + // Handle timeout + if (timeout) { + return { success: false, reason: "timeout" }; + } - ```typescript - afterExecution: async ({ context, stepName, result }) => { - // Handle step completion - } - ``` - + const status = eventData.approved ? "approved" : "rejected"; - - Called when the entire workflow run finishes. + // Step 3: Process based on approval + await context.run(`process-${status}`, async () => { + return { + status, + processedAt: Date.now(), + action, + userId, + }; + }); - **Parameters:** - * `context`: The workflow context - * `result`: The final result of the workflow + // Step 4: Finalize (only if approved) + if (eventData.approved) { + // Additional steps... + } - ```typescript - runCompleted: async ({ context, result }) => { - // Handle run completion + return { + success: true, + approved: eventData.approved, + workflowRunId: context.workflowRunId, + }; + }, + { + middlewares: [realtimeMiddleware], } - ``` - - -### Debug Events - - - Called when an error occurs. +); +``` - **Parameters:** - * `workflowRunId`: The workflow run ID (optional) - * `error`: The error object +**Key patterns:** - ```typescript - onError: async ({ workflowRunId, error }) => { - // Handle error - } - ``` - +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 a warning is logged. +### 2. Create the Notify Endpoint - **Parameters:** - * `workflowRunId`: The workflow run ID (optional) - * `warning`: The warning message +Create an endpoint at `app/api/notify/route.ts` to handle user input: - ```typescript - onWarning: async ({ workflowRunId, warning }) => { - // Handle warning - } - ``` - +```typescript +import { Client } from "@upstash/workflow"; +import { NextRequest, NextResponse } from "next/server"; - - Called when an info message is logged. +const workflowClient = new Client({ + baseUrl: process.env.QSTASH_URL!, + token: process.env.QSTASH_TOKEN!, +}); - **Parameters:** - * `workflowRunId`: The workflow run ID (optional) - * `info`: The info message +export async function POST(request: NextRequest) { + const body = await request.json(); + const { eventId, eventData } = body; - ```typescript - onInfo: async ({ workflowRunId, info }) => { - // Handle info + if (!eventId) { + return NextResponse.json( + { success: false, error: "eventId is required" }, + { status: 400 } + ); } - ``` - -## Examples + // Notify the workflow + await workflowClient.notify({ + eventId, + eventData, + }); -### Error Tracking Middleware + return NextResponse.json({ success: true }); +} +``` -Send errors to an external monitoring service: + +**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 { WorkflowMiddleware } from "@upstash/workflow"; - -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() - }) - }); - } - } +await workflowClient.notify({ + eventId, + eventData, + workflowRunId: "wfr_abc123", // Ensures notification is delivered even if sent before waitForEvent }); ``` -### Multiple Middlewares +Learn more in the [notify documentation](/docs/workflow/basics/client/notify). + -You can use multiple middlewares together: +## Building the Frontend -```typescript -import { serve } from "@upstash/workflow/nextjs"; -import { loggingMiddleware } from "@upstash/workflow"; +### 1. Extend the Custom Hook -export const { POST } = serve( - async (context) => { - // Your workflow logic - }, - { - middlewares: [ - loggingMiddleware, - errorTrackingMiddleware, - performanceMiddleware - ] - } -); -``` +Extend your hook from the basic example to handle waiting states: -Middlewares are executed in the order they're provided in the array. +```typescript +"use client"; -# Migration Guide -Source: https://upstash.com/docs/workflow/howto/migrations +import { useRealtime } from "@/lib/realtime-client"; +import { useState, useCallback } from "react"; -This guide covers migration between different versions of Upstash Workflow. +interface WorkflowStep { + stepName: string; + result?: unknown; +} - - - 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. +interface WaitingState { + eventId: string; + message: string; +} - ## Agents API → Separate Package +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); - The Agents API has been moved to a separate package to remove the AI SDK dependency from the core workflow package. + 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, + }); + } + }, + }); - ### Migration Steps + const trigger = useCallback(async () => { + setIsTriggering(true); + setSteps([]); + setWaitingState(null); + setIsRunFinished(false); - 1. Install the new package: + const response = await fetch("/api/trigger", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ workflowType: "human-in-loop" }), + }); - ```bash - npm install @upstash/workflow-agents - ``` + const data = await response.json(); + setWorkflowRunId(data.workflowRunId); + setIsTriggering(false); + }, []); - 2. Update your imports: + const continueWorkflow = useCallback( + async (data: { approved: boolean }) => { + if (!waitingState) { + throw new Error("No workflow waiting for input"); + } - ```typescript - // Old - import { serve } from "@upstash/workflow/nextjs"; + const response = await fetch("/api/notify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + eventId: waitingState.eventId, + eventData: data, + }), + }); - 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({ ... }); - }); + if (!response.ok) { + throw new Error("Failed to notify workflow"); + } - // New - import { serve } from "@upstash/workflow/nextjs"; - import { agentWorkflow } from "@upstash/workflow-agents"; + // The waiting state will be cleared when we receive inputResolved event + }, + [waitingState] + ); - export const { POST } = serve(async (context) => { - const agents = agentWorkflow(context) + return { + trigger, + continueWorkflow, + isTriggering, + workflowRunId, + steps, + waitingState, + isRunFinished, + }; +} +``` - const model = agents.openai('gpt-3.5-turbo'); - const agent = agents.agent({ ... }); - const task = agents.task({ ... }); - }); - ``` +**Key additions:** - See [Agents documentation](/docs/workflow/agents/overview) for more details. +* **`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 - ## Removed `keepTriggerConfig` and `useFailureFunction` +### 2. Use the Hook with Approval UI - These parameters are no longer needed in `client.trigger()` as both are now `true` by default. +```typescript +"use client"; - ### Migration Steps +import { useWorkflowWithRealtime } from "@/hooks/useWorkflowWithRealtime"; - Simply remove these parameters from your trigger calls: +export default function WorkflowPage() { + const { + trigger, + isTriggering, + steps, + isRunFinished, + waitingState, + continueWorkflow, + } = useWorkflowWithRealtime(); - ```typescript - // Old - const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/workflow", - retries: 3, - keepTriggerConfig: true, - useFailureFunction: true - }); + return ( +
+ - // New - const { workflowRunId } = await client.trigger({ - url: "https://your-app.com/api/workflow", - retries: 3 - }); - ``` + {isRunFinished && ( +

✅ Workflow Finished!

+ )} - Configuration passed to `trigger()` now automatically applies to the entire workflow. + {/* Show workflow steps */} +

Workflow Steps:

+
+ {steps.map((step, index) => ( +
+ {step.stepName} + {Boolean(step.result) && ( + : {JSON.stringify(step.result)} + )} +
+ ))} +
- ## Configuration Moved from `serve` to `trigger` + {/* Show approval UI when waiting for input */} + {waitingState && ( +
+

{waitingState.message}

+

+ +

+

+ +

+
+ )} +
+ ); +} +``` - The `retries`, `flowControl`, `retryDelay`, and `failureUrl` options have been removed from `serve()` and should now be passed in `client.trigger()`. +## How the Pattern Works - ### Migration Steps +### Timeline of Events - Move configuration from serve options to trigger: +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!" - ```typescript - // Old - export const { POST } = serve( - async (context) => { ... }, - { - retries: 3, - retryDelay: "1000 * (1 + retried)", - flowControl: { key: "my-key", rate: 10 } - } - ); +## Benefits - // Trigger call - await client.trigger({ url: "..." }); +* **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 - // New - export const { POST } = serve( - async (context) => { ... } - // No configuration here anymore - ); +## Full Example - // Configuration in trigger call - await client.trigger({ - url: "...", - retries: 3, - retryDelay: "1000 * (1 + retried)", - flowControl: { key: "my-key", rate: 10 } - }); - ``` +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). - This change makes it easier to configure different behavior for different workflow runs of the same endpoint. +## Next Steps - - In the Python SDK, these options remain in the `serve` decorator as they were before. - +* 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) - ## Removed `stringifyBody` from `context.call` and `context.invoke` +# Redact Private Data +Source: https://upstash.com/docs/workflow/howto/redact-fields - The `stringifyBody` parameter has been removed. The `body` parameter now expects a string. +Workflow runs can contain private data that you don't want visible in the Upstash Console or API responses. - ### Migration Steps +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. - Update your call and invoke methods to use `JSON.stringify()`: +To redact fields, pass the `redact` option when triggering a workflow run. - ```typescript - // Old - const result = await context.call("call-api", { - url: "https://api.example.com/endpoint", - method: "POST", - body: { key: "value" }, - stringifyBody: true - }); +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]`) | - // New - const result = await context.call("call-api", { - url: "https://api.example.com/endpoint", - method: "POST", - body: JSON.stringify({ key: "value" }) - }); - ``` + +Redaction is one-way. Once a field is redacted, the original value cannot be retrieved from the API or dashboard. + - The same applies to `context.invoke()`: + +```typescript TypeScript +import { Client } from "@upstash/workflow"; - ```typescript - // Old - await context.invoke("invoke-workflow", { - workflow: otherWorkflow, - body: { key: "value" }, - stringifyBody: true - }); +const client = new Client({ token: "" }); - // New - await context.invoke("invoke-workflow", { - workflow: otherWorkflow, - body: JSON.stringify({ key: "value" }) - }); - ``` +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 + }, +}); +``` - ## Logger → Middleware System +```python Python +from upstash_workflow import Client - The logging system has been replaced with a more flexible middleware system. +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 + }, +) +``` - ### Migration Steps +```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' +``` + - Replace the old logger with the new middleware: + - ```typescript - // Old - // Logging was automatic or controlled via verbose option - export const { POST } = serve( - async (context) => { ... }, - { verbose: true } - ); +Redaction is configured per workflow run, so you can redact different fields for different runs. - // New - import { loggingMiddleware } from "@upstash/workflow"; +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. - export const { POST } = serve( - async (context) => { ... }, - { - middlewares: [loggingMiddleware] - } - ); - ``` +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. - You can also create custom middlewares for more control. See [Middlewares documentation](/docs/workflow/howto/middlewares) for details. +# Schedule a Workflow +Source: https://upstash.com/docs/workflow/howto/schedule - ## Removed `onStepFinish` +You can schedule a workflow to run periodically using a cron definition. - The `onStepFinish` callback has been removed. Use middlewares instead. +For this feature, you would need to use Upstash QStash's Schedules feature. - ### Migration Steps +## Scheduling a workflow - Replace `onStepFinish` with a custom middleware: +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: - ```typescript - // Old - export const { POST } = serve( - async (context) => { ... }, - { - onStepFinish: (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`: - // New - import { WorkflowMiddleware } from "@upstash/workflow"; + - const stepFinishMiddleware = new WorkflowMiddleware({ - name: "step-finish", - callbacks: { - afterExecution: async ({ stepName, result }) => { - console.log(`Step ${stepName} finished with:`, result); - } - } - }); +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`: - export const { POST } = serve( - async (context) => { ... }, - { - middlewares: [stepFinishMiddleware] - } - ); - ``` + - See [Middlewares documentation](/docs/workflow/howto/middlewares) for more details. +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). -
+## Programmatically 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. +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. - 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. +Let's create a user-specific schedule, sending a first report to each user exactly 7 days after they signed up: - ### Install `@upstash/workflow` + - First, we will need to install the new package with: +```typescript api/sign-up/route.ts +import { signUp } from "@/utils/auth-utils"; +import { Client } from "@upstash/qstash"; - - - ```bash - npm install @upstash/workflow - ``` - - - ```bash - pnpm install @upstash/workflow - ``` - - - ```bash - bun add @upstash/workflow - ``` - - +const client = new Client({ token: process.env.QSTASH_TOKEN! }); - If you were using `@upstash/qstash` only for workflow, you can uninstall it from your project. +export async function POST(request: Request) { + const userData: UserData = await request.json(); - ### Serve methods + // 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, + }); - You will need to change the imports from `@upstash/qstash` to @upstash/workflow: + return NextResponse.json( + { success: true, message: "User registered and summary scheduled" }, + { status: 201 } + ); +} +``` - ```ts - // old - import { serve } from "@upstash/qstash/nextjs" +```python main.py +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from qstash import AsyncQStash +from datetime import datetime, timedelta - // new - import { serve } from "@upstash/workflow/nextjs" - ``` +app = FastAPI() - We have updated what our `serve` methods return. We made this change to make it - easier to extend the API in the future. +client = AsyncQStash("") - For instance, Next.js method changed like this: +@app.post("/api/sign-up") +async def sign_up(request: Request): + user_data = await request.json() - ```javascript - // old - export const POST = serve(...); + # Simulate user registration + user = await sign_up(user_data) - // new - export const { POST } = serve(...); - ``` + # Calculate the date for the first summary (7 days from now) + first_summary_date = datetime.now() + timedelta(days=7) - 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. + # 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}" - 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`. + # 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, + ) - ### Updating `context.call` + return JSONResponse( + content={"success": True, "message": "User registered and summary scheduled"}, + status_code=201, + ) - 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 - // old - const result = await context.call("call step", "", "POST", ...) + - // new - const { - status, // response status - headers, // response headers - body // response body - } = await context.call("call step", { - url: "", - method: "POST", - ... - }) - ``` +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. - 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). + + When creating a per-user schedule, pass a unique `scheduleId` to identify the schedule for better management and observability. + - 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. +# Secure a Workflow +Source: https://upstash.com/docs/workflow/howto/security - 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. +To prevent unauthorized access to your workflow endpoint, you can add an authorization layer. +Upstash Workflow supports two approaches: - ### Renaming Errors +* **Built-in request verification** (recommended) +* **Custom authorization method** - The errors in Workflow were renamed from `QStashWorkflowError` and `QStashWorkflowAbort` to `WorkflowError` and `WorkflowAbort`. +### 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. -# Select a Region -Source: https://upstash.com/docs/workflow/howto/multi-region +How it works: -## Overview +1. Upstash Workflow automatically adds the `Upstash-Signature` header to every request. + This signature is generated using your signing keys. -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. +2. When this mechanism is enabled, the SDK verifies that the signature is valid before processing the request. -## Regional URLs +This ensures that only requests originating from Upstash Workflow are processed. -* **EU Region**: `https://qstash-eu-central-1.upstash.io`, or `https://qstash.upstash.io` -* **US Region**: `https://qstash-us-east-1.upstash.io` +To enable this verification, set the following environment variables in your application: -## Key Concepts +```bash .env +QSTASH_CURRENT_SIGNING_KEY=xxxxxxxxx +QSTASH_NEXT_SIGNING_KEY=xxxxxxxxx +``` -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 +You can find the values in Upstash Workflow dashboard. -### Migration Between Regions + -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. + + For edge cases where environment variables cannot be used, you can explicitly create and pass a `Receiver` object to verify request signatures: -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 + ```typescript TypeScript + import { Receiver } from "@upstash/qstash"; + import { serve } from "@upstash/workflow/nextjs"; - + export const { POST } = serve( + async (context) => { ... }, + { + receiver: new Receiver({ + currentSigningKey: "", + nextSigningKey: "", + }), + } + ); + ``` -The migration tool will: -* Help you set up migration-mode environment variables -* Copy and update your QStash resources (schedules, url groups, queues) + ```python Python + from qstash import Receiver -Your workflow logs or DLQ aren't part of the migration. They will remain in the old region. + @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: + ... - -After migration, your app will be able to handle requests from both regions simultaneously to ensure a smooth transition. + ``` + -## Operating Modes - -Workflow SDK supports two modes of operation: +## Custom Authorization Method -### Single-Region Mode (Default) +You can implement your own authorization mechanism with Upstash Workflow. -When `QSTASH_REGION` environment variable is **not set**, the SDK operates in single-region mode: +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. -* 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 + -```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" -``` + ```typescript TypeScript + import { serve } from "@upstash/workflow/nextjs"; -### 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]; -When `QSTASH_REGION` is set to `US_EAST_1` or `EU_CENTRAL_1`, the SDK enables migration mode: + // 👇 Use your authentication function to verify the token + if (!isValid(bearerToken)) { + console.error("Authentication failed."); + return; + } -* 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 + // Your workflow steps.. + }, + { + failureFunction: async () => { + // 👇 Same auth check for failure function + const authHeader = context.headers.get("authorization"); + const bearerToken = authHeader?.split(" ")[1]; - - If a workflow run was started in one region, all its steps will execute in that region. - + if (!isValid(bearerToken)) { + // ... + } + }, + } + ); + ``` -Environment variables: + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext -```bash -# Migration mode configuration with US as primary -QSTASH_REGION="US_EAST_1" + app = FastAPI() + serve = Serve(app) -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" + @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 -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" -``` + if not is_valid(bearer_token): + print("Authentication failed.") + return - -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. - + # Your workflow steps... -## SDK Requirements + ``` -Migration support requires: -* `@upstash/workflow` >= 1.1.0 -* `@upstash/qstash` >= 2.9.0 + -Update your dependencies: + + If you implement custom authorization in your workflow route, you should also include the same authorization check in the failure function. -```bash -npm install @upstash/workflow@latest @upstash/qstash@latest -``` + The failure function executes independently of the route function, so without this check, unauthorized requests could trigger the failure function + -# Parallel Runs -Source: https://upstash.com/docs/workflow/howto/parallel-runs +# Start a Run +Source: https://upstash.com/docs/workflow/howto/start - - 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. - +You’ve defined your workflow, and the final step is to trigger the endpoint! -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: +There are two main ways to start your workflow: -```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 () => { ... }), - ]) -``` +### Using `client.trigger` (Recommended) -In a complete code example, your workflow could look like this: +We recommend using [`client.trigger`](/docs/workflow/basics/client/trigger) to start your workflow. -```typescript app/api/workflow/route.ts -import { serve } from "@upstash/workflow/nextjs"; -import { checkInventory, brewCoffee, printReceipt } from "@/utils"; + + ```ts Single Workflow + import { Client } from "@upstash/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")), - ]); + 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 + }) - // If all ingedients available, brew coffee - if (coffeeBeansAvailable && cupsAvailable && milkAvailable) { - const price = await ctx.run("brew-coffee", async () => { - return await brewCoffee({ style: "cappuccino" }); - }); + console.log(workflowRunId) + // prints wfr_my-workflow + ``` - await printReceipt(price); - } -}); -``` + ```ts Multiple Workflows + import { Client } from "@upstash/workflow"; -After running your workflow, your dashboard shows each step in detail: + const client = new Client({ token: "" }) + const results = await client.trigger([ + { + url: "", + // other options... + }, + { + url: "", + // other options... + }, + ]) - + console.log(results[0].workflowRunId) + // prints wfr_my-workflow + ``` + -# Realtime Quickstart -Source: https://upstash.com/docs/workflow/howto/realtime/basic +### 2. Sending an HTTP Request -[**Upstash Realtime**](/docs/realtime/overall/quickstart) lets you emit events from your workflow and subscribe to them in real-time on your frontend. +This approach is recommended for quick testing via curl during development. -## How It Works +You should **NOT** start the workflow run in production by direct calls to your endpoint. -Upstash Realtime is powered by Upstash Redis and provides a clean, 100% type-safe API for publishing and subscribing to events: +```bash +curl -X POST https:/// \ + -H "my-header: foo" \ + -d '{"foo": "bar"}' +``` -* 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 + + 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. -This guide shows you how to integrate Upstash Workflow with Upstash Realtime to display real-time progress updates in your frontend. + For more information, read [Secure a workflow](/docs/workflow/howto/security) documentation. + -## Setup +# Webhooks +Source: https://upstash.com/docs/workflow/howto/use-webhooks -### 1. Install Packages +This guide explains how to handle webhooks effectively in your Upstash Workflow applications. We'll walk through: -```bash -npm install @upstash/workflow @upstash/realtime @upstash/redis zod -``` +* setting up webhook endpoints +* verifying webhook requests +* and processing webhook events -### 2. Configure Upstash Realtime + + You can find the project source code on GitHub. + -Create a Realtime instance in `lib/realtime.ts`: +## Overview -```typescript title="lib/realtime.ts" -import { InferRealtimeEvents, Realtime } from "@upstash/realtime"; -import { Redis } from "@upstash/redis"; -import z from "zod/v4"; +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 redis = Redis.fromEnv(); +Upstash Workflow provides a simple way to receive these events and trigger workflows based on the incoming data autonomously. -const schema = { - workflow: { - runFinish: z.object({}), - stepFinish: z.object({ - stepName: z.string(), - result: z.unknown().optional(), - }), - }, -}; +### Setting Up Webhook Endpoints -export const realtime = new Realtime({ schema, redis }); -export type RealtimeEvents = InferRealtimeEvents; -``` +#### Basic Setup -### 3. Create a Realtime Middleware +To create a webhook endpoint, use the `serve` function from `@upstash/workflow`: -Create a custom middleware that will emit events to Realtime at `lib/middleware.ts`: + -```typescript title="lib/middleware.ts" -import { WorkflowMiddleware } from "@upstash/workflow"; -import { realtime } from "./realtime"; +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; -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", {}); - }, +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) + ); - return { success: true, workflowRunId: context.workflowRunId }; - }, - { - middlewares: [realtimeMiddleware], - } -); -``` + if (!primaryEmail) { + return false; + } -**Key points:** + return { + event: event.type, + userId: clerkUserId, + email: primaryEmail.email_address, + firstName: first_name, + } as UserPayload; + } + return false; + } + ); +}); +``` -* 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. + +```typescript {18-24} +import { createOpenAI } from '@ai-sdk/openai'; +import { HTTPMethods } from '@upstash/qstash'; +import { WorkflowAbort, WorkflowContext } from '@upstash/workflow'; -## Prerequisites +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 +When called with the same prompt as above, we will see the following logs: -### 2. Create the Notify Endpoint + -Create an endpoint at `app/api/notify/route.ts` to handle user input: +## Important Considerations -```typescript -import { Client } from "@upstash/workflow"; -import { NextRequest, NextResponse } from "next/server"; +When using Upstash Workflow with the Vercel AI SDK, there are several critical requirements that must be followed: -const workflowClient = new Client({ - baseUrl: process.env.QSTASH_URL!, - token: process.env.QSTASH_TOKEN!, -}); +### Step Execution Order -export async function POST(request: NextRequest) { - const body = await request.json(); - const { eventId, eventData } = body; +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: - if (!eventId) { - return NextResponse.json( - { success: false, error: "eventId is required" }, - { status: 400 } - ); - } + +```typescript ❌ Wrong {4} +export const { POST } = serve<{ prompt: string }>(async (context) => { + const openai = createWorkflowOpenAI(context); - // Notify the workflow - await workflowClient.notify({ - eventId, - eventData, + // Will throw "prompt is undefined" + const result = await generateText({ + model: openai('gpt-3.5-turbo'), + prompt: context.requestPayload.prompt }); - - return NextResponse.json({ success: true }); -} +}); ``` - -**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** + +1. In Datadog, open the Integrations page. +2. Search for "Upstash" and open the integration. + +![integration-tab.png]() -import { useWorkflowWithRealtime } from "@/hooks/useWorkflowWithRealtime"; +Click "Install" to add Upstash to your Datadog account. -export default function WorkflowPage() { - const { - trigger, - isTriggering, - steps, - isRunFinished, - waitingState, - continueWorkflow, - } = useWorkflowWithRealtime(); +![installation.png]() - return ( -
- +## **Step 3: Connect Accounts** - {isRunFinished && ( -

✅ Workflow Finished!

- )} +After installing Upstash, click "Connect Accounts". Datadog will redirect you to Upstash to complete account linking. - {/* Show workflow steps */} -

Workflow Steps:

-
- {steps.map((step, index) => ( -
- {step.stepName} - {Boolean(step.result) && ( - : {JSON.stringify(step.result)} - )} -
- ))} -
+![connect-acc.png]() - {/* Show approval UI when waiting for input */} - {waitingState && ( -
-

{waitingState.message}

-

- -

-

- -

-
- )} -
- ); -} -``` +## **Step 4: Select Account to Integrate** -## How the Pattern Works +1. On Upstash, select the Datadog account to integrate. +2. Personal and team accounts are supported. -### Timeline of Events +**Caveats** -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!" +* 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. -## Benefits +![personal.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 +![team.png]() -## Full Example +## **Step 5: Wait for Metrics Availability** -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). +Once the integration is completed, metrics from QStash (publish counts, success/error rates, retries, DLQ, schedule executions) will start appearing in Datadog dashboards shortly. -## Next Steps +![upstash-dashboard.png]() -* 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) +## **Step 6: Datadog Integration Removal Process** -# Redact Private Data -Source: https://upstash.com/docs/workflow/howto/redact-fields +From Datadog → Integrations → Upstash, press "Remove" to break the connection. -Workflow runs can contain private data that you don't want visible in the Upstash Console or API responses. +### Confirm Removal -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. +Upstash will stop publishing metrics after removal. Ensure any Datadog API keys/configurations for this integration are also removed on the Datadog side. -To redact fields, pass the `redact` option when triggering a workflow run. +## **Conclusion** -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]`) | +You’ve connected Datadog with Upstash QStash. Explore Datadog dashboards to monitor message delivery performance and reliability. - -Redaction is one-way. Once a field is redacted, the original value cannot be retrieved from the API or dashboard. - +If you need help, contact support. - -```typescript TypeScript -import { Client } from "@upstash/workflow"; +# OpenAI +Source: https://upstash.com/docs/workflow/integrations/openai -const client = new Client({ token: "" }); +### Calling OpenAI -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 - }, -}); -``` +The standard way to call a third-party endpoint in your workflow is by using [`context.call`](/docs/workflow/basics/context#context-call). -```python Python -from upstash_workflow import Client +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: -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 + + `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. + + +```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: - - - - ```typescript TypeScript - import { Receiver } from "@upstash/qstash"; - import { serve } from "@upstash/workflow/nextjs"; - - export const { POST } = serve( - async (context) => { ... }, - { - receiver: new Receiver({ - currentSigningKey: "", - nextSigningKey: "", - }), - } - ); - ``` +For detailed pricing information based on different plans, visit our [Workflow pricing page](https://upstash.com/pricing/workflow). - ```python Python - from qstash import Receiver +### Message Usage per Workflow Run - @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: - ... +* [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. - ``` - - +- [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) -## Custom Authorization Method +# Roadmap +Source: https://upstash.com/docs/workflow/roadmap -You can implement your own authorization mechanism with Upstash Workflow. + + 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 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. +# JavaScript SDK +Source: https://upstash.com/docs/workflow/sdk/workflow-js - +# Python SDK +Source: https://upstash.com/docs/workflow/sdk/workflow-py - ```typescript TypeScript - import { serve } from "@upstash/workflow/nextjs"; +# Overview +Source: https://upstash.com/docs/workflow/steps - export const { POST } = serve( - async (context) => { - // 👇 Extract Bearer token form the request headers - const authHeader = context.headers.get("authorization"); - const bearerToken = authHeader?.split(" ")[1]; +A workflow's **context** is an object provided by the route function. - // 👇 Use your authentication function to verify the token - if (!isValid(bearerToken)) { - console.error("Authentication failed."); - return; - } +The context object provides: +* **Workflow APIs** – functions for defining workflow steps. +* **Workflow Run Properties** – request payload, request headers, and other metadata. - // Your workflow steps.. - }, - { - failureFunction: async () => { - // 👇 Same auth check for failure function - const authHeader = context.headers.get("authorization"); - const bearerToken = authHeader?.split(" ")[1]; + + ```typescript api/workflow/route.ts highlight={4-5} + import { serve } from "@upstash/workflow/nextjs"; - if (!isValid(bearerToken)) { - // ... - } - }, + export const { POST } = serve( + // 👇 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,1054 +49236,1035 @@ 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. + -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. + + A map of headers to include in the request. + -Upstash Workflow provides a simple way to receive these events and trigger workflows based on the incoming data autonomously. + + Number of retry attempts if the request fails. Defaults to `0` (no retries). + -### Setting Up Webhook Endpoints + + 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))`. + -#### Basic Setup + + Throttle outbound requests. -To create a webhook endpoint, use the `serve` function from `@upstash/workflow`: + See [Flow Control](/docs/workflow/features/flow-control) for details. - + + + A logical grouping key that identifies which requests share the same flow control limits. + -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; + + The maximum number of allowed requests per second. + -export const { POST } = serve( - async (context) => { - // Your webhook handling logic here - }, - { - initialPayloadParser: (payload) => { - return payload; - }, - } -); -``` + + The maximum number of concurrent requests allowed. + -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext + + The time window used to enforce the defined rate limit. Default is `1s`. + + + -app = FastAPI() -serve = Serve(app) + + Maximum time (in seconds) to wait for a response. + If retries are enabled, this timeout applies individually to each attempt. + -def initial_payload_parser(payload): - return payload + + 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. + -@serve.post("/api/example", initial_payload_parser=initial_payload_parser) -async def example(context: AsyncWorkflowContext[str]) -> None: - # Your webhook handling logic here +## Response -``` + + The HTTP response status code. + -
+ + The response body. -#### Request Validation + `context.call()` attempts to parse the body as JSON. + If parsing fails, the raw body string is returned. + -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: + + The response headers. + + + + In TypeScript, you can declare the expected result type for strong typing: + + ```typescript + type ResultType = { + field1: string, + field2: number + }; + + const result = await context.call( ... ); + ``` + + +## Usage -```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 - } +```javascript TypeScript +import { serve } from "@upstash/workflow/nextjs"; - // Next steps based on the event +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 Validate and Parse in Workflow - Python -async def validate_request(payload_string: str, header_payload: dict): - # Validate the request - pass - -@serve.post("/api/example") -async def example(context: AsyncWorkflowContext[str]) -> None: - payload_string = context.request_payload - header_payload = context.headers +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext - try: - event = await validate_request(payload_string, header_payload) - except: - return +app = FastAPI() +serve = Serve(app) - # Next steps based on the event +@dataclass +class Request: + topic: str -``` +@serve.post("/api/example") +async def example(context: AsyncWorkflowContext[Request]) -> None: + request: Request = context.request_payload -```typescript Validation Function - TypeScript -import { Webhook } from "svix"; -import { WebhookEvent } from "@clerk/nextjs/server"; + 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']}", + }, + ) -const webhookSecret = "YOUR_WEBHOOK_SECRET"; + status, headers, body = result.status, result.headers, result.body -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; -} ``` -### Handling Webhook events + + 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. + -Use the context.run method to process webhook events in discrete, trackable steps: + + The `context.call()` function can make requests to any public API endpoint. However, it cannot: - + * 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. + -```typescript TypeScript -export const { POST } = serve(async (context) => { - // ... Parse and validate the incoming request +# context.cancel +Source: https://upstash.com/docs/workflow/steps/cancel - 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) - ); +All of the methods covered so far are used to define workflow steps. - if (!primaryEmail) { - return false; - } +`context.cancel` is different — it allows you to **explicitly cancel the current workflow run**. - return { - event: event.type, - userId: clerkUserId, - email: primaryEmail.email_address, - firstName: first_name, - } as UserPayload; - } - return false; - } - ); -}); -``` +```ts +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload -```python Python -@serve.post("/api/example") -async def example(context: AsyncWorkflowContext[str]) -> None: - # ... Parse and validate the incoming request + const result = await context.run("check if canceled", () => { ... }); - 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"] + if (result.cancel) { + await context.cancel() // cancel the workflow run + } +}) +``` - primary_email = next( - ( - email - for email in email_addresses - if email.id == event.data["primary_email_address_id"] - ), - None, - ) +When a workflow run is canceled: - if not primary_email: - return False +* It is labeled as **canceled** (not failed). +* The configured `failureFunction` **is not triggered**. +* No entries are sent to the **dead-letter queue (DLQ)**. - return { - "event": event.type, - "user_id": clerk_user_id, - "email": primary_email["email_address"], - "first_name": first_name, - } +# context.createWebhook +Source: https://upstash.com/docs/workflow/steps/createWebhook - return False +`context.createWebhook()` creates a unique webhook that can be called by external services to trigger workflow continuation. - user = await context.run("handle-webhook-event", _handle_webhook_event) +The webhook URL generated can be called multiple times to resume multiple [`context.waitForWebhook`](/docs/workflow/basics/context/waitForWebhook) steps. -``` +## Arguments - + + Name of the step. + -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. +## Response - + + The unique webhook URL that external services should call to resume the workflow. -```typescript TypeScript -export const { POST } = serve(async (context) => { - // ... Previous validation and user data extraction + Can be called multiple times to resume multiple [`context.waitForWebhook`](/docs/workflow/basics/context/waitForWebhook) steps. + - if (!user) { - return; - } + + The internal event identifier associated with this webhook. + This is primarily used internally by [`context.waitForWebhook`](/docs/workflow/basics/context/waitForWebhook). + - 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, - }, - }); - }); +## Usage + +```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