diff --git a/_snippets/qstash-common-request.mdx b/_snippets/qstash-common-request.mdx index b3f48317..64e93498 100644 --- a/_snippets/qstash-common-request.mdx +++ b/_snippets/qstash-common-request.mdx @@ -117,3 +117,11 @@ We will strip this prefix and forward the header to the callback destination. example: "Upstash-Failure-Callback-Forward-My-Header: my-value" -> "My-Header: my-value" + + + Assign a label to the message for filtering + + Multiple messages can have the same label + + Example: "Upstash-Label: user-signup" + diff --git a/_snippets/qstash-message-type.mdx b/_snippets/qstash-message-type.mdx index 6eaa21c4..e68164a9 100644 --- a/_snippets/qstash-message-type.mdx +++ b/_snippets/qstash-message-type.mdx @@ -59,5 +59,5 @@ - The label of the message assigned by the user. + The label of the message assigned in publish request \ No newline at end of file diff --git a/_snippets/qstash/waiter.mdx b/_snippets/qstash/waiter.mdx index b5d39a7e..29e1f0c1 100644 --- a/_snippets/qstash/waiter.mdx +++ b/_snippets/qstash/waiter.mdx @@ -1,4 +1,4 @@ - + URL to call upon notify diff --git a/_snippets/workflow/logs.mdx b/_snippets/workflow/logs.mdx new file mode 100644 index 00000000..222c86cb --- /dev/null +++ b/_snippets/workflow/logs.mdx @@ -0,0 +1,274 @@ + + + + The ID of the workflow run. + + + + The URL address of the workflow endpoint. + + + + The current state of the workflow run at this point in time + + | Value | Description | + | -------------- | -------------------------------------------------------------- | + | `RUN_STARTED` | The workflow has started to run and currently in progress. | + | `RUN_SUCCESS` | The workflow run has completed succesfully. | + | `RUN_FAILED` | Some errors has occured and workflow failed after all retries. | + | `RUN_CANCELED` | The workflow run has canceled upon user request. | + + + + + The Unix timestamp (in milliseconds) when the workflow run started. + + + + The Unix timestamp (in milliseconds) when the workflow run was completed, if applicable. + + + + The label of the run assigned by the user on trigger. + + + + The details of the failure callback message, if a failure function was defined for the workflow. + + + + The ID of the failure callback message + + + + The URL address of the failure function + + + + The state of the failure callback + + | Value | + | --------------------- | + | `CALLBACK_INPROGRESS` | + | `CALLBACK_SUCCESS` | + | `CALLBACK_FAIL` | + + + + + The HTTP headers of the message that triggered the failure function. + + + + The HTTP response status of the message that triggered the failure function. + + + + The response body of the message that triggered the failure function. + + + + The DLQ ID of the workflow run. + + + + Response body of the failure function/url. + When [failure function](https://upstash.com/docs/workflow/basics/serve#failurefunction) is used, this contains + the returned message from the failure function. + + + + Reponse headers of the failure function/url. This is valuable when the call to run the failure function/url is rejected + because of a platform limit. + + + + Reponse status of the failure function/url. This is valuable when the call to run the failure function/url is rejected + because of a platform limit. + + + + A call to failure url/function can be retried as `maxRetries` time. This array contains errors of all retry + attempts. + + + + Response status of the endpoint that caused the error + + + + Response Headers of the endpoint that caused the error + + + + Response Body of the endpoint that caused the error if available + + + + An error message that happened before/after calling the user's endpoint. + + + + The time of the error happened in Unix time milliseconds + + + + + + + Max number of retries configured when seeing an error. + + + + + + + + + + The type of grouped steps + + | Value | Description | + | ------------ | ---------------------------------------------------------------- | + | `sequential` | Indicates only one step is excuted sequentially | + | `parallel` | Indicates multiple steps being executed in parallel. | + | `next` | Indicates there is information about currently executing step(s) | + + + + + + + The ID of the step which increases monotonically. + + + + The name of the step. It is specified in workflow by user. + + + + Execution type of the step which indicates type of the context function. + + | Value | Function | + | ------------ | -------------------------------------------- | + | `Initial` | The default step which created automatically | + | `Run` | context.run() | + | `Call` | context.call() | + | `SleepFor` | context.sleepFor() | + | `SleepUntil` | context.sleepUntil() | + | `Wait` | context.waitForEvent() | + | `Notify` | context.notify() | + | `Invoke` | context.invoke() | + + + + The ID of the message associated with this step. + + + + The output returned by the step + + + + The total number of concurrent steps that is running alongside this step + + + + The state of this step at this point in time + + | Value | + | --------------- | + | `STEP_SUCCESS` | + | `STEP_RETRY` | + | `STEP_FAILED` | + | `STEP_CANCELED` | + + + + The unix timestamp in milliseconds when the message associated with this step has created. + + + + The unix timestamp in milliseconds when this step will be retried. + This is set only when the step state is `STEP_RETRY` + + + **The following fields are set only when a specific type of step is executing. These fields are not available for all step types.** + + + The duration in milliseconds which step will sleep. Only set if stepType is `SleepFor`. + + + + The unix timestamp (in milliseconds) which step will sleep until. Only set if stepType is `SleepUntil`. + + + + The event id of the wait step. Only set if stepType is `Wait`. + + + + The unix timestamp (in milliseconds) when the wait will time out. + + + + The duration of timeout in human readable format (e.g. 120s, 1m, 1h). + + + + Set to true if this step is cause of a wait timeout rather than notifying the waiter. + + + + The URL of the external address. Available only if stepType is `Call`. + + + + The HTTP method of the request sent to the external address. Available only if stepType is `Call`. + + + + The HTTP headers of the request sent to the external address. Available only if stepType is `Call`. + + + + The body of the request sent to the external address. Available only if stepType is `Call`. + + + + The HTTP status returned by the external call. Available only if stepType is `Call`. + + + + The body returned by the external call. Available only if stepType is `Call`. + + + + The HTTP headers returned by the external call. Available only if stepType is `Call`. + + + + The ID of the invoked workflow run if this step is an invoke step. + + + + The URL address of the workflow server of invoked workflow run if this step is an invoke step. + + + + The Unix timestamp (in milliseconds) when the invoked workflow was started if this step is an invoke step. + + + + The body passed to the invoked workflow if this step is an invoke step. + + + + The HTTP headers passed to invoked workflow if this step is an invoke step. + + + + + + + \ No newline at end of file diff --git a/_snippets/workflow/workflow-dlq-message-type.mdx b/_snippets/workflow/workflow-dlq-message-type.mdx index c562d651..ced543fc 100644 --- a/_snippets/workflow/workflow-dlq-message-type.mdx +++ b/_snippets/workflow/workflow-dlq-message-type.mdx @@ -47,13 +47,13 @@ Flow control key (if set). - Rate limit (if set). + Flow control rate limit (if set). - Parallelism (if set). + Flow control parallelism (if set). - Period (if set). + Flow control period (if set). HTTP response status code of the last failed delivery attempt. diff --git a/docs.json b/docs.json index dfed5cc7..006de5aa 100644 --- a/docs.json +++ b/docs.json @@ -1063,10 +1063,6 @@ "qstash/api/dlq/deleteMessages" ] }, - { - "group": "LLM", - "pages": ["qstash/api/llm/create"] - }, "qstash/api/api-ratelimiting" ] }, @@ -1160,13 +1156,146 @@ { "group": "Basics", "pages": [ - "workflow/basics/serve", - "workflow/basics/context", - "workflow/basics/client", "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/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/callback" + ] + }, + "workflow/basics/client/notify", + "workflow/basics/client/waiters" + ] + } + ] + }, "workflow/basics/caveats" ] }, + { + "group": "Features", + "pages": [ + { + "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/callback" + ] + }, + { + "group": "Flow Control", + "pages": [ + "workflow/features/flow-control", + "workflow/features/flow-control/rate-period", + "workflow/features/flow-control/parallelism" + ] + }, + { + "group": "Wait For Event", + "pages": [ + "workflow/features/wait-for-event", + "workflow/features/wait", + "workflow/features/notify" + ] + }, + { + "group": "Invoke", + "pages": [ + "workflow/features/invoke", + "workflow/features/invoke/serveMany" + ] + } + + ] + }, + { + "group": "How To", + "pages": [ + { + "group": "Local Development", + "pages": [ + "workflow/howto/local-development/development-server", + "workflow/howto/local-development/local-tunnel" + ] + }, + "workflow/howto/start", + { + "group": "Configure a Run", + "pages": [ + "workflow/howto/configure", + { + "group": "Advanced", + "pages": [ + "workflow/howto/configure/per-step-configuration" + ] + } + ] + }, + "workflow/howto/cancel", + "workflow/howto/security", + "workflow/howto/changes", + "workflow/howto/schedule", + "workflow/howto/use-webhooks" + ] + }, { "group": "Agents", "pages": [ @@ -1185,25 +1314,6 @@ "workflow/agents/examples" ] }, - { - "group": "How To", - "pages": [ - "workflow/howto/start", - "workflow/howto/failures", - "workflow/howto/cancel", - "workflow/howto/events", - "workflow/howto/monitor", - "workflow/howto/security", - "workflow/howto/flow-control", - "workflow/howto/parallel-runs", - "workflow/howto/invoke", - "workflow/howto/schedule", - "workflow/howto/changes", - "workflow/howto/local-development", - "workflow/howto/use-webhooks", - "workflow/migration" - ] - }, { "group": "REST API", "pages": [ diff --git a/img/qstash-workflow/w-concept.png b/img/qstash-workflow/w-concept.png new file mode 100644 index 00000000..2359c618 Binary files /dev/null and b/img/qstash-workflow/w-concept.png differ diff --git a/img/workflow/automatic_retry.png b/img/workflow/automatic_retry.png new file mode 100644 index 00000000..b5579b4d Binary files /dev/null and b/img/workflow/automatic_retry.png differ diff --git a/img/workflow/dlq.png b/img/workflow/dlq.png new file mode 100644 index 00000000..07b8b70a Binary files /dev/null and b/img/workflow/dlq.png differ diff --git a/img/workflow/failing_failure_function.png b/img/workflow/failing_failure_function.png new file mode 100644 index 00000000..37876fe8 Binary files /dev/null and b/img/workflow/failing_failure_function.png differ diff --git a/img/workflow/failure_callback_state_filter.png b/img/workflow/failure_callback_state_filter.png new file mode 100644 index 00000000..809bbdf8 Binary files /dev/null and b/img/workflow/failure_callback_state_filter.png differ diff --git a/img/workflow/failure_function.png b/img/workflow/failure_function.png new file mode 100644 index 00000000..3293804f Binary files /dev/null and b/img/workflow/failure_function.png differ diff --git a/img/workflow/flow_control_ex_1.png b/img/workflow/flow_control_ex_1.png new file mode 100644 index 00000000..9aa45d59 Binary files /dev/null and b/img/workflow/flow_control_ex_1.png differ diff --git a/img/workflow/flow_control_ex_2.png b/img/workflow/flow_control_ex_2.png new file mode 100644 index 00000000..701a001f Binary files /dev/null and b/img/workflow/flow_control_ex_2.png differ diff --git a/img/workflow/flow_control_ex_3.png b/img/workflow/flow_control_ex_3.png new file mode 100644 index 00000000..8ea3af99 Binary files /dev/null and b/img/workflow/flow_control_ex_3.png differ diff --git a/img/workflow/invoke.png b/img/workflow/invoke.png new file mode 100644 index 00000000..2be9e04e Binary files /dev/null and b/img/workflow/invoke.png differ diff --git a/img/workflow/parallel_steps.png b/img/workflow/parallel_steps.png new file mode 100644 index 00000000..aef567ab Binary files /dev/null and b/img/workflow/parallel_steps.png differ diff --git a/img/workflow/parallelism_1.png b/img/workflow/parallelism_1.png new file mode 100644 index 00000000..1be84c26 Binary files /dev/null and b/img/workflow/parallelism_1.png differ diff --git a/img/workflow/parallelism_2.png b/img/workflow/parallelism_2.png new file mode 100644 index 00000000..77d2ba76 Binary files /dev/null and b/img/workflow/parallelism_2.png differ diff --git a/img/workflow/parallelism_3.png b/img/workflow/parallelism_3.png new file mode 100644 index 00000000..879c0811 Binary files /dev/null and b/img/workflow/parallelism_3.png differ diff --git a/img/workflow/rate_1.png b/img/workflow/rate_1.png new file mode 100644 index 00000000..6204f734 Binary files /dev/null and b/img/workflow/rate_1.png differ diff --git a/img/workflow/rate_2.png b/img/workflow/rate_2.png new file mode 100644 index 00000000..d922e490 Binary files /dev/null and b/img/workflow/rate_2.png differ diff --git a/img/workflow/rate_3.png b/img/workflow/rate_3.png new file mode 100644 index 00000000..ae2a4d5b Binary files /dev/null and b/img/workflow/rate_3.png differ diff --git a/img/workflow/restart.png b/img/workflow/restart.png new file mode 100644 index 00000000..b72ffe4c Binary files /dev/null and b/img/workflow/restart.png differ diff --git a/img/workflow/resume.png b/img/workflow/resume.png new file mode 100644 index 00000000..b1ae28a7 Binary files /dev/null and b/img/workflow/resume.png differ diff --git a/img/workflow/retry_failure_callback.png b/img/workflow/retry_failure_callback.png new file mode 100644 index 00000000..f6efce33 Binary files /dev/null and b/img/workflow/retry_failure_callback.png differ diff --git a/qstash/api/api-ratelimiting.mdx b/qstash/api/api-ratelimiting.mdx index 3391b1c1..06b95de4 100644 --- a/qstash/api/api-ratelimiting.mdx +++ b/qstash/api/api-ratelimiting.mdx @@ -5,11 +5,10 @@ description: "This page documents the rate limiting behavior of our API and expl ## Overview -Our API implements rate limiting to ensure fair usage and maintain service stability. There are three types of rate limits: +Our API implements rate limiting to ensure fair usage and maintain service stability. There are two types of rate limits: 1. **Daily rate limit** 2. **Burst rate limit** -3. **Chat-based rate limit** When a rate limit is exceeded, the API returns a 429 status code along with specific headers that provide information about the limit, remaining requests/tokens, and reset time. @@ -35,19 +34,6 @@ This is a short-term limit **per second** to prevent rapid bursts of requests. T - `Burst-RateLimit-Remaining`: Remaining number of requests in the burst window (1 second) - `Burst-RateLimit-Reset`: Time (in unix timestamp) when the burst limit will reset -### Chat-based Rate Limit - -This limit is applied to chat-related API endpoints. - -**Headers**: - -- `x-ratelimit-limit-requests`: Maximum number of requests allowed per day -- `x-ratelimit-limit-tokens`: Maximum number of tokens allowed per day -- `x-ratelimit-remaining-requests`: Remaining number of requests for the day -- `x-ratelimit-remaining-tokens`: Remaining number of tokens for the day -- `x-ratelimit-reset-requests`: Time (in unix timestamp) until the request limit resets -- `x-ratelimit-reset-tokens`: Time (in unix timestamp) when the token limit will reset - ### Example Rate Limit Error Handling ```typescript Handling Daily Rate Limit Error @@ -93,38 +79,3 @@ try { } } ``` - -```typescript Handling Chat-based Rate Limit Error -import { QstashChatRatelimitError, Client, openai } from "@upstash/qstash"; - -try { - // Example of a chat-related request that could hit the chat rate limit - const client = new Client({ - token: "", - }); - - const result = await client.publishJSON({ - api: { - name: "llm", - provider: openai({ token: process.env.OPENAI_API_KEY! }), - }, - body: { - model: "gpt-3.5-turbo", - messages: [ - { - role: "user", - content: "Where is the capital of Turkey?", - }, - ], - }, - callback: "https://oz.requestcatcher.com/", - }); -} catch (error) { - if (error instanceof QstashChatRatelimitError) { - console.log("Chat rate limit exceeded. Retry after:", error.resetRequests); - // Handle chat-specific rate limiting, perhaps by queueing requests - } else { - console.error("An unexpected error occurred:", error); - } -} -``` diff --git a/qstash/api/llm/create.mdx b/qstash/api/llm/create.mdx deleted file mode 100644 index c8082a74..00000000 --- a/qstash/api/llm/create.mdx +++ /dev/null @@ -1,423 +0,0 @@ ---- -title: "Create Chat Completion" -description: "Creates a chat completion of one or more messages" -api: "POST https://qstash.upstash.io/llm/v1/chat/completions" -authMethod: "bearer" ---- - -Creates a chat completion that generates a textual response -for one or more messages using a large language model. - -## Request - - - Name of the model. - - - - One or more chat messages. - - - The role of the message author. One of `system`, `assistant`, or `user`. - - - The content of the message. - - - An optional name for the participant. - Provides the model information to differentiate between participants of the same role. - - - - - - Number between `-2.0` and `2.0`. Positive values penalize new tokens based on their existing - frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. - - - - Modify the likelihood of specified tokens appearing in the completion. - - Accepts a JSON object that maps tokens (specified by their token ID in the tokenizer) - to an associated bias value from `-100` to `100`. Mathematically, the bias is added to - the logits generated by the model prior to sampling. The exact effect will vary - per model, but values between `-1` and `1` should decrease or increase likelihood - of selection; values like `-100` or `100` should result in a ban or exclusive - selection of the relevant token. - - - - Whether to return log probabilities of the output tokens or not. If true, returns - the log probabilities of each output token returned in the content of message. - - - - An integer between `0` and `20` specifying the number of most likely tokens to return at - each token position, each with an associated log probability. logprobs must be set - to true if this parameter is used. - - - - The maximum number of tokens that can be generated in the chat completion. - - - - How many chat completion choices to generate for each input message. - - Note that you will be charged based on the number of generated tokens - across all of the choices. Keep `n` as `1` to minimize costs. - - - - Number between `-2.0` and `2.0`. Positive values penalize new tokens - based on whether they appear in the text so far, increasing the - model's likelihood to talk about new topics. - - - - An object specifying the format that the model must output. - - Setting to `{ "type": "json_object" }` enables JSON mode, - which guarantees the message the model generates is valid JSON. - - **Important**: when using JSON mode, you must also instruct the model - to produce JSON yourself via a system or user message. Without this, - the model may generate an unending stream of whitespace until the - generation reaches the token limit, resulting in a long-running and - seemingly "stuck" request. Also note that the message content may - be partially cut off if `finish_reason="length"`, which indicates the - generation exceeded max_tokens or the conversation exceeded the max context length. - - - Must be one of `text` or `json_object`. - - - - - - This feature is in Beta. If specified, our system will make a best effort to sample - deterministically, such that repeated requests with the same seed and parameters - should return the same result. Determinism is not guaranteed, and you should - refer to the `system_fingerprint` response parameter to monitor changes in the backend. - - - - Up to 4 sequences where the API will stop generating further tokens. - - - - If set, partial message deltas will be sent. Tokens will be sent as - data-only server-sent events as they become available, with the stream - terminated by a `data: [DONE]` message. - - - - What sampling temperature to use, between `0` and `2`. Higher values - like `0.8` will make the output more random, while lower values - like `0.2` will make it more focused and deterministic. - - We generally recommend altering this or `top_p` but not both. - - - - An alternative to sampling with temperature, called nucleus sampling, - where the model considers the results of the tokens with `top_p` - probability mass. So `0.1` means only the tokens comprising the top - `10%`` probability mass are considered. - - We generally recommend altering this or `temperature` but not both. - - -## Response - -Returned when `stream` is `false` or not set. - - - A unique identifier for the chat completion. - - - - A list of chat completion choices. Can be more than one if `n` is greater than `1`. - - - A chat completion message generated by the model. - - - The role of the author of this message. - - - The contents of the message. - - - - - The reason the model stopped generating tokens. This will be `stop` if the - model hit a natural stop point or a provided stop sequence, `length` if - the maximum number of tokens specified in the request was reached. - - - The stop string or token id that caused the completion to stop, - null if the completion finished for some other reason including - encountering the EOS token - - - The index of the choice in the list of choices. - - - Log probability information for the choice. - - - A list of message content tokens with log probability information. - - - The token. - - - The log probability of this token, if it is within the top 20 most likely tokens. - Otherwise, the value `-9999.0` is used to signify that the token is very unlikely. - - - A list of integers representing the UTF-8 bytes representation of the token. - Useful in instances where characters are represented by multiple tokens and - their byte representations must be combined to generate the correct text - representation. Can be null if there is no bytes representation for the token. - - - List of the most likely tokens and their log probability, at this token position. - In rare cases, there may be fewer than the number of requested `top_logprobs` returned. - - - The token. - - - The log probability of this token, if it is within the top 20 most likely tokens. - Otherwise, the value `-9999.0` is used to signify that the token is very unlikely. - - - A list of integers representing the UTF-8 bytes representation of the token. - Useful in instances where characters are represented by multiple tokens and - their byte representations must be combined to generate the correct text - representation. Can be null if there is no bytes representation for the token. - - - - - - - - - - - - The Unix timestamp (in seconds) of when the chat completion was created. - - - - The model used for the chat completion. - - - - This fingerprint represents the backend configuration that the model runs with. - - Can be used in conjunction with the `seed` request parameter to understand - when backend changes have been made that might impact determinism. - - - - The object type, which is always `chat.completion`. - - - - Usage statistics for the completion request. - - - Number of tokens in the generated completion. - - - Number of tokens in the prompt. - - - Total number of tokens used in the request (prompt + completion). - - - - -## Stream Response - -Returned when `stream` is `true`. - - - A unique identifier for the chat completion. Each chunk has the same ID. - - - - A list of chat completion choices. Can be more than one if `n` is greater than `1`. - Can also be empty for the last chunk. - - - A chat completion delta generated by streamed model responses. - - - The role of the author of this message. - - - The contents of the chunk message. - - - - - The reason the model stopped generating tokens. This will be `stop` if the - model hit a natural stop point or a provided stop sequence, `length` if - the maximum number of tokens specified in the request was reached. - - - The index of the choice in the list of choices. - - - Log probability information for the choice. - - - A list of message content tokens with log probability information. - - - The token. - - - The log probability of this token, if it is within the top 20 most likely tokens. - Otherwise, the value `-9999.0` is used to signify that the token is very unlikely. - - - A list of integers representing the UTF-8 bytes representation of the token. - Useful in instances where characters are represented by multiple tokens and - their byte representations must be combined to generate the correct text - representation. Can be null if there is no bytes representation for the token. - - - List of the most likely tokens and their log probability, at this token position. - In rare cases, there may be fewer than the number of requested `top_logprobs` returned. - - - The token. - - - The log probability of this token, if it is within the top 20 most likely tokens. - Otherwise, the value `-9999.0` is used to signify that the token is very unlikely. - - - A list of integers representing the UTF-8 bytes representation of the token. - Useful in instances where characters are represented by multiple tokens and - their byte representations must be combined to generate the correct text - representation. Can be null if there is no bytes representation for the token. - - - - - - - - - - - - The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp. - - - - The model used for the chat completion. - - - - This fingerprint represents the backend configuration that the model runs with. - - Can be used in conjunction with the `seed` request parameter to understand - when backend changes have been made that might impact determinism. - - - - The object type, which is always `chat.completion.chunk`. - - - - it contains a null value except for the last chunk which contains the token usage statistics for the entire request. - - - Number of tokens in the generated completion. - - - Number of tokens in the prompt. - - - Total number of tokens used in the request (prompt + completion). - - - - - - -```sh curl -curl "https://qstash.upstash.io/llm/v1/chat/completions" \ - -X POST \ - -H "Authorization: Bearer QSTASH_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "meta-llama/Meta-Llama-3-8B-Instruct", - "messages": [ - { - "role": "user", - "content": "What is the capital of Turkey?" - } - ] - }' -``` - - - - -```json 200 OK -{ - "id": "cmpl-abefcf66fae945b384e334e36c7fdc97", - "object": "chat.completion", - "created": 1717483987, - "model": "meta-llama/Meta-Llama-3-8B-Instruct", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "The capital of Turkey is Ankara." - }, - "logprobs": null, - "finish_reason": "stop", - "stop_reason": null - } - ], - "usage": { - "prompt_tokens": 18, - "total_tokens": 26, - "completion_tokens": 8 - } -} -``` - -```json 200 OK - Stream -data: {"id":"cmpl-dfc1ad80d0254c2aaf3e7775d1830c9d","object":"chat.completion.chunk","created":1717484084,"model":"meta-llama/Meta-Llama-3-8B-Instruct","choices":[{"index":0,"delta":{"role":"assistant"},"logprobs":null,"finish_reason":null}]} - -data: {"id":"cmpl-dfc1ad80d0254c2aaf3e7775d1830c9d","object":"chat.completion.chunk","created":1717484084,"model":"meta-llama/Meta-Llama-3-8B-Instruct","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}]} - -data: {"id":"cmpl-dfc1ad80d0254c2aaf3e7775d1830c9d","object":"chat.completion.chunk","created":1717484084,"model":"meta-llama/Meta-Llama-3-8B-Instruct","choices":[{"index":0,"delta":{"content":" capital"},"logprobs":null,"finish_reason":null}]} - -data: {"id":"cmpl-dfc1ad80d0254c2aaf3e7775d1830c9d","object":"chat.completion.chunk","created":1717484084,"model":"meta-llama/Meta-Llama-3-8B-Instruct","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}]} - -data: {"id":"cmpl-dfc1ad80d0254c2aaf3e7775d1830c9d","object":"chat.completion.chunk","created":1717484084,"model":"meta-llama/Meta-Llama-3-8B-Instruct","choices":[{"index":0,"delta":{"content":" Turkey"},"logprobs":null,"finish_reason":null}]} - -data: {"id":"cmpl-dfc1ad80d0254c2aaf3e7775d1830c9d","object":"chat.completion.chunk","created":1717484084,"model":"meta-llama/Meta-Llama-3-8B-Instruct","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}]} - -data: {"id":"cmpl-dfc1ad80d0254c2aaf3e7775d1830c9d","object":"chat.completion.chunk","created":1717484084,"model":"meta-llama/Meta-Llama-3-8B-Instruct","choices":[{"index":0,"delta":{"content":" Ankara"},"logprobs":null,"finish_reason":null}]} - -data: {"id":"cmpl-dfc1ad80d0254c2aaf3e7775d1830c9d","object":"chat.completion.chunk","created":1717484084,"model":"meta-llama/Meta-Llama-3-8B-Instruct","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} - -data: {"id":"cmpl-dfc1ad80d0254c2aaf3e7775d1830c9d","object":"chat.completion.chunk","created":1717484084,"model":"meta-llama/Meta-Llama-3-8B-Instruct","choices":[{"index":0,"delta":{"content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":18,"total_tokens":26,"completion_tokens":8}} - -data: [DONE] -``` - \ No newline at end of file diff --git a/qstash/api/schedules/get.mdx b/qstash/api/schedules/get.mdx index 5d755ff4..d7cd54e9 100644 --- a/qstash/api/schedules/get.mdx +++ b/qstash/api/schedules/get.mdx @@ -46,6 +46,9 @@ authMethod: "bearer" The url where we send a callback to after the message is delivered + + The label of the schedule assigned while creating the schedule + diff --git a/qstash/api/schedules/list.mdx b/qstash/api/schedules/list.mdx index 70ecd4b2..50a30d12 100644 --- a/qstash/api/schedules/list.mdx +++ b/qstash/api/schedules/list.mdx @@ -40,6 +40,8 @@ authMethod: "bearer" The url where we send a callback to after the message is delivered + + The label of the schedule assigned while creating the schedule diff --git a/qstash/howto/local-development.mdx b/qstash/howto/local-development.mdx index 55404b9b..e8107608 100644 --- a/qstash/howto/local-development.mdx +++ b/qstash/howto/local-development.mdx @@ -76,31 +76,31 @@ Select and copy the credentials from one of the following test users: ```javascript User 1 -QSTASH_URL=http://localhost:8080 -QSTASH_TOKEN=eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0= -QSTASH_CURRENT_SIGNING_KEY=sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r -QSTASH_NEXT_SIGNING_KEY=sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs +QSTASH_URL="http://localhost:8080" +QSTASH_TOKEN="eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" +QSTASH_CURRENT_SIGNING_KEY="sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r" +QSTASH_NEXT_SIGNING_KEY="sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs" ``` ```javascript User 2 -QSTASH_URL=http://localhost:8080 -QSTASH_TOKEN=eyJVc2VySUQiOiJ0ZXN0VXNlcjEiLCJQYXNzd29yZCI6InRlc3RQYXNzd29yZCJ9 -QSTASH_CURRENT_SIGNING_KEY=sig_7GVPjvuwsfqF65iC8fSrs1dfYruM -QSTASH_NEXT_SIGNING_KEY=sig_5NoELc3EFnZn4DVS5bDs2Nk4b7Ua +QSTASH_URL="http://localhost:8080" +QSTASH_TOKEN="eyJVc2VySUQiOiJ0ZXN0VXNlcjEiLCJQYXNzd29yZCI6InRlc3RQYXNzd29yZCJ9" +QSTASH_CURRENT_SIGNING_KEY="sig_7GVPjvuwsfqF65iC8fSrs1dfYruM" +QSTASH_NEXT_SIGNING_KEY="sig_5NoELc3EFnZn4DVS5bDs2Nk4b7Ua" ``` ```javascript User 3 -QSTASH_URL=http://localhost:8080 -QSTASH_TOKEN=eyJVc2VySUQiOiJ0ZXN0VXNlcjIiLCJQYXNzd29yZCI6InRlc3RQYXNzd29yZCJ9 -QSTASH_CURRENT_SIGNING_KEY=sig_6jWGaWRxHsw4vMSPJprXadyvrybF -QSTASH_NEXT_SIGNING_KEY=sig_7qHbvhmahe5GwfePDiS5Lg3pi6Qx +QSTASH_URL="http://localhost:8080" +QSTASH_TOKEN="eyJVc2VySUQiOiJ0ZXN0VXNlcjIiLCJQYXNzd29yZCI6InRlc3RQYXNzd29yZCJ9" +QSTASH_CURRENT_SIGNING_KEY="sig_6jWGaWRxHsw4vMSPJprXadyvrybF" +QSTASH_NEXT_SIGNING_KEY="sig_7qHbvhmahe5GwfePDiS5Lg3pi6Qx" ``` ```javascript User 4 -QSTASH_URL=http://localhost:8080 -QSTASH_TOKEN=eyJVc2VySUQiOiJ0ZXN0VXNlcjMiLCJQYXNzd29yZCI6InRlc3RQYXNzd29yZCJ9 -QSTASH_CURRENT_SIGNING_KEY=sig_5T8FcSsynBjn9mMLBsXhpacRovJf -QSTASH_NEXT_SIGNING_KEY=sig_7GFR4YaDshFcqsxWRZpRB161jguD +QSTASH_URL="http://localhost:8080" +QSTASH_TOKEN="eyJVc2VySUQiOiJ0ZXN0VXNlcjMiLCJQYXNzd29yZCI6InRlc3RQYXNzd29yZCJ9" +QSTASH_CURRENT_SIGNING_KEY="sig_5T8FcSsynBjn9mMLBsXhpacRovJf" +QSTASH_NEXT_SIGNING_KEY="sig_7GFR4YaDshFcqsxWRZpRB161jguD" ``` diff --git a/workflow/agents/getting-started.mdx b/workflow/agents/getting-started.mdx index b38be4ca..fe271997 100644 --- a/workflow/agents/getting-started.mdx +++ b/workflow/agents/getting-started.mdx @@ -42,7 +42,7 @@ Once you start the QStash server, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` v ```txt .env.local QSTASH_URL="http://127.0.0.1:8080" -QSTASH_TOKEN= +QSTASH_TOKEN="" OPENAI_API_KEY= ``` diff --git a/workflow/basics/client.mdx b/workflow/basics/client.mdx index d8419d1f..2760e409 100644 --- a/workflow/basics/client.mdx +++ b/workflow/basics/client.mdx @@ -1,331 +1,38 @@ --- -title: "Workflow Client" +title: "Overview" --- - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/workflow/roadmap) for feature parity plans and - [Changelog](/workflow/changelog) for updates. - +The Workflow Client lets you programmatically interact with your workflow runs. +You can use it from the same application that hosts your workflows, or from any external service. -Workflow client allows you to interact with your workflow runs. Currently, it has three basic functionality: -- [cancel a running workflow run](/workflow/howto/cancel) -- notify a workflow run [waiting for an event](/workflow/basics/context#context-waitforevent) -- get workflow runs waiting for some event +## Initialization -We are planning to add more functionality in the future. See [the roadmap](/workflow/roadmap) for more details. - -## Trigger Workflow - -Using the `trigger` method, you can start a workflow run and get its run id. - - -```ts Single Workflow -import { Client } from "@upstash/workflow"; - -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 - retryDelay: "1000 * (1 + retried)", // optional delay between retries - delay: "10s" // optional delay value - failureUrl: "https://", // optional failure url - useFailureFunction: true, // whether a failure function is defined in the endpoint - flowControl: { // optional flow control - key: "USER_GIVEN_KEY", - rate: 10, - parallelism: 5, - period: "10m" - }, -}) - -console.log(workflowRunId) -// prints wfr_my-workflow -``` - -```ts Multiple Workflows -import { Client } from "@upstash/workflow"; - -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 -``` - - -If both `failureUrl` and `useFailureFunction` are provided, `useFailureFunction` takes precedence and the value of the `url` parameter is used as `failureUrl`. - -If `workflowRunId` parameter isn't passed, a run id will be generated randomly. If `workflowRunId` is passed, `wfr_` prefix will be added to it. - -For other alternatives of starting a workflow, see the [documentation on starting a workflow run](/workflow/howto/start). - -## Get Workflow Logs - -Using the `log` method, you can use the [List Workflow Runs API](/workflow/rest/runs/logs): - -```ts -import { Client } from "@upstash/workflow"; - -const client = new Client({ token: "" }) -const { runs, cursor } = await client.logs({ - // Id of the workflow run to get - workflowRunId, - // Number of workflows to get - count, - // Workflow state to filter for. - // One of "RUN_STARTED", "RUN_SUCCESS", "RUN_FAILED", "RUN_CANCELED" - state, - // Workflow url to search for. should be an exact match - workflowUrl, - // Unix timestamp when the run was created - workflowCreatedAt, - // Cursor from a previous request to continue the search - cursor -}) -``` - -All the parameters above are optional. - -The response includes `cursor` and `runs`. Using the `cursor`, you can continue the search later. - -`runs` will be a list of runs. Each run has these fields: - -- `workflowRunId`: ID of the workflow -- `workflowUrl`: URL of the workflow -- `workflowState`: State of the workflow run. -- `workflowRunCreatedAt`: number; Time when the workflow run started (as unix timestamp) -- `workflowRunCompletedAt`; If run has completed, time when workflow run completed (as unix timestamp) -- `failureFunction`: Information on the message published when the workflow failed and [`failureUrl`](/workflow/basics/serve#failureurl) or [`failureFunction`](/workflow/basics/serve#failurefunction) were called -- `dlqId`: If the workflow run has failed, DLQ id associated with the workflow run. -- `workflowRunResponse`: Result returned at the end of the workflow. -- `invoker`: If the [workflow was invoked](/workflow/howto/invoke); run id, url and created time of the invoking workflow. -- `steps`: List of steps showing the progress of the workflow. - -The `steps` field contains a list of steps. Each step is in one of the following three formats: - - -```ts Single Step -{ - type: "single"; - // a single step in an array - steps: [ - { - // step info... - } - ] -} -``` - -```ts Parallel Steps -{ - type: "parallel"; - // an array of steps - steps: [ ... ] -} -``` - -```ts Next Step -{ - type: "next"; - // and array of steps with messageId and state - steps: [ - { - messageId: string; - state: "STEP_PROGRESS" | "STEP_RETRY" | "STEP_FAILED" - } - ]; -} -``` - - -## Cancel Workflow Runs - -There are multiple ways you can cancel workflow runs: - -- pass one or more workflow run ids to cancel them -- pass a workflow url to cancel all runs starting with this url -- cancel all pending or active workflow runs - -### Cancel a set of workflow runs - -```ts -// cancel a single workflow -await client.cancel({ ids: "" }); - -// cancel a set of workflow runs -await client.cancel({ ids: ["", ""] }); -``` - -### Cancel workflows starting with a url - -If you have an endpoint called `https://your-endpoint.com` and you -want to cancel all workflow runs on it, you can use `urlStartingWith`. - -Note that this will cancel workflows in all endpoints under -`https://your-endpoint.com`. - -```ts -await client.cancel({ urlStartingWith: "https://your-endpoint.com" }); -``` - -### Cancel _all_ workflows - -To cancel all pending and currently running workflows, you can -do it like this: - -```ts -await client.cancel({ all: true }); -``` - -## Dead Letter Queue (DLQ) - -The DLQ functionality allows you to manage failed workflow runs that have been moved to the dead letter queue. - -### List DLQ Messages - -```ts -import { Client } from "@upstash/workflow"; - -const client = new Client({ token: "" }); - -// Define filters for the DLQ messages (optional) -const dlqFilters = { - fromDate: Date.now() - 86400000, // last 24 hours - toDate: Date.now(), - url: "https://your-endpoint.com", - responseStatus: 500 -} - -// List all DLQ messages -const { messages, cursor } = await client.dlq.list({ filter: dlqFilters }); - -// List with pagination and filtering -const result = await client.dlq.list({ - cursor, - count: 10, - filter: dlqFilters -}); - -``` - -### Resume Failed Workflows - -Resume a workflow from where it failed: - - -```ts Single -const { messages } = await client.dlq.list(); - -const response = await client.dlq.resume({ - dlqId: messages[0].dlqId, // Use the dlqId from the message - flowControl: { - key: "my-flow-control-key", - value: "my-flow-control-value", - }, - retries: 3, -}); - -``` -```ts Multiple -const responses = await client.dlq.resume({ - dlqId: ["dlq-12345", "dlq-67890"], - retries: 5, -}); -``` - - -### Restart Failed Workflows - -Restart a workflow from the beginning: - - -```ts Single -const { messages } = await client.dlq.list(); - -const response = await client.dlq.restart({ - dlqId: messages[0].dlqId, // Use the dlqId from the message - flowControl: { - key: "my-flow-control-key", - value: "my-flow-control-value", - }, - retries: 3, -}); - -``` -```ts Multiple -const responses = await client.dlq.restart({ - dlqId: ["dlq-12345", "dlq-67890"], - retries: 5, -}); -``` - - -The difference between `resume` and `restart`: -- **Resume**: Continues execution from where the workflow failed -- **Restart**: Starts execution from the beginning with the same initial payload - -### Retry Failure Function - -If a workflow's `failureFunction` or `failureUrl` request has failed, you can retry it using the `retryFailureFunction` method: - -```ts -import { Client } from "@upstash/workflow"; - -const client = new Client({ token: "" }); - -// Retry the failure callback for a specific DLQ message -const response = await client.dlq.retryFailureFunction({ - dlqId: "dlq-12345" // The ID of the DLQ message to retry -}); -``` - -This method allows you to retry the failure callback of a workflow run whose `failureUrl` or `failureFunction` request has failed. - -## Notify Waiting Workflow - -To notify a workflow waiting for an event, you can use the `notify` method: +Initialize a new client with your credentials: ```javascript -import { Client } from "@upstash/workflow"; +import { Client } from "@upstash/workflow" -const client = new Client({ token: "" }); -await client.notify({ - eventId: "my-event-id", - eventData: "my-data", // data passed to the workflow run -}); +const client = new Client({ + baseUrl: process.env.QSTASH_URL!, + token: process.env.QSTASH_TOKEN! +}) ``` -The data passed in `eventData` will be available to the workflow run in the -[`eventData` field of the `context.waitForEvent` method](/workflow/basics/context#context-waitforevent). - -## Get Waiters of Event +The client is lightweight and stateless. You can safely reuse a single instance across your application. -To get the list of waiters for some event id, you can use the `getWaiters` method: -```javascript -import { Client } from "@upstash/workflow"; - -const client = new Client({ token: "" }); -const result = await client.getWaiters({ - eventId: "my-event-id", -}); -``` +## Functionality -Result will be a list of `Waiter` objects: +The client exposes a set of functions to manage workflow runs and inspect their state: - +- [client.trigger](/workflow/basics/client/trigger) +- [client.cancel](/workflow/basics/client/cancel) +- [client.notify](/workflow/basics/client/notify) +- [client.logs](/workflow/basics/client/logs) +- [client.getWaiters](/workflow/basics/client/waiters) +- client.dlq + - [client.dlq.list](/workflow/basics/client/dlq/list) + - [client.dlq.restart](/workflow/basics/client/dlq/restart) + - [client.dlq.resume](/workflow/basics/client/dlq/resume) + - [client.dlq.retryFailureFunction](/workflow/basics/client/dlq/callback) \ No newline at end of file diff --git a/workflow/basics/client/cancel.mdx b/workflow/basics/client/cancel.mdx new file mode 100644 index 00000000..87bdcda3 --- /dev/null +++ b/workflow/basics/client/cancel.mdx @@ -0,0 +1,56 @@ +--- +title: "client.cancel" +--- + +There are multiple ways you can cancel workflow runs: + +- Pass one or more workflow run ids to cancel them +- Pass a workflow url to cancel all runs starting with this url +- cancel all pending or active workflow runs + +## Arguments + + + The set of workflow run IDs you want to cancel + + + + The URL address you want to filter while canceling + + + + Whether you want to cancel all workflow runs without any filter. + + +## Usage + +### Cancel a set of workflow runs + +```ts +// cancel a single workflow +await client.cancel({ ids: "" }); + +// cancel a set of workflow runs +await client.cancel({ ids: ["", ""] }); +``` + +### Cancel workflow runs with URL filter + +If you have an endpoint called `https://your-endpoint.com` and you +want to cancel all workflow runs on it, you can use `urlStartingWith`. + +Note that this will cancel workflows in all endpoints under +`https://your-endpoint.com`. + +```ts +await client.cancel({ urlStartingWith: "https://your-endpoint.com" }); +``` + +### Cancel _all_ workflows + +To cancel all pending and currently running workflows, you can +do it like this: + +```ts +await client.cancel({ all: true }); +``` diff --git a/workflow/basics/client/dlq/callback.mdx b/workflow/basics/client/dlq/callback.mdx new file mode 100644 index 00000000..7e26231d --- /dev/null +++ b/workflow/basics/client/dlq/callback.mdx @@ -0,0 +1,21 @@ +--- +title: "client.dlq.retryFailureFunction" +--- + +If a workflow's `failureFunction` or `failureUrl` request has failed, you can retry it using the `retryFailureFunction` method: + +## Arguments + +## Response + +## Usage +```ts +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }); + +// Retry the failure callback for a specific DLQ message +const response = await client.dlq.retryFailureFunction({ + dlqId: "dlq-12345" // The ID of the DLQ message to retry +}); +``` diff --git a/workflow/basics/client/dlq/list.mdx b/workflow/basics/client/dlq/list.mdx new file mode 100644 index 00000000..07726425 --- /dev/null +++ b/workflow/basics/client/dlq/list.mdx @@ -0,0 +1,76 @@ +--- +title: "client.dlq.list" +--- + +The `dlq.list` method retrieves messages that were sent to the **Dead Letter Queue (DLQ)**. + +DLQ messages represent failed workflow or QStash deliveries that could not be retried successfully. + +## Arguments + + + A pagination cursor from a previous request. + Use this to fetch the next batch of results. + + + + Maximum number of DLQ messages to return. + Defaults to a system-defined limit if not provided. + + + + Filter options for narrowing down DLQ messages + + + + Earliest timestamp (Unix ms) to include. + + + + Latest timestamp (Unix ms) to include. + + + + Filter messages that targeted a specific URL. + + + + Filter by HTTP response status code. + + + + +## Response + + + An array of DLQ messages that match the provided filters. + + + + A cursor to paginate through additional results. + If not returned, you have reached the end of the DLQ. + + +## Usage + +```ts +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }); + +// 👇 List all DLQ messages +const { messages, cursor } = await client.dlq.list({ filter: dlqFilters }); + +// 👇 List with pagination and filtering +const result = await client.dlq.list({ + cursor, + count: 10, + filter: { + fromDate: Date.now() - 86400000, // last 24 hours + toDate: Date.now(), + url: "https://your-endpoint.com", + responseStatus: 500 + } +}); + +``` diff --git a/workflow/basics/client/dlq/restart.mdx b/workflow/basics/client/dlq/restart.mdx new file mode 100644 index 00000000..1e8b4020 --- /dev/null +++ b/workflow/basics/client/dlq/restart.mdx @@ -0,0 +1,86 @@ +--- +title: "client.dlq.restart" +--- + +The `dlq.restart` method restarts one or more workflow runs from **Dead Letter Queue (DLQ)**. +This allows you to reprocess workflow runs that previously failed after exhausting retries. + +## Arguments + + + The DLQ entry ID or list of IDs to restart. + Use the `dlqId` field from messages returned by `client.dlq.list()`. + + + + An optional flow control configuration to limit concurrency and execution rate of restarted workflow runs. + + See [Flow Control](/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`. + + + + + + + Number of retry attempts to apply to the restarted workflow invocation. + Defaults to `3` if not provided. + + +## Response + +`client.dlq.restart()` returns one or more objects containing details of the restarted workflow run(s): + + + + + The ID of the new workflow run created from the restarted DLQ message. + + + + The Unix timestamp (in milliseconds) when the new workflow run was created. + + + + + + +## Usage + + +```ts Single +const { messages } = await client.dlq.list(); + +const response = await client.dlq.restart({ + dlqId: messages[0].dlqId, // Use the dlqId from the message + flowControl: { + key: "my-flow-control-key", + parallelism: 10, + }, + retries: 3, +}); + +``` +```ts Multiple +const responses = await client.dlq.restart({ + dlqId: ["dlq-12345", "dlq-67890"], + retries: 5, +}); +``` + + diff --git a/workflow/basics/client/dlq/resume.mdx b/workflow/basics/client/dlq/resume.mdx new file mode 100644 index 00000000..88b88792 --- /dev/null +++ b/workflow/basics/client/dlq/resume.mdx @@ -0,0 +1,83 @@ +--- +title: "client.dlq.resume" +--- + +The `dlq.resume` method resumes one or more workflow runs from the **Dead Letter Queue (DLQ)** at the point where they previously failed. +This allows you to continue execution from the failed step instead of restarting the workflow from the beginning. + +## Arguments + + + The DLQ entry ID or list of IDs to resume. + Use the `dlqId` field from messages returned by `client.dlq.list()`. + + + + An optional flow control configuration to limit concurrency and execution rate + of resumed workflow runs. + + See [Flow Control](/workflow/features/flow-control) for details. + + + + A logical grouping key that identifies which resumed runs share the same flow control limits. + + + + The maximum number of allowed resumption requests per second. + + + + The maximum number of resumed runs that can execute concurrently. + + + + The time window used to enforce the defined rate limit. Default is `1s`. + + + + + + Number of retry attempts to apply when resuming the workflow run. + Defaults to `3` if not provided. + + +## Response + +`client.dlq.resume()` returns one or more objects with details of the resumed workflow run(s): + + + + + The ID of the workflow run resumed from the DLQ message. + + + + The Unix timestamp (in milliseconds) when the resumed run was created. + + + + +## Usage + + +```ts Single +const { messages } = await client.dlq.list(); + +const response = await client.dlq.resume({ + dlqId: messages[0].dlqId, // Use the dlqId from the message + flowControl: { + key: "my-flow-control-key", + value: "my-flow-control-value", + }, + retries: 3, +}); + +``` +```ts Multiple +const responses = await client.dlq.resume({ + dlqId: ["dlq-12345", "dlq-67890"], + retries: 5, +}); +``` + diff --git a/workflow/basics/client/logs.mdx b/workflow/basics/client/logs.mdx new file mode 100644 index 00000000..8833adaf --- /dev/null +++ b/workflow/basics/client/logs.mdx @@ -0,0 +1,59 @@ +--- +title: "client.logs" +--- + +The `logs` method retrieves workflow run logs using the [List Workflow Runs API](/workflow/rest/runs/logs). + +## Arguments + + + Filter by a specific workflow run ID. + + + + Maximum number of runs to return. Defaults to a system-defined value if not specified. + + + + Filter workflow runs by execution state. + + | State | Description | + |--------------|--------------------------------------------| + | `RUN_STARTED` | The workflow run is in progress. | + | `RUN_SUCCESS` | The workflow run completed successfully. | + | `RUN_FAILED` | The run failed after all retries. | + | `RUN_CANCELED` | The run was manually canceled. | + + + + Filter by the exact workflow URL. + + + + Filter by the workflow creation time (Unix timestamp). + + + + A pagination cursor from a previous request. + Use this to fetch the next set of results. + + +## Response + + + A cursor to use for pagination. + If no cursor is returned, there are no more workflow runs. + + + + +--- + +## Usage + +```ts +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }) +const { runs, cursor } = await client.logs({}) +``` diff --git a/workflow/basics/client/notify.mdx b/workflow/basics/client/notify.mdx new file mode 100644 index 00000000..7a616910 --- /dev/null +++ b/workflow/basics/client/notify.mdx @@ -0,0 +1,38 @@ +--- +title: "client.notify" +--- + +The `notify` method notifies workflows that are waiting for a specific event. + +Workflows paused at a [`context.waitForEvent`](/workflow/basics/context/waitForEvent) step with the matching `eventId` will be resumed, and the provided `eventData` will be passed back to them. + +## Arguments + + + The identifier of the event to notify. + + + + Data to deliver to the waiting workflow(s). + This value will be returned in the `eventData` field of `context.waitForEvent`. + + +## Response + +Returns a list of `Waiter` objects representing the workflows that were notified: + + + +## Usage + +```javascript +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }); + +await client.notify({ + eventId: "my-event-id", + eventData: "my-data", // data passed to the workflow run +}); +``` + diff --git a/workflow/basics/client/trigger.mdx b/workflow/basics/client/trigger.mdx new file mode 100644 index 00000000..c927616e --- /dev/null +++ b/workflow/basics/client/trigger.mdx @@ -0,0 +1,132 @@ +--- +title: "client.trigger" +--- + +The `trigger` method starts a new workflow run and returns its `workflowRunId`. + +You can also trigger multiple workflow runs in a single call by passing an array of arguments instead of a single object. + +## Arguments + + + The public URL of the workflow endpoint. + + + + A custom identifier for the workflow run. + Each run must use a unique ID. + + The final ID will be prefixed with `wfr_`. + For example: passing `my-workflow` results in `wfr_my-workflow`. + + If omitted, a run ID will be generated automatically. + + + + The request payload to pass into the workflow run. + Accessible as `context.requestPayload` inside the workflow. + + + + HTTP headers to pass into the workflow run. + Accessible as `context.headers` inside the workflow. + + + + retry to use in the initial request. in the rest of the workflow, `retries` option of the `serve` will be used. + + + + delay between retries. + + + + An optional flow control configuration to limit concurrency and execution rate of the workflow runs. + + See [Flow Control](/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`. + + + + + + Delay for the workflow run. This is used to delay the execution of the workflow run. The delay is in seconds or can be passed as a string with a time unit (e.g. "1h", "30m", "15s"). + + + + Optionally set the absolute delay of this message. + This will override the delay option. + The message will not delivered until the specified time. + + Unix timestamp in seconds. + + + + If both `failureUrl` and `useFailureFunction` are provided, `useFailureFunction` takes precedence and the value of the `url` parameter is used as `failureUrl`. + + + + An optional label to assign to the workflow run. + This can be useful for identifying and filtering runs in the dashboard or logs. + + +## Usage + +```ts Single Workflow +import { Client } from "@upstash/workflow"; + +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 + retryDelay: "1000 * (1 + retried)", // optional delay between retries + delay: "10s" // optional delay value + failureUrl: "https://", // optional failure url + useFailureFunction: true, // whether a failure function is defined in the endpoint + flowControl: { // optional flow control + key: "USER_GIVEN_KEY", + rate: 10, + parallelism: 5, + period: "10m" + }, +}) +``` + +```ts Multiple Workflows +import { Client } from "@upstash/workflow"; + +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 +``` + diff --git a/workflow/basics/client/waiters.mdx b/workflow/basics/client/waiters.mdx new file mode 100644 index 00000000..69bf81b6 --- /dev/null +++ b/workflow/basics/client/waiters.mdx @@ -0,0 +1,31 @@ +--- +title: "client.getWaiters" +--- + +The `getWaiters` method retrieves all waiters that are currently listening for a given event. + +A **waiter** represents a workflow run that is paused at a `context.waitForEvent` step and is waiting for the specified `eventId`. + +## Arguments + + + The identifier of the event to look up. + + +## Response + +Returns a list of `Waiter` objects describing workflows that are waiting on the given event. + + + +## Usage + +```javascript +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }); + +const result = await client.getWaiters({ + eventId: "my-event-id", +}); +``` \ No newline at end of file diff --git a/workflow/basics/context.mdx b/workflow/basics/context.mdx index a6227ca9..18f8b153 100644 --- a/workflow/basics/context.mdx +++ b/workflow/basics/context.mdx @@ -1,615 +1,86 @@ --- -title: "Workflow Context" +title: "Overview" --- -A workflow's context is a JavaScript object provided by the serve function and is used to define your workflow endpoint. This context object offers utility methods for creating workflow steps, managing delays, and performing timeout-resistant HTTP calls. +A workflow's **context** is an object provided by the route function. - - -```typescript api/workflow/route.ts -import { serve } from "@upstash/workflow/nextjs"; - -export const { POST } = serve( - // 👇 the workflow context - async (context) => { - // ... - } -); -``` - -```python main.py -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext - -app = FastAPI() -serve = Serve(app) - - -@serve.post("/api/example") -async def example(context: AsyncWorkflowContext[str]) -> None: ... - -``` - - - -This context object provides utility methods to create workflow steps, wait for certain periods of time or perform timeout-resistant HTTP calls. - -Further, the context object provides all request headers, the incoming request payload and current workflow ID. - -## Context Object Properties - -- `qstashClient`: QStash client used by the serve method - -- `workflowRunId`: Current workflow run ID - -- `url`: Publically accessible workflow endpoint URL - -- `failureUrl`: URL for workflow failure notifications. - -- `requestPayload`: Incoming request payload - -- `rawInitialPayload`: String version of the initial payload - -- `headers`: Request headers - -- `env`: Environment variables - -## Core Workflow Methods - -### context.run - -Defines and executes a workflow step. - - -```typescript Serial execution (TypeScript) -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); - }); -}); - -``` - -```typescript Parallel execution (TypeScript) -import { serve } from "@upstash/workflow/nextjs" - -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]); - }, -); -``` - -```python Serial execution (Python) -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext - -app = FastAPI() -serve = Serve(app) - - -@serve.post("/api/example") -async def example(context: AsyncWorkflowContext[str]) -> None: - input = context.request_payload - - async def _step1(): - return some_work(input) - - result1 = await context.run("step-1", _step1) - - async def _step2(): - return some_other_work(result1) - - await context.run("step-2", _step2) - -``` - - - -### context.sleep - -Pauses workflow execution for a specified duration. - -Always `await` a `sleep` action to properly pause execution. +The context object provides: +- **Workflow APIs** – functions for defining workflow steps. +- **Workflow Run Properties** – request payload, request headers, and other metadata. + ```typescript api/workflow/route.ts highlight={4-5} + import { serve } from "@upstash/workflow/nextjs"; -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; -import { signIn, sendEmail } from "@/utils/onboarding-utils"; - -export const { POST } = serve(async (context) => { - const userData = context.requestPayload; - - const user = await context.run("sign-in", async () => { - const signedInUser = await signIn(userData); - return signedInUser; - }); - - // 👇 Wait for one day (in seconds) - await context.sleep("wait-until-welcome-email", "1d"); - - await context.run("send-welcome-email", async () => { - return sendEmail(user.name, user.email); - }); -}); -``` - -```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 - -app = FastAPI() -serve = Serve(app) - - -@serve.post("/api/onboarding") -async def onboarding(context: AsyncWorkflowContext[User]) -> None: - user_data = context.request_payload - - async def _sign_in(): - return await sign_in(user_data) - - user = await context.run("sign-in", _sign_in) + export const { POST } = serve( + // 👇 the workflow context + async (context) => { + // ... + } + ); + ``` - # 👇 Wait for one day (in seconds) - await context.sleep("wait-until-welcome-email", "1d") + ```python main.py + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext - async def _send_email(): - return await send_email(user.name, user.email) + app = FastAPI() + serve = Serve(app) - await context.run("send-welcome-email", _send_email) -``` + @serve.post("/api/example") + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` -### context.sleepUntil - -Pauses workflow execution until a specific timestamp. - -Always await a `sleepUntil` action to properly pause execution. - - - -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; -import { signIn, sendEmail } from "@/utils/onboarding-utils"; - -export const { POST } = serve(async (context) => { - const userData = context.requestPayload; - - const user = await context.run("sign-in", async () => { - return signIn(userData); - }); - - // 👇 Calculate the date for one week from now - const oneWeekFromNow = new Date(); - oneWeekFromNow.setDate(oneWeekFromNow.getDate() + 7); - - // 👇 Wait until the calculated date - await context.sleepUntil("wait-for-one-week", oneWeekFromNow); - - await context.run("send-welcome-email", async () => { - return sendEmail(user.name, user.email); - }); -}); -``` - -```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 - -app = FastAPI() -serve = Serve(app) - - -@serve.post("/api/onboarding") -async def onboarding(context: AsyncWorkflowContext[User]) -> None: - user_data = context.request_payload - - async def _sign_in(): - return await sign_in(user_data) - - user = await context.run("sign-in", _sign_in) - - # 👇 Calculate the date for one week from now - one_week_from_now = datetime.now() + timedelta(days=7) - - # 👇 Wait until the calculated date - await context.sleep_until("wait-for-one-week", one_week_from_now) - - async def _send_email(): - return await send_email(user.name, user.email) - - await context.run("send-welcome-email", _send_email) - -``` - - - -### context.call - -Performs an HTTP call as a workflow step, allowing for longer response times. - -Can take up to 15 minutes or 2 hours, depending on your [QStash plans](https://upstash.com/pricing/workflow) max HTTP connection timeout. - - - -```javascript TypeScript -import { serve } from "@upstash/workflow/nextjs"; - -export const { POST } = serve<{ topic: string }>(async (context) => { - const request = context.requestPayload; - - const { - status, // response status - headers, // response headers - body, // response body - } = await context.call( - "generate-long-essay", // Step name - { - url: "https://api.openai.com/v1/chat/completions", // Endpoint URL - method: "POST", - body: { - // Request 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: { - // request headers - authorization: `Bearer ${process.env.OPENAI_API_KEY}`, - }, - } - ); -}); - -``` - -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext - -app = FastAPI() -serve = Serve(app) - - -@dataclass -class Request: - topic: str - - -@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 - -``` - - - -If you want to call OpenAI, you can also use [`context.api.openai.call`](/workflow/basics/context#context-api). - - - If the endpoint you request does not return a success response (status code 200-299), - `context.call` will still be treated as successful, and the workflow will continue executing. - As a result, `failureFunction` or `failureUrl` will not be invoked. - -To handle non-success cases, you can check the `status` field in the response and implement custom logic as needed. - - - -context.call parameters are: - -- `url`: The URL to send the HTTP request to. -- `method`: The HTTP method to use for the request (e.g., GET, POST, PUT, etc.). Defaults to GET. -- `body`: Body to use in the request -- `headers`: An object representing the HTTP headers to include in the request. -- `retries`: The number of retry attempts to make if the request fails. [Retries use exponential backoff](https://upstash.com/docs/qstash/features/retry). Defaults to 0 (no retries). -- `retryDelay`: 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))`. -- `flowControl`: To limit the number of calls made to your endpoint. See [Flow Control](/workflow/howto/flow-control) for more details. The default is no limit. - - `key`: The key to use for flow control. - - `rate`: The maximum number of calls per second. - - `parallelism`: The maximum number of calls that can be active at the same time. - - `period`: Time window over which the rate limit is enforced. -- `timeout`: The maximum duration to wait for a response from the endpoint, in seconds. If retries are enabled, this timeout applies individually to each retry attempt. -- `workflow`: If you are using [`serveMany`](/workflow/howto/invoke#servemany), you can call another workflow defined under the same `serveMany` method by passing it to the `workflow` parameter of `context.call`. - -context.call attempts to parse the response body as JSON. If this is not possible, the body is returned as it is. - -In TypeScript, you can declare the expected result type as follows: - -```typescript -type ResultType = { - field1: string, - field2: number -}; - -const result = await context.call( ... ); -``` - - - The context.call method 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](http://localhost:3000/workflow/howto/local-development)) - - Make requests to internal Upstash QStash endpoints - - -### context.api - - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/workflow/roadmap) for feature parity plans and - [Changelog](/workflow/changelog) for updates. - - -In addition to context.call, another way to make third party requests is to use the context.api namespace. Under this namespace, you can find integrations for OpenAI, Anthropic and Resend. - -With context.api, you can call the available integrations in a typesafe manner. - - - -```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!'" }, - ], - }, -}); -``` - -```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", - }, -}); -``` - -
- -We are planning to add more integrations over time. - -You can learn more about these integrations under the [integrations section](/workflow/integrations/openai). - -### context.waitForEvent - - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/workflow/roadmap) for feature parity plans and - [Changelog](/workflow/changelog) for updates. - - -Stops the workflow run until it is notified externally to continue. - -There is also a timeout setting which makes the workflow continue if it's not notified within the time frame. - -```javascript -import { serve } from "@upstash/workflow/nextjs"; - -export const { POST } = serve<{ topic: string }>(async (context) => { - const request = context.requestPayload; - - const { - eventData, // data passed in notify - timeout, // boolean denoting whether the step was notified or timed out - } = await context.waitForEvent("wait for some event", "my-event-id", { - timeout: "1000s", // 1000 second timeout - }); -}); - -``` - -Default timeout value is 7 days. - -A workflow run waiting for event can be notified in two ways: - -- `context.notify`: [Notify step explained below](/workflow/basics/context#context-notify) -- `client.notify`: [Notify method of the Workflow Client](/workflow/basics/client#notify-waiting-workflow). - -### context.notify - - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/workflow/roadmap) for feature parity plans and - [Changelog](/workflow/changelog) for updates. - - -Notifies workflows waiting for an event with some payload. - -```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); -}); - -``` - -`notifyResponse` is a list of `NotifyResponse` objects: - -```typescript -export type NotifyResponse = { - waiter: Waiter; - messageId: string; - error: string; - workflowRunId: string; - workflowCreatedAt: number; -}; -``` - -More details about the `Waiter` object: - - - -### context.invoke - -Triggers another workflow run and waits for its completion. -The workflow continues when the invoked workflow finishes, whether successfully, with failure, or is canceled. -The `body` is the data returned by the invoked workflow, if any is returned. - -```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) - } -); -``` - -Only workflows that served together can invoke each other. To learn more about how you can serve multiple workflows together, -checkout [Invoke other workflows](/workflow/howto/invoke) guide. - -### context.cancel - - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/workflow/roadmap) for feature parity plans and - [Changelog](/workflow/changelog) for updates. - - -The methods we listed so far were all for defining a workflow step. `context.cancel` is different. - -`context.cancel` allows you to cancel the current workflow: +## Context Object Properties -```ts -export const { POST } = serve<{ topic: string }>(async (context) => { - const payload = context.requestPayload + + The request payload passed to the workflow run via `trigger()` call. + - const result = await context.run("check if canceled", () => { ... }); + + The request headers passed to the workflow run via `trigger()` call. + - if (result.cancel) { - await context.cancel() // cancel the workflow run - } -}) -``` + + The unique identifier of the current workflow run. + -The run will be labeled as cancelled, so failure function -is not triggered in this case, and no dlq entries generated. + + The public URL of the workflow endpoint. + -## Error handling and retries + + The URL used for workflow failure callback. -- `context.run` automatically retries on failures. Default is 3 retries with exponential backoff. -- `context.call` doesn't retry by default. If you wish to add retry, you can use `retries` option. -- Both `context.call` and `context.invoke` support the `retryDelay` parameter to customize the delay between retry attempts. You can use mathematical expressions with functions like `pow`, `sqrt`, `min`, `max`, etc., and the variable `retried` (current retry attempt count). -- `WorkflowNonRetryableError` allows you to fail workflow on will - without going into retry cycle. The run will be labeled as failed, - triggering the failure function and creating a dlq entry: + If a failure function is defined, this is the same as the workflow's `url`. + -```ts -export const { POST } = serve<{ topic: string }>(async (context) => { - const payload = context.requestPayload + + The environment variables available to the workflow. + - const result = await context.run("check if failed", () => { ... }); + + The QStash client instance used by the workflow endpoint. + - if (result.fail) { - throw new WorkflowNonRetryableError("error message") // fail the workflow run - } -}) -``` + + The label of the current workflow run, if set in [client.trigger](/workflow/basics/client/trigger). + -- Future releases will allow more configuration of retry behavior. +## Context Object Functions -## Limitations and Plan-Specific-Features +You can use the functions exposed by context object to define workflow steps. -- Sleep durations and HTTP timeouts vary based on your QStash plan. -- See your plan's "Max Delay" and "Max HTTP Connection Timeout" for specific limits. +- [context.run](/workflow/basics/context/run) +- [context.sleep](/workflow/basics/context/sleep) +- [context.sleepUntil](/workflow/basics/context/sleepUntil) +- [context.waitForEvent](/workflow/basics/context/waitForEvent) +- [context.notify](/workflow/basics/context/notify) +- [context.invoke](/workflow/basics/context/invoke) +- [context.call](/workflow/basics/context/call) +- [context.cancel](/workflow/basics/context/cancel) +- [context.api](/workflow/basics/context/api) diff --git a/workflow/basics/context/api.mdx b/workflow/basics/context/api.mdx new file mode 100644 index 00000000..f779d74d --- /dev/null +++ b/workflow/basics/context/api.mdx @@ -0,0 +1,64 @@ +--- +title: "context.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: [ + { + 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", + }, +}); +``` + +
+ +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. + +For detailed guides on usage and configuration, see the [Integrations section](/workflow/integrations/openai). \ No newline at end of file diff --git a/workflow/basics/context/call.mdx b/workflow/basics/context/call.mdx new file mode 100644 index 00000000..eb5ffc9e --- /dev/null +++ b/workflow/basics/context/call.mdx @@ -0,0 +1,189 @@ +--- +title: "context.call" +--- + +`context.call()` performs an HTTP request as a workflow step, supporting longer response times up to 12 hours. + +The request is executed by **Upstash on your behalf**, so your application does not consume compute resources during the request duration. + +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. + +If you want requests to retry automatically, you can explicitly pass a retry configuration. + +## Arguments + + + The URL of the HTTP endpoint to call. + + + + TThe HTTP method to use (`GET`, `POST`, `PUT`, etc.). Defaults to `GET`. + + + + The request body. + + + + 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](/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`](/workflow/howto/invoke#servemany), you can call another workflow defined in the same `serveMany` by passing it to this parameter. + + + + Whether to automatically stringify the body as JSON. Defaults to `true` + + If set to `false`, the body will be required to be a string and will be sent as-is. + + +## 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. + + + + 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 + + + +```javascript TypeScript +import { serve } from "@upstash/workflow/nextjs"; + +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: { + userId, + name + }, + headers: { + authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + }, + } + ); +}); + +``` + +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext + +app = FastAPI() +serve = Serve(app) + + +@dataclass +class Request: + topic: str + + +@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 + +``` + + + + + 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`](/workflow/basics/context#context-api) for details. + + + + 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](http://localhost:3000/workflow/howto/local-development)) + - Make requests to internal Upstash QStash endpoints. + diff --git a/workflow/basics/context/cancel.mdx b/workflow/basics/context/cancel.mdx new file mode 100644 index 00000000..5f800b4d --- /dev/null +++ b/workflow/basics/context/cancel.mdx @@ -0,0 +1,25 @@ +--- +title: "context.cancel" +--- + +All of the methods covered so far are used to define workflow steps. + +`context.cancel` is different — it allows you to **explicitly cancel the current workflow run**. + +```ts +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload + + const result = await context.run("check if canceled", () => { ... }); + + if (result.cancel) { + await context.cancel() // cancel the workflow run + } +}) +``` + +When a workflow run is canceled: + +- It is labeled as **canceled** (not failed). +- The configured `failureFunction` **is not triggered**. +- No entries are sent to the **dead-letter queue (DLQ)**. \ No newline at end of file diff --git a/workflow/basics/context/invoke.mdx b/workflow/basics/context/invoke.mdx new file mode 100644 index 00000000..0749246a --- /dev/null +++ b/workflow/basics/context/invoke.mdx @@ -0,0 +1,106 @@ +--- +title: "context.invoke" +--- + +`context.invoke()` triggers another workflow run and pauses until the invoked workflow finishes. + +The calling workflow resumes once the invoked workflow either **succeeds**, **fails**, or is **canceled**. + + + Workflows can only invoke other workflows that were served together in the same `serveMany` route. + For details, see [Invoke other workflows](/workflow/features/invoke). + + +## Arguments + + + The workflow definition to invoke. + Must be a workflow exposed under the same `serveMany`. + + + + The payload to send to the invoked workflow. + This value will be set as `context.requestPayload` in the invoked workflow. + + + + Optional HTTP headers to forward to the invoked workflow. + This value will be set as `context.headers` in the invoked workflow. + + + + Override the workflow run ID for the invoked workflow. + Defaults to a new ID if not specified. + + + + Number of retry attempts configuration of the invoked workflow. + Defaults to `3`. Retries use exponential backoff. + + + + Delay between retries of the invoked workflow. + + + + Flow control configuration of the invoked workflow. + + See [Flow Control](/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`. + + + + + + Whether to automatically stringify the body as JSON. Defaults to `true` + + If set to `false`, the body will be required to be a string and will be sent as-is. + + +## 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. + + +## Usage + +```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) + } +); +``` + + diff --git a/workflow/basics/context/notify.mdx b/workflow/basics/context/notify.mdx new file mode 100644 index 00000000..939f74d4 --- /dev/null +++ b/workflow/basics/context/notify.mdx @@ -0,0 +1,65 @@ +--- +title: "context.notify" +--- + +`context.notify()` notifies workflows that are waiting for a specific event, passing along an optional payload. + +It is typically used in combination with [`context.waitForEvent`](/workflow/basics/context#context-waitforevent). + +## Arguments + + + 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. + + +## Response + +`context.notify()` returns a list of waiters describing the workflows that were notified. + + + A list of `NotifyResponse` objects describing each workflow that was waiting on the event. + + + + 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. + + + + Unix timestamp (in milliseconds) representing when the workflow was created. + + + + + + + + +## Usage + +```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); +}); + +``` diff --git a/workflow/basics/context/run.mdx b/workflow/basics/context/run.mdx new file mode 100644 index 00000000..1f99fbfe --- /dev/null +++ b/workflow/basics/context/run.mdx @@ -0,0 +1,127 @@ +--- +title: "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 + + + 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); + }); + }); + + ``` + + ```typescript Parallel execution (TypeScript) + import { serve } from "@upstash/workflow/nextjs" + + 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]); + }, + ); + ``` + + ```python Serial execution (Python) + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext + + app = FastAPI() + serve = Serve(app) + + + @serve.post("/api/example") + async def example(context: AsyncWorkflowContext[str]) -> None: + input = context.request_payload + + async def _step1(): + return some_work(input) + + result1 = await context.run("step-1", _step1) + + async def _step2(): + return some_other_work(result1) + + await context.run("step-2", _step2) + + ``` + + + + + 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. + + To fix this, you can recreate the instance using Object.assign() or a custom factory: + ```typescript + export const { POST } = serve( + async (context) => { + + let user = await context.run("step-1", async () => { + // 👇 Return a class instance from step + return new User("John Doe", "john.doe@example.com"); + }); + + // 👇 Properties are accessible by default + console.log(user.name) + + // 👇 Create a Proper Instance with Object.assign() + user = Object.assign(new User(), user); + + await context.run("greet", async () => { + // 👇 Now instance methods are available as well + console.log(user.greet()); + }); + } + ); + ``` + diff --git a/workflow/basics/context/sleep.mdx b/workflow/basics/context/sleep.mdx new file mode 100644 index 00000000..3320dc23 --- /dev/null +++ b/workflow/basics/context/sleep.mdx @@ -0,0 +1,97 @@ +--- +title: "context.sleep" +--- + +`context.sleep()` pauses workflow execution for a specified duration. + +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. + +Always `await` a `sleep` step to properly pause execution. + +## Arguments + + + A unique identifier for the step. + + + + The duration to pause workflow execution. + + - **Human-readable string format:** + + | 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 | + + - **Numeric format (seconds):** + + | Input | Duration | + |---------|---------------| + | `60` | 60 seconds (1 minute) | + | `3600` | 3600 seconds (1 hour) | + | `86400` | 86400 seconds (1 day) | + + +## Usage + + + +```typescript TypeScript highlight={12-13} +import { serve } from "@upstash/workflow/nextjs"; +import { signIn, sendEmail } from "@/utils/onboarding-utils"; + +export const { POST } = serve(async (context) => { + const userData = context.requestPayload; + + const user = await context.run("sign-in", async () => { + const signedInUser = await signIn(userData); + return signedInUser; + }); + + // 👇 Wait for one day (in seconds) + await context.sleep("wait-until-welcome-email", "1d"); + + await context.run("send-welcome-email", async () => { + return sendEmail(user.name, user.email); + }); +}); +``` + +```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 + +app = FastAPI() +serve = Serve(app) + + +@serve.post("/api/onboarding") +async def onboarding(context: AsyncWorkflowContext[User]) -> None: + user_data = context.request_payload + + async def _sign_in(): + return await sign_in(user_data) + + user = await context.run("sign-in", _sign_in) + + # 👇 Wait for one day (in seconds) + await context.sleep("wait-until-welcome-email", "1d") + + async def _send_email(): + return await send_email(user.name, user.email) + + await context.run("send-welcome-email", _send_email) + +``` + + diff --git a/workflow/basics/context/sleepUntil.mdx b/workflow/basics/context/sleepUntil.mdx new file mode 100644 index 00000000..c72a00d0 --- /dev/null +++ b/workflow/basics/context/sleepUntil.mdx @@ -0,0 +1,88 @@ +--- +title: "context.sleepUntil" +--- + +`context.sleepUntil()` pauses workflow execution until a specific timestamp. + +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. + +Always await a `sleepUntil` step to properly pause execution. + +## Arguments + + + 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 + + + +```typescript TypeScript highlight={11-16} +import { serve } from "@upstash/workflow/nextjs"; +import { signIn, sendEmail } from "@/utils/onboarding-utils"; + +export const { POST } = serve(async (context) => { + const userData = context.requestPayload; + + const user = await context.run("sign-in", async () => { + return signIn(userData); + }); + + // 👇 Calculate the date for one week from now + const oneWeekFromNow = new Date(); + oneWeekFromNow.setDate(oneWeekFromNow.getDate() + 7); + + // 👇 Sleep until the calculated date + await context.sleepUntil("wait-for-one-week", oneWeekFromNow); + + await context.run("send-welcome-email", async () => { + return sendEmail(user.name, user.email); + }); +}); +``` + +```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 + +app = FastAPI() +serve = Serve(app) + + +@serve.post("/api/onboarding") +async def onboarding(context: AsyncWorkflowContext[User]) -> None: + user_data = context.request_payload + + async def _sign_in(): + return await sign_in(user_data) + + user = await context.run("sign-in", _sign_in) + + # 👇 Calculate the date for one week from now + one_week_from_now = datetime.now() + timedelta(days=7) + + # 👇 Wait until the calculated date + await context.sleep_until("wait-for-one-week", one_week_from_now) + + async def _send_email(): + return await send_email(user.name, user.email) + + await context.run("send-welcome-email", _send_email) + +``` + + + diff --git a/workflow/basics/context/waitForEvent.mdx b/workflow/basics/context/waitForEvent.mdx new file mode 100644 index 00000000..4c82153f --- /dev/null +++ b/workflow/basics/context/waitForEvent.mdx @@ -0,0 +1,55 @@ +--- +title: "context.waitForEvent" +--- + +`context.waitForEvent` pauses workflow execution until a given event occurs or a timeout is reached. + +Default timeout value is 7 days. + +## Arguments + + + A unique identifier for the step. + + + + A unique identifier for the event to wait on. + + + + The maximum time to wait before continuing execution. + + - **String format**: Human‑readable duration (e.g., `"10s"`, `"2h"`, `"1d"`). + - **Number format**: Duration in seconds (e.g., `60`, `3600`). + + Defaults to `7d` (7 days). + + +## Response + + + The data passed in when the event is triggered via `notify()`. + + + + `true` if execution resumed because the timeout elapsed, + `false` if resumed due to the event being received. + + +## Usage + +```javascript highlight={6-11} +import { serve } from "@upstash/workflow/nextjs"; + +export const { POST } = serve<{ topic: string }>(async (context) => { + const request = context.requestPayload; + + const { + eventData, + timeout, + } = await context.waitForEvent("wait for some event", "my-event-id", { + timeout: "1000s", // 1000 second timeout + }); +}); + +``` diff --git a/workflow/basics/how.mdx b/workflow/basics/how.mdx index 9d5345da..0bb3fc54 100644 --- a/workflow/basics/how.mdx +++ b/workflow/basics/how.mdx @@ -1,39 +1,65 @@ --- -title: "How Workflow works" +title: "How Workflow Works" --- -We created Upstash Workflow to help you ship **reliable code with minimal changes to your existing infrastructure**: +Upstash Workflow is an orchestration layer that allows you to write **multi‑step workflows** which are: -- No more serverless function timeouts -- Automatic recovery when a workflow fails mid-execution -- Automatic retries when your service is temporarily unavailable -- Easily monitor system activity in real-time +- **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 QStash, our serverless messaging and scheduling solution, to achieve these features. Here's a simplified breakdown of how it works: +Upstash Workflow is built on top of Upstash QStash, our serverless messaging and scheduling solution, to achieve these features. + +## 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. + +These approaches can work, but they often fail to handle production load reliably or become increasingly difficult to maintain over time: + +- **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. + +--- + +## How Upstash Workflow Solves This + +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. + +- 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 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.** -1. Call your workflow endpoint to trigger the workflow +--- -2. The Workflow SDK sends a request to QStash with an automatically generated workflow run ID, headers, and the initial data as the request body +## Extended Features -3. QStash calls your workflow endpoint with the current step to execute and the results of the previous steps: - - Skip successfully executed steps - - Assign previous step results to their respective variables - - Invoke the next step +Upstash Workflow extends the basic step model with additional primitives: ---- +- **Parallel Steps** + Define multiple steps (e.g. inside a `Promise.all()`). The engine detects independent work and runs steps concurrently as separate HTTP executions. + +- **Delays / Sleep** + `context.sleep` and `context.sleepUntil` allow pausing a workflow for hours, days, or even months. No compute is held during the wait time; execution resumes when the delay has expired. -In serverless environments, an API route is normally limited by a maximum function execution time (i.e. 10 seconds). By using `context.run`, _each step_ can now take up to the maximum duration, as each step is a separate HTTP request containing data from previous steps. +- **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. -As Upstash Workflow is built on top of QStash, the `context` methods map to QStash features: +- **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` -> regular QStash call -- `context.sleep` and `context.sleepUntil` -> QStash's [delay feature](/qstash/features/delay) -- `context.call` -> QStash's [callback feature](/qstash/features/callbacks) +--- -Retries, for example, are built into all QStash calls by default and, therefore, apply to every step in your workflow. For step-by-step resumability, QStash keeps a copy of your workflow state until the run is complete. +This architecture makes your serverless functions durable, reliable, and performance‑optimized, even in the face of runtime errors or temporary service outages. -This architecture makes your serverless functions **durable, reliable, and optimized for performance**, even during runtime errors or temporarily unavailable services. +It's quick and easy to get started: follow the [Quickstarts](/workflow/quickstarts/platforms) to define your first workflow in minutes. \ No newline at end of file diff --git a/workflow/basics/serve.mdx b/workflow/basics/serve.mdx index cb925dfc..5ee6b12d 100644 --- a/workflow/basics/serve.mdx +++ b/workflow/basics/serve.mdx @@ -1,508 +1,141 @@ --- -title: "Create a workflow" +title: "Overview" --- -Use the `serve` method to define an endpoint that serves a workflow. It provides a context object that you use to create all of your workflow's steps. +Use the `serve()` function to define an endpoint that runs a workflow. +It accepts two arguments: -For example, in a Next.js or FastAPI app: +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 TypeScript -import { serve } from "@upstash/workflow/nextjs"; + export const { POST } = serve(async (context) => { + // Route function + }, { + // Options + }); + ``` -export const { POST } = serve(async (context) => { - const result = await context.run("step-1", async () => { - // define a piece of business logic as step 1 - }); + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext - await context.run("step-2", async () => { - // define another piece of business logic as step 2 - }); -}); -``` + app = FastAPI() + serve = Serve(app) -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext -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" + result = await context.run("step-1", _step1) -@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" - - result = await context.run("step-1", _step1) - - async def _step2() -> None: - # define another piece of business logic as step 2 - pass - - await context.run("step-2", _step2) - -``` + async def _step2() -> None: + # define another piece of business logic as step 2 + pass + await context.run("step-2", _step2) + ``` -## Platform support +## Route Function -Besides Next.js and FastAPI, the `serve` method supports multiple frameworks and platforms. See [all supported platforms](/workflow/quickstarts/platforms) here. +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. -## Options - -### `failureUrl` +The context object provides: +- **Workflow APIs** – functions for defining workflow steps. +- **Workflow Run Properties** – request payload, request headers, and other metadata. -Use the `failureUrl` option to specify a URL your workflow will call if it exhausted all retries and fails. +For a full list of available APIs and properties, see the [Workflow Context](/workflow/basics/context) documentation. + ```typescript TypeScript highlight={4-9} + import { serve } from "@upstash/workflow/nextjs"; -```typescript Typescript -export const { POST } = serve( - async (context) => { ... }, - { - failureUrl: "https:///..." - } -); -``` + export const { POST } = serve( + async (context) => { + // 👇 Access context properties + const { userId } = context.requestPayload; + // 👇 Define a workflow step + await context.run("step-1", async () => {}) + } + ); + ``` -```python Python -@serve.post("/api/example", failureUrl="https:///...") -async def example(context: AsyncWorkflowContext[str]) -> None: ... -``` + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext - + app = FastAPI() + serve = Serve(app) - -If you add a `failureUrl` and use `client.trigger` to start your workflow, [you should pass `failureUrl` in `client.trigger` too](/workflow/basics/client#trigger-workflow). - -If specified, this URL will be called with [the failure callback payload](qstash/features/callbacks#what-is-a-failure-callback), and the error message will be included in the `body` field. + @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 default value is `undefined`, meaning no failure URL is called. + result = await context.run("step-1", _step1) -### `failureFunction` + async def _step2() -> None: + # define another piece of business logic as step 2 + pass -Use the `failureFunction` to define a function that's executed when your workflow exhausts all its [retries](/qstash/features/retry) and fails. - - - -```typescript TypeScript -export const { POST } = serve( - async (context) => { ... }, - { - failureFunction: async ({ - context, // context during failure - failStatus, // failure status - failResponse, // failure message - failHeaders // failure headers - }) => { - // 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 - -@serve.post("/api/example", failure_function=failure_function) -async def example(context: AsyncWorkflowContext[str]) -> None: ... -``` + await context.run("step-2", _step2) + ``` - -If you add a `failureFunction` and use `client.trigger` to start your workflow, [you should pass `useFailureFunction: true` in `client.trigger`](/workflow/basics/client#trigger-workflow). - - -If both `failureUrl` and `failureFunction` are provided, the failure function takes precedence and the value of `failureUrl` is ignored. - -By default, `failureFunction` is `undefined`, meaning that no function is executed on failure. - -### `retries` - -To specify the number of times QStash will call the workflow endpoint in case of errors, you can use the `verbose` parameter. - -The default value is 3. - - - -```typescript TypeScript -export const { POST } = serve( - async (context) => { ... }, - { - retries: 3 - } -); -``` - -```python Python -@serve.post("/api/example", retries=3) -async def example(context: AsyncWorkflowContext[str]) -> None: ... -``` - - - - -### `retryDelay` - -To specify the delay between retries, you can use the `retryDelay` option. - - - -```typescript TypeScript -export const { POST } = serve( - async (context) => { ... }, - { - retryDelay: "(retried + 1) * 1000" // delay in milliseconds - } -); -``` - - - -You can refer to the [QStash retry delay documentation](/qstash/features/retry#custom-retry-delay) for more details about retry delay. - - -### `flowControl` - -To control the rate of requests to your endpoint, use the `rate` and `period` options. To limit the maximum number of concurrent requests, use the `parallelism` option within the `flowControl` settings. See the [flow control section](/workflow/howto/flow-control) for more details. - - - -```typescript TypeScript -export const { POST } = serve( - async (context) => { ... }, - { - flowControl: { key: "aFlowControlKey", rate: 10, parallelism: 3 } - } -); -``` - - - -You can also pass only `rate` or `parallelism` if you only need one of the two. - -By default, there is no rate per second or parallelism limit. - - -### `verbose` - - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/workflow/roadmap) for feature parity plans and - [Changelog](/workflow/changelog) for updates. - - -To gain insights into how the workflow operates, you can enable verbose mode: - -```typescript -export const { POST } = serve( - async (context) => { ... }, - { - verbose: true - } -); -``` - -Each log entry has the following structure: - -``` -{ - timestamp: number, - workflowRunId: string, - logLevel: string, - eventType: string, - details: unknown, -} -``` - -The `eventType` can be: - -- `ENDPOINT_START` each time the workflow endpoint is called -- `RUN_SINGLE` or `RUN_PARALLEL` when step(s) are executed -- `SUBMIT_STEP` when a single step is executed -- `SUBMIT_FIRST_INVOCATION` when a new workflow run starts -- `SUBMIT_CLEANUP` when a workflow run finishes -- `SUBMIT_THIRD_PARTY_RESULT` when a third-party call result is received (see `context.call`) - -Verbose mode is disabled by default. - -### `initialPayloadParser` - -When calling the workflow endpoint to start a workflow run, your initial request's payload is expected to be either empty, a string, or JSON. - -If your payload differs, you can process it as needed using the `initialPayloadParser` option: - - - -```typescript TypeScript -type InitialPayload = { - foo: string; - bar: number; -}; - -// 👇 1: provide initial payload type -export const { POST } = serve( - async (context) => { - // 👇 3: parsing result is available as requestPayload - const payload: InitialPayload = context.requestPayload; - }, - { - // 👇 2: custom parsing for initial payload - initialPayloadParser: (initialPayload) => { - const payload: InitialPayload = parsePayload(initialPayload); - return payload; - }, - } -); -``` - -```python Python -@dataclass -class InitialPayload: - foo: str - bar: int - - -def initial_payload_parser(initial_payload: str) -> InitialPayload: - return parse_payload(initial_payload) - - -@serve.post("/api/example", initial_payload_parser=initial_payload_parser) -async def example(context: AsyncWorkflowContext[InitialPayload]) -> None: - payload: InitialPayload = context.request_payload - -``` - - - -### `url` - -Since a workflow run involves multiple calls to the workflow endpoint, the `serve` method needs to know where your endpoint is hosted. - -Typically, your workflow infers this using the `request.url` field. - -However, if you use a proxy or a local tunnel for development, you may want to override the URL inferred from `request.url`: - - - -```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: ... -``` - - - -By default, `url` is `undefined`, and the URL is inferred from `request.url`. - -### `baseUrl` - -An alternative to the `url` option is the `baseUrl` option. While `url` replaces the entire URL inferred from `request.url`, `baseUrl` only changes the base of the URL: - - - -```typescript TypeScript -export const { POST } = serve( - async (context) => { - ... - }, - // options: - { - baseUrl: "" - } -); -``` - -```python Python -@serve.post("/api/example", base_url="") -async def example(context: AsyncWorkflowContext[str]) -> None: ... - -``` - - - -The default value of `baseUrl` is `undefined`. - -If you have multiple endpoints and you don't want to set `baseUrl` in every single one, you can set the `UPSTASH_WORKFLOW_URL` environment variable to apply `baseUrl` across your entire application. - -Setting this environment variable is especially useful during [local development](/workflow/howto/local-development). In production, `baseUrl` or `UPSTASH_WORKFLOW_URL` are not necessary. - ---- - -## Further options - -The following options can be considered convenience methods. They are intended to support edge cases or testing pipelines and are **not required for regular use**. - -### `qstashClient` - -The `qstashClient` option allows you to pass a QStash Client explicitly. This can be helpful when using multiple QStash clients in the same project with different environment variables. - - - -```typescript TypeScript -import { Client } from "@upstash/qstash"; -import { serve } from "@upstash/workflow/nextjs"; - -export const { POST } = serve( - async (context) => { ... }, - { - qstashClient: new Client({ token: process.env.QSTASH_TOKEN }) - } -); -``` - -```python Python -from qstash import AsyncQStash - - -@serve.post("/api/example", qstash_client=AsyncQStash(os.environ["QSTASH_TOKEN"])) -async def example(context: AsyncWorkflowContext[str]) -> None: ... - -``` - - +## Options -By default, a `qstashClient` is initialized as: +Options provide additional configuration for workflow runs. +Most of them are advanced settings and are not required for typical use cases. See [Advanced Options](/workflow/basics/serve/advanced) for more details. -```typescript TypeScript -new Client({ - baseUrl: process.env.QSTASH_URL!, - token: process.env.QSTASH_TOKEN!, -}); -``` - -```python Python -AsyncQStash(os.environ["QSTASH_TOKEN"]) -``` + ```typescript TypeScript highlight={5-8} + import { serve } from "@upstash/workflow/nextjs"; - - -### `receiver` + export const { POST } = serve( + async (context) => { ... }, + // 👇 Workflow options + { + failureFunction: async ({ ... }) => {} + } + ); + ``` -You can pass a QStash Receiver to verify that **every** request the endpoint receives comes from QStash, preventing anyone from triggering your workflow. - - - -```typescript TypeScript -import { Receiver } from "@upstash/qstash"; -import { serve } from "@upstash/workflow/nextjs"; - -export const { POST } = serve( - async (context) => { ... }, - { - receiver: new Receiver({ - // 👇 grab these variables from your QStash dashboard - currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!, - nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!, - }) - } -); -``` - -```python Python -from qstash import Receiver - -@serve.post( - "/api/example", - receiver=Receiver( - current_signing_key=os.environ["QSTASH_CURRENT_SIGNING_KEY"], - next_signing_key=os.environ["QSTASH_NEXT_SIGNING_KEY"], - ), -) -async def example(context: AsyncWorkflowContext[str]) -> None: - ... -``` - - + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext -The default receiver is automatically used if the environment variables `QSTASH_CURRENT_SIGNING_KEY` and `QSTASH_NEXT_SIGNING_KEY` are set. If you want to turn off the Receiver, remove these environment variables or pass `receiver: undefined` in the options. Note that this will skip any verification that requests are coming from QStash and allow anyone to start your workflow. + app = FastAPI() + serve = Serve(app) -### `env` -By default, the SDK uses `process.env` to initialize the QStash client and the receiver (if the two environment variables are set). If `process.env` doesn't exist, the SDK won't be able to access the environment variables. In this case, you can either pass `qstashClient` and `receiver` options or use the `env` option. + @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" -If you pass `env`, this `env` will be used instead of `process.env`: + result = await context.run("step-1", _step1) - + async def _step2() -> None: + # define another piece of business logic as step 2 + pass -```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; - }, - { - receiver: new Receiver({ - // 👇 grab these variables from your QStash dashboard - currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!, - nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!, - }), - } -); -``` - -```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: - ... -``` + await context.run("step-2", _step2) + ``` - -### `disableTelemetry` - - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/workflow/roadmap) for feature parity plans and - [Changelog](/workflow/changelog) for updates. - - -By default, Workflow SDK sends telemetry about SDK version, framework or runtime. - -You can set `disableTelemetry` to `false` if you wish to disable this behavior. - -```typescript -import { serve } from "@upstash/workflow/nextjs"; - -export const { POST } = serve( - async (context) => { ... }, - { - disableTelemetry: true - } -); -``` diff --git a/workflow/basics/serve/advanced.mdx b/workflow/basics/serve/advanced.mdx new file mode 100644 index 00000000..193ed6ca --- /dev/null +++ b/workflow/basics/serve/advanced.mdx @@ -0,0 +1,462 @@ +--- +title: "Advanced Options" +--- + +Advanced Options are intended to support edge cases or testing pipelines and are **not required for regular use**. + + + + Defines a function that executes if the workflow fails after all retries are exhausted. + + For details, see [failureFunction](/workflow/features/failure-callback). + + + When adding a `failureFunction`, you must set `useFailureFunction: true` in `client.trigger()` when starting a workflow run. + + + + ```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 + + @serve.post("/api/example", failure_function=failure_function) + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` + + + + + + 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](/workflow/features/failureFunction/advanced). + + + When adding a `failureUrl`, you must set `failureUrl` in `client.trigger()` when starting a workflow run. + + + + + ```typescript Typescript + export const { POST } = serve( + async (context) => { ... }, + { + failureUrl: "https:///..." + } + ); + ``` + + ```python Python + @serve.post("/api/example", failureUrl="https:///...") + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` + + + + + + Defines the number of retry attempts if a workflow step fails. + The default value is 3. + + For details, see [retry configuration](/workflow/features/retries/configuration). + + + We recommend configuring workflow runs when starting them with `client.trigger()`, rather than applying configuration on the server side. + + See [Configure a Run](/workflow/howto/configure) for details. + + + + + ```typescript TypeScript + export const { POST } = serve( + async (context) => { ... }, + { + retries: 3 + } + ); + ``` + + ```python Python + @serve.post("/api/example", retries=3) + async def example(context: AsyncWorkflowContext[str]) -> None: ... + ``` + + + + + Defines the delay between retry attempts. + This option accepts an expression that evaluates to the number of milliseconds. + + You can use the `retried` variable—which starts at 0 for the first retry—to compute a dynamic delay. + For a constant delay, provide a fixed millisecond value. + + For details, see [retry configuration](/workflow/features/retries/configuration). + + + We recommend configuring workflow runs when starting them with `client.trigger()`, rather than applying configuration on the server side. + + See [Configure a Run](/workflow/howto/configure) for details. + + + + + ```typescript TypeScript + export const { POST } = serve( + async (context) => { ... }, + { + retryDelay: "(retried + 1) * 1000" // delay in milliseconds + } + ); + ``` + + + + + + Applies throttling to workflow execution using rate limits or concurrency limits. + + See [flow control](/workflow/features/flow-control) for details. + + + We recommend configuring workflow runs when starting them with `client.trigger()`, rather than applying configuration on the server side. + + See [Configure a Run](/workflow/howto/configure) for details. + + + + + A logical grouping key that identifies which executions 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. + + + + + + ```typescript TypeScript + export const { POST } = serve( + async (context) => { ... }, + { + flowControl: { key: "custom-flow-control-key", rate: 10, parallelism: 3 } + } + ); + ``` + + + + + + Enables custom parsing of the initial request payload. + + 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. + + + + ```typescript TypeScript + type InitialPayload = { + foo: string; + bar: number; + }; + + // 👇 1: provide initial payload type + export const { POST } = serve( + async (context) => { + // 👇 3: parsing result is available as requestPayload + const payload: InitialPayload = context.requestPayload; + }, + { + // 👇 2: custom parsing for initial payload + initialPayloadParser: (initialPayload) => { + const payload: InitialPayload = parsePayload(initialPayload); + return payload; + }, + } + ); + ``` + + ```python Python + @dataclass + class InitialPayload: + foo: str + bar: int + + + def initial_payload_parser(initial_payload: str) -> InitialPayload: + return parse_payload(initial_payload) + + + @serve.post("/api/example", initial_payload_parser=initial_payload_parser) + async def example(context: AsyncWorkflowContext[InitialPayload]) -> None: + payload: InitialPayload = context.request_payload + + ``` + + + + + + + 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: ... + ``` + + + + + + + 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. + + + If you have multiple workflow endpoints, you can set the `UPSTASH_WORKFLOW_URL` environment variable instead of configuring `baseUrl` on each endpoint. + The `UPSTASH_WORKFLOW_URL` environment variable corresponds directly to this option and configures it globally. + + + + + + ```typescript TypeScript + export const { POST } = serve( + async (context) => { + ... + }, + // options: + { + baseUrl: "" + } + ); + ``` + + ```python Python + @serve.post("/api/example", base_url="") + async def example(context: AsyncWorkflowContext[str]) -> None: ... + + ``` + + + + + + + + + Use `qstashClient` if you want to provide your own QStash client instead of letting Workflow use the default from environment variables. + + This is useful if you're working with multiple QStash projects in the same app. + + + + ```typescript TypeScript + import { Client } from "@upstash/qstash"; + import { serve } from "@upstash/workflow/nextjs"; + + export const { POST } = serve( + async (context) => { ... }, + { + qstashClient: new Client({ token: "" }) + } + ); + ``` + + ```python Python + from qstash import AsyncQStash + + + @serve.post("/api/example", qstash_client=AsyncQStash(os.environ["QSTASH_TOKEN"])) + async def example(context: AsyncWorkflowContext[str]) -> None: ... + + ``` + + + + + + + + + The `Receiver` verifies that every request to your endpoint actually comes from QStash, blocking anyone else from triggering your workflow. + + The `receiver` option allows you to pass a QStash Receiver explicitly. + + 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. + + + + ```typescript TypeScript + import { Receiver } from "@upstash/qstash"; + import { serve } from "@upstash/workflow/nextjs"; + + export const { POST } = serve( + async (context) => { ... }, + { + receiver: new Receiver({ + currentSigningKey: "", + nextSigningKey: "", + }) + } + ); + ``` + + ```python Python + from qstash import Receiver + + @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: + ... + ``` + + + + + + + + +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`. + +Inside your workflow, these values are also exposed on `context.env`. + + + +```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: "", + } + } +); +``` + +```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: + ... +``` + + + + + + + + + Enables verbose mode to print detailed logs of workflow execution to the application's `stdout`. + + Verbose mode is disabled by default. + + ```typescript + export const { POST } = serve( + async (context) => { ... }, + { + verbose: true + } + ); + ``` + + Each log entry has the following structure: + + ``` + { + timestamp: number, + workflowRunId: string, + logLevel: string, + eventType: string, + details: unknown, + } + ``` + + |eventType| Description| + |--|--| + `ENDPOINT_START`| each time the workflow endpoint is called + `RUN_SINGLE` or `RUN_PARALLEL` | when step(s) are executed + `SUBMIT_STEP` | when a single step is executed + `SUBMIT_FIRST_INVOCATION` | when a new workflow run starts + `SUBMIT_CLEANUP` | when a workflow run finishes + `SUBMIT_THIRD_PARTY_RESULT` | when a third-party call result is received (see `context.call`) + + + + diff --git a/workflow/changelog.mdx b/workflow/changelog.mdx index e39dcd45..4178b8aa 100644 --- a/workflow/changelog.mdx +++ b/workflow/changelog.mdx @@ -74,10 +74,10 @@ title: Changelog - 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](/workflow/agents/features#tools). - Added `context.invoke` to call one workflow from another with full type-safety. See the guide [here](/workflow/howto/invoke). - - Introduced flow control parameters to limit the rate or concurrency of workflow runs. Learn more [here](/workflow/howto/flow-control). + - Introduced flow control parameters to limit the rate or concurrency of workflow runs. Learn more [here](/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](/workflow/howto/flow-control). + - Added RateLimit and Parallelism controls to manage the frequency and concurrency of workflow runs. Learn more [here](/workflow/features/flow-control). diff --git a/workflow/features/dlq.mdx b/workflow/features/dlq.mdx new file mode 100644 index 00000000..b43eb496 --- /dev/null +++ b/workflow/features/dlq.mdx @@ -0,0 +1,38 @@ +--- +title: "Overview" +--- + +The Dead Letter Queue (DLQ) automatically captures failed workflow runs that have exhausted all retry attempts. + +This ensures that no workflow execution is lost and provides multiple options for recovering from failures gracefully. + +## How it works? + +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. + + + + + +The DLQ serves as a safety net, preserving failed workflow runs with their complete execution context. + + + 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 + + After the retention duration expires, DLQ items are automatically removed and cannot be recovered. + + +## Recovery Actions + +Once a workflow run is in the DLQ, you can take the following actions: + +- **[Restart](/workflow/features/dlq/restart)** – trigger the workflow from the beginning. +- **[Resume](/workflow/features/dlq/resume)** – continue the workflow from the point of failure. +- **[Re-run Failure Function](/workflow/features/dlq/callback)** – execute the workflow's failure handling logic again. +- or delete 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. \ No newline at end of file diff --git a/workflow/features/dlq/callback.mdx b/workflow/features/dlq/callback.mdx new file mode 100644 index 00000000..f50752f1 --- /dev/null +++ b/workflow/features/dlq/callback.mdx @@ -0,0 +1,42 @@ +--- +title: "Rerun Failure Function" +--- + +The **Rerun Failure Function** action allows you to retry the failure function that executes when a workflow run enters the Dead Letter Queue (DLQ). + +The failure function is typically a cleanup or notification operation that runs automatically whenever a workflow is moved to the DLQ. + +This feature is particularly helpful for: + +- 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. + +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. + + + + + + +You can perform this action programmatically as well: + + + ```typescript TypeScript + import { Client } from "@upstash/workflow"; + + const client = new Client({ token: "" }); + + await client.dlq.retryFailureFunction({ + dlqId: "dlq-12345", + }); + ``` + + + + This action is only available if the failure function itself has failed as well. + If the failure function already succeeded, it cannot be rerun. + + You can view the status of the failure function in the **DLQ** and **Logs** dashboards, which indicate whether it succeeded or failed. + + diff --git a/workflow/features/dlq/restart.mdx b/workflow/features/dlq/restart.mdx new file mode 100644 index 00000000..f9ac4441 --- /dev/null +++ b/workflow/features/dlq/restart.mdx @@ -0,0 +1,33 @@ +--- +title: "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, + }); + ``` + + + diff --git a/workflow/features/dlq/resume.mdx b/workflow/features/dlq/resume.mdx new file mode 100644 index 00000000..6fd669d1 --- /dev/null +++ b/workflow/features/dlq/resume.mdx @@ -0,0 +1,37 @@ +--- +title: "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. + + + + + +You can perform this action programmatically as well: + + + ```typescript TypeScript + import { Client } from "@upstash/workflow"; + + const client = new Client({ token: "" }); + + await client.dlq.resume({ + dlqId: "dlq-12345", + retries: 3, + }); + ``` + + + + 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](/workflow/howto/changes) page. + diff --git a/workflow/features/failure-callback.mdx b/workflow/features/failure-callback.mdx new file mode 100644 index 00000000..75b0bcd9 --- /dev/null +++ b/workflow/features/failure-callback.mdx @@ -0,0 +1,118 @@ +--- +title: "Overview" +--- + +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. + +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( + async (context) => { + // Your workflow logic... + }, + { + failureFunction: async ({ + context, + failStatus, + failResponse, + failHeaders, + }) => { + + // 👇 Log error to monitoring system + await logToSentry(...); + + // 👇 Send alert to team + await sendSlackAlert(...); + + // 👇 Perform cleanup operations + await cleanupWorkflowResources(...); + }, + } +``` +); + + + +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. + + + 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. + + Read more here: [securing your workflow endpoint](/workflow/howto/security). + + + + + + +## Parameters + +The `failureFunction` receives an object with the following parameters: + + + The workflow context object containing: + + + + The ID of the failed workflow run + + + + The publicly accessible workflow endpoint URL + + + + The original request payload that triggered the workflow + + + + The original request headers + + + + Environment variables + + + + + + The HTTP status code returned by the failed workflow step. + + + + The response body returned by the failed workflow step. + + + + The response headers returned by the failed workflow step. + + +## Configuration + +You can enable failure function for a workflow run when starting it: + +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }) + +const { workflowRunId } = await client.trigger({ + url: "https:///", + // 👇 Activates the failure function execution if it is defined + useFailureFunction: true, + applyConfiguration: true, +}) +``` \ No newline at end of file diff --git a/workflow/features/failureFunction/advanced.mdx b/workflow/features/failureFunction/advanced.mdx new file mode 100644 index 00000000..ef88bac0 --- /dev/null +++ b/workflow/features/failureFunction/advanced.mdx @@ -0,0 +1,37 @@ +--- +title: "Advanced failureUrl Option" +--- + +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. + +You can use either `failureFunction` or `failureUrl`, but not both. These options are mutually exclusive. + +For most users, **Failure Function** is the better choice because: +- It runs alongside your workflow and has access to the same context and dependencies +- Failure function requests are automatically retried on failure as well. +- You can manually retry failure function if it fails via DLQ. + +If you think this advanced option fits your need, you can configure it by passing `failureUrl` configuration. + + + ```typescript + import { Client } from "@upstash/workflow"; + + const client = new Client({ token: "" }) + + const { workflowRunId } = await client.trigger({ + url: "https:///workflow" + failureUrl: "https:///workflow-failure", + applyConfiguration: true, + }) + ``` + + ```python Python + @serve.post("/api/example", failure_url="https:///workflow-failure") + async def example(context: AsyncWorkflowContext[str]) -> None: + # Your workflow logic... + pass + ``` + + diff --git a/workflow/features/failureFunction/reliability.mdx b/workflow/features/failureFunction/reliability.mdx new file mode 100644 index 00000000..a8285b04 --- /dev/null +++ b/workflow/features/failureFunction/reliability.mdx @@ -0,0 +1,33 @@ +--- +title: "Reliability of Failure Function" +--- + +The failure function is executed whenever a workflow run fails. + +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. + +You can view and filter workflow runs with failed failure function executions in the DLQ dashboard. + + + + + +From the DLQ dashboard, you can retry the failure function. + + + + + +You can perform this action programmatically as well: + +```ts +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }); + +const response = await client.dlq.retryFailureFunction({ + dlqId: "dlq-12345" // The ID of the DLQ message to retry +}); +``` \ No newline at end of file diff --git a/workflow/features/flow-control.mdx b/workflow/features/flow-control.mdx new file mode 100644 index 00000000..bc8d61d3 --- /dev/null +++ b/workflow/features/flow-control.mdx @@ -0,0 +1,133 @@ +--- +title: "Overview" +--- + +Flow Control allows you to limit how many workflow steps are executed by delaying and queuing their delivery. + +This feature helps to: +- Manage resource consumption +- Prevent violations of external API rate limits +- Ensure workflows run within defined system constraints + +## 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. + +There are two main parameters to configure: + +- [Rate and Period](/workflow/features/flow-control/rate-period): Maximum number of steps that may start within a time window +- [Parallelism](/workflow/features/flow-control/parallelism): Maximum number of steps allowed to run concurrently + +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. + +## Example + +Suppose you have the following workflow: + +```typescript +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload + + await context.run("step-1", () => { ... }); + + await context.run("step-2", () => { ... }); + + await context.run("step-3", () => { ... }); +}) +``` + +Now imagine you trigger **N workflow runs** for this workflow with the following configuration: + +```typescript +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "fw_example", + parallelism: 7, + rate: 3, + period: "1m", + } + applyConfiguration: true, +}) +``` + +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. + + + + + +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. + +Steps that exceed these limits are automatically queued and executed later. + + + + + +Note that each step above corresponds to a separate workflow run. +Because this workflow is sequential, each workflow run has only one pending step at a time. +In workflows with **parallel branches**, multiple steps from the same workflow run may appear in the schedule simultaneously. + +Parallelism slots are consumed by running steps. +If no slots are available, new steps enter the **waitlist** until resources free up: + + + + + + +Upstash Workflow does not support per-step level configuration. Meaning that you can attach a flow-control configuration +to the workflow run and all the steps will inherit to the same limits. +Following the analogy above, you cannot enforce parallelism limit on "green" steps natively. + +The context.call and context.invoke steps are exception this to this rule and accept their own flow control configuration: + +- [context.call](/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](/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. + +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. + +See [Advanced Per-Step Configuration](/workflow/howto/configure/per-step-configuration) for more details. + + + +## Configuration + +You can configure flow control when starting a workflow run: + +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }) + +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "user-signup", + parallelism: 1, + rate: 10, + period: 100, + } + applyConfiguration: true, +}) +``` + +All steps within a workflow run will adhere to the specified flow control configuration. + + +Keep in mind that rate/period and parallelism info are kept on each step separately. +If you change the rate/period or parallelism on a new deployment, the old fired ones will not be affected. +They will keep their flow control configuration. + +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. + diff --git a/workflow/features/flow-control/parallelism.mdx b/workflow/features/flow-control/parallelism.mdx new file mode 100644 index 00000000..75db8254 --- /dev/null +++ b/workflow/features/flow-control/parallelism.mdx @@ -0,0 +1,77 @@ +--- +title: "Parallelism" +--- + +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. + +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }) + +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "user-signup", + parallelism: 10, + } + applyConfiguration: true, +}) +``` + + +**Example**: +If `parallelism = 3`, at most 3 requests can run concurrently. + +When tokens are available, requests acquire one and start execution: + + + + +When all tokens are in use, additional requests are not failed — they’re queued in a **waitlist**: + + + + +The step in the waitlist will wait for a step to complete and hand off it's token to a pending request: + + + Token handoff does not guarantee strict ordering. + A later request in the waitlist may acquire a token before an earlier one. + + + + + + +## Monitoring + +You can monitor wait list size of your flow control key's using the REST API. + + + +```bash Single Flow Control Key +curl -X GET https://qstash.upstash.io/v2/flowControl/YOUR_FLOW_CONTROL_KEY \ + -H "Authorization: Bearer " +``` + +```bash List All Flow Control Keys +curl -X GET https://qstash.upstash.io/v2/flowControl/ \ + -H "Authorization: Bearer " +``` + + +It will return the wait list size. In case you request all flow-control keys, it is an array response. + + + The identifier for your flow control configuration + + + + Number of steps waiting to be executed due to parallelism limits + + + + Adding a dashboard to list and manage flow control key's is on our roadmap. + diff --git a/workflow/features/flow-control/rate-period.mdx b/workflow/features/flow-control/rate-period.mdx new file mode 100644 index 00000000..6de61627 --- /dev/null +++ b/workflow/features/flow-control/rate-period.mdx @@ -0,0 +1,47 @@ +--- +title: "Rate and Period" +--- + +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: "" }) + +const { workflowRunId } = await client.trigger({ + url: "https:///", + flowControl: { + key: "user-signup", + rate: 10, + period: 100, + } + applyConfiguration: true, +}) +``` + + +**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: + + + + + +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: + + + + + +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. \ No newline at end of file diff --git a/workflow/features/invoke.mdx b/workflow/features/invoke.mdx new file mode 100644 index 00000000..ff62bbca --- /dev/null +++ b/workflow/features/invoke.mdx @@ -0,0 +1,41 @@ +--- +title: "Overview" +--- + +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. + +When you use `context.invoke`, invoking workflow will wait until the invoked workflow finishes before running the next step. + + + + +```typescript +const { + body, // response from the invoked workflow + isFailed, // whether the invoked workflow was canceled + isCanceled // whether the invoked workflow failed +} = await context.invoke( + "analyze-content", + { + workflow: analyzeContent, + body: "test", + header: {...}, // headers to pass to anotherWorkflow (optional) + retries, // number of retries (optional, default: 3) + flowControl, // flow control settings (optional) + workflowRunId // workflowRunId to set (optional) + } +) +``` + +You can return a response from a workflow, which will be delivered to invoker workflow run. + + + + + + + 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. + + + diff --git a/workflow/features/invoke/serveMany.mdx b/workflow/features/invoke/serveMany.mdx new file mode 100644 index 00000000..6a4ec141 --- /dev/null +++ b/workflow/features/invoke/serveMany.mdx @@ -0,0 +1,108 @@ +--- +title: "Using Serve Many" +--- + +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 + +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. + + It works just like `serve()`—accepting the same arguments—but does **not** expose the workflow directly as an HTTP endpoint. + Instead, it simply initializes a workflow object. + + ```typescript + const anotherWorkflow = createWorkflow( + // 👇 Request Payload Type + async (context: WorkflowContext) => { + + await context.sleep("wait 1 second", 1) + + // 👇 Workflow Response Type + return { message: "This is the data returned by the workflow" }; + } + ); + + const someWorkflow = createWorkflow(async (context) => { + // 👇 Invoke the workflow with type-safe call + const { body } = await context.invoke( + "invoke anotherWorkflow", + { + workflow: anotherWorkflow, + 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. + + ```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. + + ```ts + const someWorkflow = createWorkflow(async (context) => { + // 👇 Invoke the workflow with type-safe call + const { body } = await context.invoke( + "invoke anotherWorkflow", + { + // 👇 Pass the workflow object as argument + workflow: anotherWorkflow, + body: "user-1" + } + ), + }); + ``` + + + + + In this example, both `workflowOne` and `workflowTwo` are exposed through `serveMany`, sharing the same parent path. + + You can start `workflowOne` by sending a trigger request to: + `https://your-app/serve-many/workflow-one-route`. + + ```typescript + import { Client } from "@upstash/workflow"; + + const client = new Client({ token: "" }) + + const { workflowRunId } = await client.trigger({ + // 👇 URL of workflow one + url: "https://your-app/serve-many/workflow-one-route", + applyConfiguration: true, + }) + ``` + + 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`. + + diff --git a/workflow/features/notify.mdx b/workflow/features/notify.mdx new file mode 100644 index 00000000..ec054825 --- /dev/null +++ b/workflow/features/notify.mdx @@ -0,0 +1,116 @@ +--- +title: "Notify" +--- + +You can notify all the workflow runs waitingi for a specific event ID. +There are two ways to send a notify request. + + +## Notify within Workflow + +Notifies other workflows waiting for a specific event from within a workflow. + + +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; + +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() + } + ); + +}); +``` + +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext +from datetime import datetime + +app = FastAPI() +serve = Serve(app) + +@serve.post("/api/order-processor") +async def order_processor(context: AsyncWorkflowContext[str]) -> None: + order_id = context.request_payload["order_id"] + processing_result = context.request_payload["processing_result"] + + # Process the order + async def _process_order(): + return await process_order(order_id) + + result = await context.run("process-order", _process_order) + + # 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() + } + ) + + # 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) +``` + + +## External Notification + +You can also notify workflows from external systems using the Workflow Client: + + +```typescript TypeScript +import { Client } from "@upstash/workflow"; + +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" + } +) +``` + \ No newline at end of file diff --git a/workflow/features/parallel-steps.mdx b/workflow/features/parallel-steps.mdx new file mode 100644 index 00000000..46d07494 --- /dev/null +++ b/workflow/features/parallel-steps.mdx @@ -0,0 +1,41 @@ +--- +title: "Parallel Steps" +--- + +Upstash Workflow supports executing multiple steps in parallel. + +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"; + +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")), + ]); + +}); +``` + +The results of the parallel steps are available as usual once awaited. + +The dashboard visualizes parallel execution as shown below: + + + + + +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. + \ No newline at end of file diff --git a/workflow/features/retries.mdx b/workflow/features/retries.mdx new file mode 100644 index 00000000..64002899 --- /dev/null +++ b/workflow/features/retries.mdx @@ -0,0 +1,73 @@ +--- +title: "Overview" +--- + +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 + +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. + + + + + +By default, the retry count is set to **3**, and an **exponential backoff** delay strategy is used. + +```javascript Default Backoff Algorithm +// n = how many times this request has been retried +delay = min(86400, e ** (2.5*n)) // in seconds +``` + +| Retry Attempt | Algorithm | Delay | +|---------------|--------------|--------| +| 1 | $$e^{2.5}$$ | 12s | +| 2 | $$e^5$$ | 2m28s | +| 3 | $$e^{7.5}$$ | 30m8s | +| 4+ | $$86400$$ | 24h | + +## Configuration + +You can configure retry behavior when starting a new workflow run. + +### Configure Retry Attempt Count + +You can specify how many times a step should be retried upon failure. + +```typescript Configure Retry Attempt Count +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }) + +const { workflowRunId } = await client.trigger({ + url: "https:///", + retries: 3, + applyConfiguration: true, +}) +``` + +### Configure Retry Delay Strategy + +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). + +To apply a constant delay, you can simply provide a fixed value. + +The expression must return the delay in **milliseconds**. + +```typescript Configure Retry Delay Strategy +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }) + +const { workflowRunId } = await client.trigger({ + url: "https:///", + retries: 3, + retryDelay: "(1 + retried) * 1000", + applyConfiguration: true, +}) +``` diff --git a/workflow/features/retries/prevent-retries.mdx b/workflow/features/retries/prevent-retries.mdx new file mode 100644 index 00000000..a7eb7984 --- /dev/null +++ b/workflow/features/retries/prevent-retries.mdx @@ -0,0 +1,140 @@ +--- +title: "Prevent Retries" +--- + +It is recommended to enable retries for workflow runs to improve reliability. + +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. + +## Using `WorkflowNonRetryableError` + +`WorkflowNonRetryableError` lets you explicitly fail a workflow without entering the retry cycle. + +When thrown, the workflow run is marked as failed, which: +- Triggers the failure function (if defined) +- Sends the workflow run to the DLQ + +```ts TypeScript highlight={7} +export const { POST } = serve<{ topic: string }>(async (context) => { + const payload = context.requestPayload + + const isExists = await context.run("is-user-exists", () => { ... }); + + if (!isExists) { + throw new WorkflowNonRetryableError("The user does not exists!") + } +}) +``` + +## Using `context.cancel()` + +You can cancel a workflow run explicitly from inside the workflow. + +When canceled, the run is labeled as canceled instead of failed. This means: +- The failure handler will **NOT** be triggered +- The workflow will **NOT** be sent to the DLQ + + +```typescript highlight={10-11} TypeScript +export const { POST } = serve<{ orderId: string }>(async (context) => { + const { orderId } = context.requestPayload; + + // 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; + } + + // Continue processing if order is valid + await context.run("process-order", async () => { + return await processOrder(orderId); + }); +}); +``` + +```python Python +@serve.post("/graceful-cancellation") +async def graceful_cancellation(context: AsyncWorkflowContext[dict]) -> None: + order_id = context.request_payload["order_id"] + + async def _check_order_status(): + return await get_order_status(order_id) + + # Check if order is still valid + order_status = await context.run("check-order-status", _check_order_status) + + 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) + + await context.run("process-order", _process_order) +``` + + +## Using conditional execution + +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 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); + }); + + if (orderStatus === "not-found") { + // Stop execution without error + return; + } + + // Continue processing if order is valid + await context.run("process-order", async () => { + return await processOrder(orderId); + }); + }); + ``` + + ```python Python + @serve.post("/conditional-execution") + async def conditional_execution(context: AsyncWorkflowContext[dict]) -> None: + data = context.request_payload["data"] + + async def _validate_data(): + return validate_input_data(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"]) + + 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) + + await context.run("process-valid-data", _process_valid_data) + ``` + + diff --git a/workflow/features/sleep.mdx b/workflow/features/sleep.mdx new file mode 100644 index 00000000..353b58c3 --- /dev/null +++ b/workflow/features/sleep.mdx @@ -0,0 +1,165 @@ +--- +title: "Sleep" +--- + +Upstash Workflow provides a **Sleep** feature that allows you to pause workflow execution for specified durations without consuming compute resources. + +This feature enables you to build time-based workflows, implement delays between steps, and create scheduled operations without the limitations of traditional serverless timeouts. + +## 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) + + +## Sleep Methods + +Upstash Workflow provides two methods for implementing delays in your workflows: + +### 1. context.sleep + +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) => { + const { userId } = context.requestPayload; + + // Send welcome email immediately + await context.run("send-welcome-email", async () => { + return await sendWelcomeEmail(userId); + }); + + // Wait for 3 days before sending follow-up + await context.sleep("wait-for-follow-up", "3d"); + + // 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 + +app = FastAPI() +serve = Serve(app) + +@serve.post("/api/onboarding") +async def onboarding(context: AsyncWorkflowContext[str]) -> None: + user_id = context.request_payload["user_id"] + + # Send welcome email immediately + async def _send_welcome_email(): + return await send_welcome_email(user_id) + + await context.run("send-welcome-email", _send_welcome_email) + + # Wait for 3 days before sending follow-up + await context.sleep("wait-for-follow-up", "3d") + + # Send follow-up email + async def _send_follow_up_email(): + return await send_follow_up_email(user_id) + + await context.run("send-follow-up-email", _send_follow_up_email) +``` + + +You can specify durations using human-readable strings: + +- `"10s"` = 10 seconds +- `"1m"` = 1 minute +- `"30m"` = 30 minutes +- `"2h"` = 2 hours +- `"1d"` = 1 day +- `"1w"` = 1 week +- `"1mo"` = 1 month +- `"1y"` = 1 year + +You can also use numeric values in seconds: + +- `60` = 60 seconds (1 minute) +- `3600` = 3600 seconds (1 hour) +- `86400` = 86400 seconds (1 day) + + +### 2. context.sleepUntil + +Pauses workflow execution until a specific timestamp in the future. + + +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; + +export const { POST } = serve(async (context) => { + const { userId, scheduledTime } = context.requestPayload; + + // Calculate the scheduled time + const scheduledDate = new Date(scheduledTime); + + // Wait until the scheduled time + await context.sleepUntil("wait-until-scheduled", scheduledDate); + + // Execute the scheduled task + await context.run("execute-scheduled-task", async () => { + return await executeTask(userId); + }); +}); +``` + +```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"] + + # Calculate the scheduled time + scheduled_date = datetime.fromisoformat(scheduled_time) + + # Wait until the scheduled time + await context.sleep_until("wait-until-scheduled", scheduled_date) + + # Execute the scheduled task + async def _execute_task(): + return await execute_task(user_id) + + await context.run("execute-scheduled-task", _execute_task) +``` + + +For `context.sleepUntil`, you can use: + +- `Date` objects (JavaScript/TypeScript) +- Unix timestamps (Python) +- ISO string dates + + + + 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. + + By leveraging this feature, you can build sophisticated business logic that spans hours, days, or even months while maintaining cost efficiency and reliability. diff --git a/workflow/features/wait-for-event.mdx b/workflow/features/wait-for-event.mdx new file mode 100644 index 00000000..65303400 --- /dev/null +++ b/workflow/features/wait-for-event.mdx @@ -0,0 +1,67 @@ +--- +title: "Overview" +--- + +Wait for Event feature that allows you to pause workflow execution until an external event occurs. + +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. + +## 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. + +Each waiter has a timeout duration to wait for the event and then fires automatically. + + + 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 + +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. + +To prevent race conditions, always check the response of the `notify` operation. +The `notify` method returns a list of notified waiters. +If this list is empty, it means no workflows were waiting for the event, and you should retry the notification. + + + +```typescript TypeScript +import { Client } from "@upstash/workflow"; + +const client = new Client({ token: "" }); + +const result = await client.notify({ + eventId, + eventData +}); + +// Check if any workflows were notified +if (result.waiters && result.waiters.length > 0) { + console.log(`Notified ${result.waiters.length} workflows`); + return result; +} + +// 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)); + +return await client.notify({ + eventId, + eventData +}); +``` + + +## Selecting an Event ID + +When a workflow run waits on an event ID, it's appended to a list of waiters for the event ID. + +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. \ No newline at end of file diff --git a/workflow/features/wait.mdx b/workflow/features/wait.mdx new file mode 100644 index 00000000..8b24481e --- /dev/null +++ b/workflow/features/wait.mdx @@ -0,0 +1,84 @@ +--- +title: "Wait" +--- + +You can pause a workflow run with the `waitForEvent` step. An event is uniquely identified by event ID. + +The workflow will resume when the matching event is published. + +`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). + + +If no timeout is specified, the default is **7 days**. + + + +```typescript TypeScript +import { serve } from "@upstash/workflow/nextjs"; + +export const { POST } = serve(async (context) => { + const { orderId, userEmail } = context.requestPayload; + + // Wait for order processing completion + const { eventData, timeout } = await context.waitForEvent( + "wait-for-order-processing", + `order-${orderId}`, + { + timeout: "1d" // 1 day timeout + } + ); + + if (timeout) { + // Handle timeout scenario + await context.run("handle-timeout", async () => { + return await handleOrderTimeout(orderId, userEmail); + }); + return; + } + +}); +``` + +```python Python +from fastapi import FastAPI +from upstash_workflow.fastapi import Serve +from upstash_workflow import AsyncWorkflowContext + +app = FastAPI() +serve = Serve(app) + +@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"] + + # Send order processing request + async def _request_order_processing(): + return await request_order_processing(order_id) + + await context.run("request-order-processing", _request_order_processing) + + # Wait for order processing completion + result = await context.wait_for_event( + "wait-for-order-processing", + f"order-{order_id}", + timeout="10m" # 10 minutes timeout + ) + + if result["timeout"]: + # Handle timeout scenario + async def _handle_timeout(): + return await handle_order_timeout(order_id, user_email) + + await context.run("handle-timeout", _handle_timeout) + return + + # Process the completed order + async def _process_completed_order(): + return await process_completed_order(order_id, result["event_data"]) + + await context.run("process-completed-order", _process_completed_order) +``` + + diff --git a/workflow/getstarted.mdx b/workflow/getstarted.mdx index 34467342..02389bbf 100644 --- a/workflow/getstarted.mdx +++ b/workflow/getstarted.mdx @@ -17,6 +17,35 @@ Upstash Workflow lets you write **durable, reliable and performant serverless fu allowFullScreen > + +## Quickstarts + +Upstash Workflow supports Next.js, Cloudflare Workers and [many other frameworks](/workflow/quickstarts/platforms) in TypeScript and Python. + + + + 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 + + + ## Key Features @@ -35,24 +64,28 @@ Upstash Workflow lets you write **durable, reliable and performant serverless fu 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. @@ -65,46 +98,20 @@ Upstash Workflow lets you write **durable, reliable and performant serverless fu 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. -## Quickstarts - -Workflow supports Next.js, Cloudflare Workers and [many more frameworks](/workflow/quickstarts/platforms) in TypeScript and Python. - - - - 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 - - ## Example Use Cases @@ -170,204 +177,17 @@ Here are some example real world use-cases for 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. +Each of the steps are executed by a separate request to your application, by preserving the output of previous steps. + 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. -## Code example - -Let's see a practical implementation of Upstash Workflow using customer onboarding as an example. See our [Next.js Quickstart](/workflow/quickstarts/vercel-nextjs) or [FastAPI Quickstart](/workflow/quickstarts/fastapi) for a complete guide. - - - -```typescript api/workflow/route.ts -import { serve } from "@upstash/workflow/nextjs"; -import { sendEmail } from "./emailUtils"; - -// Type-safety for starting our workflow -interface InitialData { - userId: string - email: string - name: string -} - -export const { POST } = serve(async (context) => { - const { userId, email, name } = context.requestPayload; - - // Step 1: Send welcome email - await context.run("send-welcome-email", async () => { - await sendEmail(email, "Welcome to our service!"); - }); - - // Step 2: Wait for 3 days (in seconds) - await context.sleep("sleep-until-follow-up", 60 * 60 * 24 * 3); - - // Step 3: AI-generate personalized follow-up message - const { body: aiResponse } = await context.api.openai.call( - "generate-personalized-message", - { - token: "", - operation: "chat.completions.create", - body: { - model: "gpt-3.5-turbo", - messages: [ - { role: "system", content: "You are an assistant creating personalized follow-up messages." }, - { role: "user", content: `Create a short, friendly follow-up message for ${name} who joined our service 3 days ago.` } - ] - }, - } - ); - - const personalizedMessage = aiResponse.choices[0].message.content; - - // Step 4: Send personalized follow-up email - await context.run("send-follow-up-email", async () => { - await sendEmail(email, personalizedMessage); - }); -}); -``` - -```python main.py -from fastapi import FastAPI -from typing import Dict, TypedDict -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext, CallResponse -from email_utils import send_email - -app = FastAPI() -serve = Serve(app) - - -# Type-safety for starting our workflow -class InitialData(TypedDict): - user_id: str - email: str - name: str - - -@serve.post("/api/onboarding") -async def onboarding_workflow(context: AsyncWorkflowContext[InitialData]) -> None: - data = context.request_payload - user_id = data["user_id"] - email = data["email"] - name = data["name"] - - # Step 1: Send welcome email - async def _send_welcome_email() -> None: - await send_email(email, "Welcome to our service!") - - await context.run("send-welcome-email", _send_welcome_email) - - # Step 2: Wait for 3 days (in seconds) - await context.sleep("sleep-until-follow-up", 60 * 60 * 24 * 3) - - # Step 3: AI-generate personalized follow-up message - ai_response: CallResponse[Dict[str, str]] = await context.call( - "generate-personalized-message", - url="https://api.openai.com/v1/chat/completions", - method="POST", - headers={...}, - body={ - "model": "gpt-3.5-turbo", - "messages": [ - { - "role": "system", - "content": "You are an assistant creating personalized follow-up messages.", - }, - { - "role": "user", - "content": f"Create a short, friendly follow-up message for {name} who joined our service 3 days ago.", - }, - ], - }, - ) - - personalized_message = ai_response.body["choices"][0]["message"]["content"] - - # Step 4: Send personalized follow-up email - async def _send_follow_up_email() -> None: - await send_email(email, personalized_message) - - await context.run("send-follow-up-email", _send_follow_up_email) - -``` - - - - - Any HTTP request using `context.call`, like the AI-generation above, does not - count towards your function's execution time and does not increase your - serverless bill. It can also run for up to 2 hours, completely bypassing any - platform-specific function timeouts. - - -Once your endpoint is ready, you can trigger the workflow via our SDKs or using plain REST. -See [here](/workflow/howto/start) for details. - -```ts TypeScript SDK -import { Client } from "@upstash/workflow"; - -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 for the initial request -}); -``` - -```py Python SDK -from qstash import AsyncQStash - -client = AsyncQStash("") - -res = await client.message.publish_json( - url="https:///", - body={"hello": "there!"}, - headers={...}, - retries=3, -) -``` - -```bash REST -curl -X POST https:/// -b '{"hello": "there!"}' -``` - - -The above example should give you a rough idea of how a workflow looks in code. For step-by-step instructions on setting up your first workflow with images along the way, see our [Next.js Quickstart](/workflow/quickstarts/vercel-nextjs) or [FastAPI Quickstart](/workflow/quickstarts/fastapi). - ---- - -Here are more details about what the `context` object does: - -- [context.run](/workflow/basics/context#context-run) -- [context.sleep](/workflow/basics/context#context-sleep) -- [context.sleepUntil](/workflow/basics/context#context-sleepuntil) -- [context.call](/workflow/basics/context#context-call) -- [context.waitForEvent](/workflow/basics/context#context-waitforevent) -- [context.notify](/workflow/basics/context#context-notify) -- [context.cancel](/workflow/basics/context#context-cancel) - -See [caveats](/workflow/basics/caveats) for more complex API usage and best-practices when using Upstash Workflow. - ---- - -Guides on common workflow topics: - -- [Cancel a running workflow](/workflow/howto/cancel) -- [Wait for External Events](/workflow/howto/events) -- [Handle failed workflow runs](/workflow/howto/failures) -- [Monitor active workflows in real-time](/workflow/howto/failures) -- [Schedule repeated workflow runs](/workflow/howto/schedule) -- [Secure a workflow endpoint](/workflow/howto/security) -- [Handle workflow route code changes](/workflow/howto/changes) -- [Develop your workflows locally](/workflow/howto/local-development) -- [Pricing](/workflow/pricing) +## Support -If you're curious about the behind-the-scenes about how we ensure separate step execution or prevent serverless timeouts, we wrote about it [here](/workflow/basics/how)! :) +Need help or have questions? We're here to support you: -Here is our [Upstash Workflow roadmap](/workflow/roadmap) to see what we planned for the future. +- Join our Discord community to ask questions and share feedback +- Open a ticket through the Intercom chatbox in the dashboard for any issue diff --git a/workflow/howto/cancel.mdx b/workflow/howto/cancel.mdx index 6edff467..ed62ec70 100644 --- a/workflow/howto/cancel.mdx +++ b/workflow/howto/cancel.mdx @@ -28,6 +28,6 @@ const client = new Client({ token: "" }); await client.cancel({ ids: "" }); ``` -And replace `` with your actual run ID. See [the documentation of `client.cancel` method for more information about other ways of canceling workflows](/workflow/basics/client#cancel-workflow). +And replace `` with your actual run ID. See [the documentation of `client.cancel` method for more information about other ways of canceling workflows](/workflow/basics/client/cancel). You can also use the [Upstash Workflow REST API](/workflow/rest/runs/cancel) to cancel a run programatically. diff --git a/workflow/howto/changes.mdx b/workflow/howto/changes.mdx index fe0bd951..d3c39d7b 100644 --- a/workflow/howto/changes.mdx +++ b/workflow/howto/changes.mdx @@ -2,13 +2,13 @@ title: "Update a Workflow" --- -## Understanding workflow continuity - Workflows are composed of multiple steps. When you modify workflow code, it's important to consider how these changes might affect in-progress workflows. -## Potential issues +## Issues + +You cannot change the step order of a existing workflow. -If your code changes remove or modify 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: +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 @@ -16,10 +16,10 @@ HTTP status 400. Incompatible step name. Expected , got ## Safe changes -Modifying a workflow's code is safe when: +Updating workflow code is safe in the following cases: -- There are no active workflow runs -- You're only adding new steps to the end of the workflow +- No active workflow runs exist +- Only new steps are added to the end of the workflow ## Guidelines for updating workflows diff --git a/workflow/howto/configure.mdx b/workflow/howto/configure.mdx new file mode 100644 index 00000000..830d8844 --- /dev/null +++ b/workflow/howto/configure.mdx @@ -0,0 +1,52 @@ +--- +title: "Overview" +--- + +You can configure a workflow run when starting it. The following are the options you can configure: + +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. +4. Failure Function: You can enable or disable failure function execution for the workflow run. + +You can pass these configuration options when starting a workflow run: + +```typescript +import { Client } from "@upstash/workflow"; + +const client = Client() + +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 + } + useFailureFunction: true, + useTriggerConfiguration: true +}); +``` + +You must set `useTriggerConfiguration: true` to enable workflow run configuration. +If this flag is not provided, your configuration will **not be applied** and the workflow will fall back to default values instead. +This option is disabled by default for backward compatibility. + + +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. + +For details, see: +- [context.call](/workflow/basics/context/run) +- [context.invoke](/workflow/basics/context/run) + + +Upstash Workflow does not support step level configuration. The configuration applies to all steps executed by a workflow run. + +If you want to specifically throttle a step, there is a workaround by splitting step to another workflow and using `context.invoke()`. + +See [Per-User Configuration](/workflow/howto/configure/per-step) documentation on how to do that. + \ No newline at end of file diff --git a/workflow/howto/configure/per-step-configuration.mdx b/workflow/howto/configure/per-step-configuration.mdx new file mode 100644 index 00000000..ee208716 --- /dev/null +++ b/workflow/howto/configure/per-step-configuration.mdx @@ -0,0 +1,23 @@ +--- +title: "Per-Step Configuration" +--- + +Normally, it's not possible to configure a step specifically. When you start a workflow run with a configuration, all steps share the same configuration. + +Suppose that in your workflow you have three different steps, and you trigger multiple runs of this workflow. +Then, flow control configuration will looks like following: + + + + +However, `context.call()` and `context.invoke()` steps are exceptions to this approach. +The context.call send a request to a external address, and context.invoke triggers a new workflow run. +Because they are not bound to calling workflow run's environment, we allow to configure separately. + +However, in some cases you might want to use an third party SDK inside a step and want to throttle that specific step instead of thorttling all the steps of workflow run. +Recommended way for throttling is passing a flow-control configuration when triggering the workflow run. +But it means all the steps of that workflow run will share the same config. +For example, parallelism=5 means 5 step can be active with the same flow control key. +This might dramatically decrease the thoroguhput if one of the steps require very low parallelism. + +A workaround is to move step to a separate workflow and use `context.invoke` to run it with a specific configuration. \ No newline at end of file diff --git a/workflow/howto/flow-control.mdx b/workflow/howto/flow-control.mdx index d3492171..e69de29b 100644 --- a/workflow/howto/flow-control.mdx +++ b/workflow/howto/flow-control.mdx @@ -1,107 +0,0 @@ ---- -title: "Limit Rate and Parallelism" ---- - -`Flow Control` enables you to limit the number of calls made to your workflow by delaying the delivery. - -There are two main use cases for Flow-Control in Workflow: -1. [Limiting the Workflow Environment](#limiting-the-workflow-environment): Controlling the execution environment. -2. [Limiting External API Calls](#limiting-external-api-calls): Preventing excessive requests to external services. - ---- - -There are three parameters you can configure to achieve the desired behavior: - -- **Rate and Period**: - - **Rate** specifies the maximum number of calls allowed within a given period (default: 1 second, configurable via the `period` parameter). All calls sharing the same `FlowControl` key count toward this limit. Instead of rejecting calls that exceed the rate, QStash automatically queues and delays them to ensure the limit is respected. - - **Period** is the time window over which the rate limit is enforced. Adjust this to control how frequently calls are allowed. - -- **Parallelism**: - Sets the maximum number of calls that can be executed concurrently. Unlike rate limiting, this parameter considers the duration of each call. If the parallelism limit is reached, additional calls are queued and will only start when an active call completes, ensuring that the number of simultaneous executions never exceeds the specified limit. - -**Using Rate and Parallelism Together**: All parameters can be combined. For example, with a rate of 10 per second and parallelism of 20, if each request takes a minute to complete, QStash will trigger 10 calls in the first second and another 10 in the next. Since none of them will have finished, the system will wait until one completes before triggering another. - -For the `FlowControl`, you need to choose a key first. This key is used to count the number of calls made to your endpoint. -There are not limits to number of keys you can use. - - -The rate/parallelism limits are not applied per `url`, they are applied per `Flow-Control-Key`. - - - -Keep in mind that rate/period and parallelism info are kept on each publish separately. That means -if you change the rate/period or parallelism on a new publish, the old fired ones will not be affected. They will keep their flowControl config. -During the period that old `publishes` has not delivered but there are also the `publishes` with the new rates, QStash 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. - - -#### Limiting the Workflow Environment - -To limit the execution environment, you need to configure both the `serve` and `trigger` methods. -When configured, all the steps of the workflow will respect the limits. -Due to the nature of the Workflow SDK, QStash calls the `serve` method multiple times. This means that to stay within the limits -of the deployed environments, the given rate will be applied to all calls going from QStash servers to the `serve` method. - -Note that if there are multiple Workflows running in the same environment, their steps can interleave, but overall rate and parallelism limits will be respected -if they share the same `flowControl` key. - -- **In the `serve` method**: - -```js -export const { POST } = serve( - async (context) => { - await context.run("step-1", async () => { - return someWork(); - }); - }, - { - flowControl: { key: "app1", parallelism: 3, rate: 10, period: "1m" } - } -); -``` - -For more details, see the [`flowControl` documentation under `serve` parameters](/workflow/basics/serve#flowcontrol). - -- **In the `trigger` method**: - -```js -import { Client } from "@upstash/workflow"; - -const client = new Client({ token: "" }); -const { workflowRunId } = await client.trigger({ - url: "https://workflow-endpoint.com", - body: "hello there!", - flowControl: { key: "app1", parallelism: 3, rate: 10 } -}); -``` - -For more details on `trigger`, see the documentation [here](/workflow/basics/client#trigger-workflow). - -#### Limiting External API Calls - -To limit requests to an external API, use `context.call`: - -```js -import { serve } from "@upstash/workflow/nextjs"; - -export const { POST } = serve<{ topic: string }>(async (context) => { - const request = context.requestPayload; - - const response = await context.call( - "generate-long-essay", - { - url: "https://api.openai.com/v1/chat/completions", - method: "POST", - body: {/*****/}, - flowControl: { key: "opani-call", parallelism: 3, rate: 10 } - } - ); -}); -``` - -For more details, see the documentation [here](/workflow/basics/context#context-call). - -#### Rest API for Flow Control Information - -You can also use the Rest API to get information on the flow control. -See the [API documentation](/workflow/rest/flow-control/get) for more details. \ No newline at end of file diff --git a/workflow/howto/invoke.mdx b/workflow/howto/invoke.mdx deleted file mode 100644 index bc50ff55..00000000 --- a/workflow/howto/invoke.mdx +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: "Invoke Other Workflows" ---- - -You can start another workflow run inside a workflow and await its execution to complete. -This allows to orchestrate multiple workflows together without external syncranization. - -```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) - } -) -``` - -As you may notice, we pass a workflow object to the invoke function. This object is initialized using the `createWorkflow()` function. - -### `createWorkflow` - -Normally, workflows are initialized with `serve()` method and exposed as a standalone route in your application. -However, when workflows are defined separately using `serve()`, type safety is not guaranteed. - -To ensure type safety for request and response when invoking other workflows, we introduced the `createWorkflow()` function. -`createWorkflow()` returns a referenceable workflow object that can be used in `context.invoke()`. - -```typescript {2, 5-7, 10-14, 19-28} -import { WorkflowContext } from "@upstash/workflow"; -import { createWorkflow } from "@upstash/workflow/nextjs"; - -const anotherWorkflow = createWorkflow( - // Define the workflow logic, specifying the type of the initial request body. - // In this case, the body is a string: - async (context: WorkflowContext) => { - - await context.sleep("wait 1 second", 1) - - // Return a response from the workflow. The type of this - // response will be available when `context.invoke` is - // called with `anotherWorkflow`. - return { message: "This is the data returned by the workflow" }; - } -); - -const someWorkflow = createWorkflow(async (context) => { - // Invoke anotherWorkflow with a string body and get the response - // The types of the body parameter and the response are - // typesafe and inferred from anotherWorkflow - const { body } = await context.invoke( - "invoke anotherWorkflow", - { - workflow: anotherWorkflow, - body: "user-1" - } - ), -}); -``` - - - When you use `context.invoke`, invoking workflow will wait until the invoked workflow finishes before running the next step. - - If you don't want to wait for the invoked workflow, you can use `context.call` instead of `context.invoke`. Simply pass `anotherWorkflow` in the example above to the `workflow` parameter of `context.call`. - - - -Next question is, how do we expose these workflows as endpoints? That's where `serveMany` comes in. - -### `serveMany` - -`createWorkflow()` does not expose your workflow like `serve()`, it just initializes the workflow object. -To be able to use the workflow, they must be exposed with `serveMany` function. - -First step of using `serveMany` is to define a catch-all route. - - - In this example, we are using Next.js. 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). - - If you need any assistance, feel free to reach out through the chat box at the bottom right of this page. - - -In Next.js, a catch-all route is defined by creating a `route.ts` file under a directory named with `[...]`, like `app/serve-many/[...any]/route.ts` - -```ts app/serve-many/[...any]/route.ts {26-31} -import { WorkflowContext } from "@upstash/workflow"; -import { createWorkflow, serveMany } from "@upstash/workflow/nextjs"; - -const workflowOne = createWorkflow(async (context) => { - await context.run("say hi", () => { - console.log("workflow one says hi!") - }) - - const { body, isCanceled, isFailed } = await context.invoke("invoking other", { - workflow: workflowTwo, - body: "hello from workflow one", - }) - - console.log(`received response from workflowTwo: ${body}`) -}) - -const workflowTwo = createWorkflow(async (context: WorkflowContext) => { - await context.run("say hi", () => { - console.log("workflowTwo says hi!") - console.log(`received: '${context.requestPayload}' in workflowTwo`) - }) - - return "Workflow two finished!" -}) - -export const { POST } = serveMany( - { - "workflow-one-route": workflowOne, - "workflow-two-route": workflowTwo, - } -) -``` - - - If a workflow is going to invoke another workflow, these two workflows must be exposed in the same `serveMany` endpoint. - - If you pass a workflow object which is initialized with `createWorkflow()` but not exposed inside the same `serveMany`, you will get a runtime error. - - -In this example, we have two workflows defined under `serveMany`. `workflowOne` is a workflow that invokes `workflowTwo`. To start `workflowOne`, you can send a POST request to `https://your-app/serve-many/workflow-one-route`. - -```bash -curl -X POST https://your-app/serve-many/workflow-one-route -``` - -Note that `workflow-one-route` is infered from the key passed to `serveMany`. Similarly, you can send a POST request to `https://your-app/serve-many/workflow-two-route` to start `workflowTwo`. - -### Options - -Just like `serve`, you can pass [options](/workflow/basics/serve#options) to both `createWorkflow` and `serveMany`. `createWorkflow` accepts all the parameters that `serve` does. `serveMany` accepts some specific parameters. - -```ts {5-7, 14-16} -const workflowOne = createWorkflow( - async (context) => { - // ... - }, - { - retries: 0 - } -) - -export const { POST } = serveMany( - { - workflowOne - }, - { - failureUrl: "https://some-url" - } -) -``` - -If the same parameter is provided to both `createWorkflow` and `serveMany`, the value specified in `createWorkflow` will take precedence. - -Additionally, when you invoke `workflowOne` from another workflow, some options defined in `createWorkflow` for `workflowOne` will be applied in the invocation request. These options include `retries`, `failureFunction`, `failureUrl`, and `flowControl`. - -### Limitations - -One limitation of `invoke` is that you cannot create an infinite chain of workflow invocations. If you set up an 'invoke loop' where workflows continuously invoke other workflows, the process will fail once it reaches a depth of 100. - diff --git a/workflow/howto/local-development/development-server.mdx b/workflow/howto/local-development/development-server.mdx new file mode 100644 index 00000000..ebcf70b7 --- /dev/null +++ b/workflow/howto/local-development/development-server.mdx @@ -0,0 +1,96 @@ +--- +title: "Development Server" +--- + +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. + + + + + Start the development server using the QStash CLI: + + ```javascript + npx @upstash/qstash-cli dev + ``` + + The QStash CLI output will look something like this: + + ```plaintext QStash CLI Output + Upstash QStash development server is runnning at + + 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 + + Sample cURL request: + curl -X POST http://127.0.0.1:8080/v2/publish/https://example.com -H "Authorization: Bearer eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" + + Check out documentation for more details: + https://upstash.com/docs/qstash/howto/local-development + ``` + + For detailed instructions on setting up the development server, see our [QStash Local Development Guide](/qstash/howto/local-development). + + + + 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. + + + + + + Once your development server is running, update your environment variables to route QStash requests to your local server. + + ```env + QSTASH_URL="http://127.0.0.1:8080" + QSTASH_TOKEN="eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" + QSTASH_CURRENT_SIGNING_KEY="sig_7RvLjqfZBvP5KEUimQCE1pvpLuou" + QSTASH_NEXT_SIGNING_KEY="sig_7W3ZNbfKWk5NWwEs3U4ixuQ7fxwE" + ``` + + + + + It's all set up 🎉 + + Now, you can use your local address when triggering the workflow runs. + + + ```javascript + import { Client } from "@upstash/workflow"; + + const client = Client() + + 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: + + - 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 + + 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: + + ```javascript + const BASE_URL = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : `http://localhost:3000` + + const { workflowRunId } = await client.trigger({ + url: `${BASE_URL}/api/workflow`, + retries: 3, + }); + ``` + + + + + + + diff --git a/workflow/howto/local-development.mdx b/workflow/howto/local-development/local-tunnel.mdx similarity index 50% rename from workflow/howto/local-development.mdx rename to workflow/howto/local-development/local-tunnel.mdx index 3c738c46..86ba3d4e 100644 --- a/workflow/howto/local-development.mdx +++ b/workflow/howto/local-development/local-tunnel.mdx @@ -1,66 +1,30 @@ --- -title: "Local Development" +title: "Local Tunnel" --- -Upstash Workflow requires your application to be publicly accessible in production. -For development, you can either run the QStash development server locally or set up a local tunnel to make your local server publicly accessible. - -## Development Server (Recommended) - -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. - -Start the development server using the QStash CLI: - -```javascript -npx @upstash/qstash-cli dev -``` - -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 trigger and monitor workflow runs with the local server. - - - -For detailed instructions on setting up the development server, see our [QStash Local Development Guide](/qstash/howto/local-development). - -Once your development server is running, update your environment variables to route QStash requests to your local server. -This eliminates the need for tunneling during development. - -The QStash CLI output will look something like this: - -```plaintext -Upstash QStash development server is runnning at http://127.0.0.1:8080 - -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 - -Sample cURL request: -curl -X POST http://127.0.0.1:8080/v2/publish/https://example.com -H "Authorization: Bearer eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" - -Check out documentation for more details: -https://upstash.com/docs/qstash/howto/local-development -``` - -You should set the `QSTASH_URL` environment variable to point to your local server, along with the user credentials provided in the output. - -## Local tunnel with ngrok +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. The easiest way to make a local URL publically available is [ngrok](https://ngrok.com), a free tunneling service. 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. -For example on MacOS, you can connect your account like this: - - - - - -Or on Windows: - - - - +You can connect your account like this: + + + + + + + + + + + + + + Once you have installed the ngrok CLI, add your ngrok-issued auth token like this: diff --git a/workflow/howto/logging.mdx b/workflow/howto/logging.mdx new file mode 100644 index 00000000..1f1d5902 --- /dev/null +++ b/workflow/howto/logging.mdx @@ -0,0 +1,5 @@ +--- +title: "Logging in Workflow" +--- + +das \ No newline at end of file diff --git a/workflow/howto/monitor.mdx b/workflow/howto/monitor.mdx deleted file mode 100644 index 360cab6c..00000000 --- a/workflow/howto/monitor.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: "Monitor a Run" ---- - - -Logs of the Workflow can be found in the console under the [Workflow tab](https://console.upstash.com/qstash?tab=workflows) - -It has two modes that can be changed from to top right corner: -- **Grouped By Workflow Run Id** : This is the default view where the overall workflow run is summarized. You can see each step, its start date and output and also whether it is successfully delivered or not. -- **Flat View** : The flat view is mostly for debugging when a workflow step has failed. It has more details about individual -events of each step. - -We have 4 new event types introduced on top of [existing message events](/qstash/howto/debug-logs). Note that message events are relavent for individual steps. -The new 4 events are to show the state of a workflow run. - -- **RUN_STARTED** -- **RUN_FAILED** -- **RUN_CANCELLED** -- **RUN_SUCCESS** - -Here is an example route implementation with our SDK and its corresponding screenshot from [Workflow tab]() showing its events. - -```javascript -import { serve } from "@upstash/workflow/nextjs"; -import { retrieveEmail, fetchFromLLm, UserRequest} from "../../../lib/util"; - - -export const { POST } = serve( - async (context) => { - const input = context.requestPayload; - - await context.sleep("sleep", 10); - - const p1 = context.run("retrieveEmail", async () => { - return retrieveEmail(input.id); - }); - - const p2 = context.run("askllm", async () => { - return fetchFromLLm(input.question); - }); - - await Promise.all([p1, p2]) - }, -); -``` - -We are calling this endpoint as follows: -```bash -curl -XPOST https://qstash-workflow.vercel.app/api/example -d '{"id": "id_123", "question": "what is the meaning of life?"}' -``` - -Steps of the related workflow runs look as follows: - - - - - -Here is another run where `fetchFromLLm` is throwing an error. -When a particular step is retrying/failed, you can click it to see more details as shown below: - - - - - -While a step is retrying, it is composed of several messages. You can click the `messageId` field above to -see all the events of a particular step to find out why it is retrying or failed. -When clicked, the workflow events will switch to `flat` view with `messageId` filtered as shown below: - - - - - -In this screen, you can go to one of the `error` events, and see what response it returned to understand -the details of the error as shown below: - - - - diff --git a/workflow/howto/schedule.mdx b/workflow/howto/schedule.mdx index 347ea95a..d5f614ac 100644 --- a/workflow/howto/schedule.mdx +++ b/workflow/howto/schedule.mdx @@ -1,63 +1,14 @@ --- -title: "Schedule Repeated Runs" +title: "Schedule a Workflow" --- You can schedule a workflow to run periodically using a cron definition. -## Scheduling a workflow - -For example, let's define a workflow that creates a backup of some important data daily. Our workflow endpoint might look like this: - - - -```typescript api/workflow/route.ts -import { serve } from "@upstash/workflow/nextjs"; -import { createBackup, uploadBackup } from "./utils"; - -export const { POST } = serve( - async (ctx) => { - const backup = await ctx.run("create-backup", async () => { - return await createBackup(); - }); - - await ctx.run("upload-backup", async () => { - await uploadBackup(backup); - }); - }, - { - failureFunction({ context, failStatus, failResponse, failHeader }) { - // immediately get notified for failed backups - // i.e. send an email, log to Sentry - }, - } -); -``` - -```python main.py -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext -from utils import create_backup, upload_backup - -app = FastAPI() -serve = Serve(app) - - -@serve.post("/api/workflow") -async def workflow(context: AsyncWorkflowContext[str]) -> None: - async def _step1(): - return await create_backup() - - backup = await context.run("create_backup", _step1) - - async def _step2(): - await upload_backup(backup) +For this feature, you would need to use Upstash QStash's Schedules feature. - await context.run("upload_backup", _step2) - -``` +## Scheduling a workflow - +For example, let's suppose that you have a workflow that creates a backup of some important data daily. Our workflow endpoint might look like this: To run this endpoint on a schedule, navigate to `Schedules` in your QStash dashboard and click `Create Schedule`: @@ -73,7 +24,7 @@ Enter your live endpoint URL, add a CRON expression to define the interval at wh Your workflow will now run repeatedly at the interval you have defined. For more details on CRON expressions, see our [QStash scheduling documentation](/qstash/features/schedules). -## Scheduling a per-user workflow +## Programmatically Schedule 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. @@ -90,15 +41,6 @@ const client = new Client({ token: process.env.QSTASH_TOKEN! }); export async function POST(request: Request) { const userData: UserData = await request.json(); - // Simulate user registration - const user = await signUp(userData); - - // Calculate the date for the first summary (7 days from now) - const firstSummaryDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); - - // Create cron expression for weekly summaries starting 7 days from signup - const cron = `${firstSummaryDate.getMinutes()} ${firstSummaryDate.getHours()} * * ${firstSummaryDate.getDay()}`; - // Schedule weekly account summary await client.schedules.create({ scheduleId: `user-summary-${user.email}`, @@ -157,81 +99,6 @@ async def sign_up(request: Request): 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. -Note: when creating a user-specific schedule, pass a unique `scheduleId` to ensure the operation is idempotent. (See [caveats](/workflow/basics/caveats) for more details on why this is important). - -Lastly, add the summary-creating and email-sending logic inside of your workflow. For example: - - - -```typescript api/send-weekly-summary/route.ts -import { serve } from "@upstash/workflow/nextjs"; -import { getUserData, generateSummary } from "@/utils/user-utils"; -import { sendEmail } from "@/utils/email-utils"; - -// Type-safety for starting our workflow -interface WeeklySummaryData { - userId: string; -} - -export const { POST } = serve(async (context) => { - const { userId } = context.requestPayload; - - // Step 1: Fetch user data - const user = await context.run("fetch-user-data", async () => { - return await getUserData(userId); - }); - - // Step 2: Generate weekly summary - const summary = await context.run("generate-summary", async () => { - return await generateSummary(userId); - }); - - // Step 3: Send email with weekly summary - await context.run("send-summary-email", async () => { - await sendEmail(user.email, "Your Weekly Summary", summary); - }); -}); -``` - -```python main.py -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext -from utils import get_user_data, generate_summary, send_email - -app = FastAPI() -serve = Serve(app) - - -@dataclass -class WeeklySummaryData: - user_id: str - - -@serve.post("/api/send-weekly-summary") -async def send_weekly_summary(context: AsyncWorkflowContext[WeeklySummaryData]) -> None: - user_id = context.request_payload.user_id - - # Step 1: Fetch user data - async def _step1(): - return await get_user_data(user_id) - - user = await context.run("fetch_user_data", _step1) - - # Step 2: Generate weekly summary - async def _step2(): - return await generate_summary(user_id) - - summary = await context.run("generate_summary", _step2) - - # Step 3: Send email with weekly summary - async def _step3(): - await send_email(user.email, "Your Weekly Summary", summary) - - await context.run("send_summary_email", _step3) - -``` - - - -Just like that, each user will receive an account summary every week, starting one week after signing up. + + When creating a per-user schedule, pass a unique `scheduleId` to identify the schedule for better management and observability. + diff --git a/workflow/howto/security.mdx b/workflow/howto/security.mdx index d364485d..74d4dee6 100644 --- a/workflow/howto/security.mdx +++ b/workflow/howto/security.mdx @@ -1,131 +1,143 @@ --- -title: "Secure an Endpoint" +title: "Secure a Workflow" --- -To prevent _anyone_ from triggering your workflow endpoint, you can use two methods: +To prevent unauthorized access to your workflow endpoint, you can add an authorization layer. +Upstash Workflow supports two approaches: -- QStash's [built-in request verification](/qstash/features/security) -- Custom header and custom authorization mechanism +- **Built-in request verification** (recommended) +- **Custom authorization method** -### Using QStash's built-in request verification (recommended) +### Built-in request verification (recommended) -Use QStash's built-in request verification to allow only authorized clients to trigger your workflow. Set two environment variables in addition to your QStash API key: +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. + +How it works: + +1. Upstash Workflow automatically adds the `Upstash-Signature` header to every request. + This signature is generated using your signing keys. + +2. When this mechanism is enabled, the SDK verifies that the signature is valid before processing the request. + +This ensures that only requests originating from Upstash Workflow are processed. + +To enable this verification, set the following environment variables in your application: ```bash .env QSTASH_CURRENT_SIGNING_KEY=xxxxxxxxx QSTASH_NEXT_SIGNING_KEY=xxxxxxxxx ``` -And replace `xxxxxxxxx` with your actual keys. Find both of these keys in your QStash dashboard under the **"Signing keys"** tab: +You can find the values in Upstash Workflow dashboard. -This will require **every request** to your workflow endpoint to include a valid signature, including the initial request that triggers a workflow. In other words: all requests need to come either from QStash (which automatically populates the `Upstash-Signature` header) or from a client that manually populates the `Upstash-Signature` header with your signing secret. - -We suggest using QStash's publish API to trigger your workflow: - -```bash Terminal -curl -XPOST \ - -H 'Authorization: Bearer ' \ - -H "Content-type: application/json" \ - -d '{ "initialData": "hello world" }' \ - 'https://qstash.upstash.io/v2/publish/https:///api/workflow' -``` - -For edge cases that do not support environment variables as outlined above, you can explicitly pass your signing keys to the `serve` function: - - - -```typescript TypeScript -import { Receiver } from "@upstash/qstash"; -import { serve } from "@upstash/workflow/nextjs"; - -export const { POST } = serve( - async (context) => { - // Your workflow steps... - }, - { - receiver: new Receiver({ - currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY, - nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY, - }), - } -); -``` - -```python Python -from qstash import Receiver - -@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: - ... - -``` - - + + 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: "", + }), + } + ); + ``` + + ```python Python + from qstash import Receiver + + @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: + ... + + ``` + + ## Custom Authorization Method -You can use your own authorization mechanism with Upstash Workflow. We ensure that the initial headers and initial request body will be available on every call made to your workflow. - - +You can implement your own authorization mechanism with Upstash Workflow. -```typescript TypeScript -import { serve } from "@upstash/workflow/nextjs"; +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. -export const { POST } = serve( - async (context) => { - const authHeader = context.headers.get("authorization"); - const bearerToken = authHeader?.split(" ")[1]; + - if (!isValid(bearerToken)) { - console.error("Authentication failed."); - return; - } + ```typescript TypeScript + 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)) { + // ... + } + }, + } + ); + ``` - // Your workflow steps.. - }, - { - failureFunction: async () => { - const authHeader = context.headers.get("authorization"); - const bearerToken = authHeader?.split(" ")[1]; + ```python Python + from fastapi import FastAPI + from upstash_workflow.fastapi import Serve + from upstash_workflow import AsyncWorkflowContext - if (!isValid(bearerToken)) { - // ... - } - }, - } -); -``` + app = FastAPI() + serve = Serve(app) -```python Python -from fastapi import FastAPI -from upstash_workflow.fastapi import Serve -from upstash_workflow import AsyncWorkflowContext -app = FastAPI() -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 + if not is_valid(bearer_token): + print("Authentication failed.") + return -@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 + # Your workflow steps... - if not is_valid(bearer_token): - print("Authentication failed.") - return + ``` - # Your workflow steps... + -``` + + If you implement custom authorization in your workflow route, you should also include the same authorization check in the failure function. - \ No newline at end of file + The failure function executes independently of the route function, so without this check, unauthorized requests could trigger the failure function + \ No newline at end of file diff --git a/workflow/howto/start.mdx b/workflow/howto/start.mdx index 6b7fa8fd..57fc1d9a 100644 --- a/workflow/howto/start.mdx +++ b/workflow/howto/start.mdx @@ -6,101 +6,65 @@ You’ve defined your workflow, and the final step is to trigger the endpoint! There are two main ways to start your workflow: -### 1. [Using `client.trigger` (Recommended)](/workflow/basics/client#trigger-workflow) - - - This feature is not yet available in - [workflow-py](https://github.com/upstash/workflow-py). See our - [Roadmap](/workflow/roadmap) for feature parity plans and - [Changelog](/workflow/changelog) for updates. - +### Using `client.trigger` (Recommended) We recommend using `client.trigger` to start your workflow. -```ts Single Workflow -import { Client } from "@upstash/workflow"; - -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 - useFailureFunction: true, // whether a failure function is defined in the endpoint - flowControl: { ... } // optional flow control -}) - -console.log(workflowRunId) -// prints wfr_my-workflow -``` - -```ts Multiple Workflows -import { Client } from "@upstash/workflow"; - -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 -``` + ```ts Single Workflow + import { Client } from "@upstash/workflow"; + + 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 + useFailureFunction: true, // whether a failure function is defined in the endpoint + flowControl: { ... } // optional flow control + }) + + console.log(workflowRunId) + // prints wfr_my-workflow + ``` + + ```ts Multiple Workflows + import { Client } from "@upstash/workflow"; + + 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 + ``` ### 2. Sending an HTTP Request -This approach is recommended for quick testing via curl. +This approach is recommended for quick testing via curl during development. + +You should **NOT** start the workflow run in production by direct calls to your endpoint. + +```bash +curl -X POST https:/// \ + -H "my-header: foo" \ + -d '{"foo": "bar"}' +``` -If you’ve [secured your endpoint with signing keys](/workflow/howto/security), only the `trigger` methid will work. Direct calls to the endpoint (e.g., via `curl` or `fetch`) will not be accepted. - + If you’ve secured your endpoint with signing keys, only the `trigger` methid will work. Direct calls to the endpoint (e.g., via `curl` or `fetch`) will not be possible since `Upstash-Signature` header is missing. - - - ```bash - curl -X POST https:/// \ - -H "my-header: foo" \ - -d '{"foo": "bar"}' - ``` - - - ```js - await fetch("https:///", { - method: "POST", - body: JSON.stringify({ "foo": "bar" }), - headers: { - "my-header": "foo" - } - }); - ``` - - - ```python - import requests - - requests.post( - "https:///", json={"foo": "bar"}, headers={"my-header": "foo"} - ) - - ``` - - - -### Accessing Payload and Headers - -When you call the endpoint, the payload and headers you send will be accessible in the context: -- The payload is available in the `context.requestPayload` field. -- The headers are available in the `context.headers` field. - -For more details, refer to the documentation on [the Context object](/workflow/basics/context). + For more information, read [Secure a workflow](/workflow/howto/security) documentation. + diff --git a/workflow/quickstarts/astro.mdx b/workflow/quickstarts/astro.mdx index f9305b60..61638592 100644 --- a/workflow/quickstarts/astro.mdx +++ b/workflow/quickstarts/astro.mdx @@ -64,7 +64,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```txt QSTASH_URL="http://127.0.0.1:8080" -QSTASH_TOKEN= +QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -78,7 +78,7 @@ Alternatively, you can set up a local tunnel. For this option: ```txt QSTASH_TOKEN="***" -UPSTASH_WORKFLOW_URL= +UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/cloudflare-workers.mdx b/workflow/quickstarts/cloudflare-workers.mdx index 56151209..25ba2c59 100644 --- a/workflow/quickstarts/cloudflare-workers.mdx +++ b/workflow/quickstarts/cloudflare-workers.mdx @@ -61,7 +61,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```txt .dev.vars QSTASH_URL="http://127.0.0.1:8080" -QSTASH_TOKEN= +QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -75,7 +75,7 @@ Alternatively, you can set up a local tunnel. For this option: ```txt .dev.vars QSTASH_TOKEN="***" -UPSTASH_WORKFLOW_URL= +UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/express.mdx b/workflow/quickstarts/express.mdx index 082fddcb..46f53800 100644 --- a/workflow/quickstarts/express.mdx +++ b/workflow/quickstarts/express.mdx @@ -53,7 +53,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```txt QSTASH_URL="http://127.0.0.1:8080" -QSTASH_TOKEN= +QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -67,7 +67,7 @@ Alternatively, you can set up a local tunnel. For this option: ```txt QSTASH_TOKEN="***" -UPSTASH_WORKFLOW_URL= +UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/fastapi.mdx b/workflow/quickstarts/fastapi.mdx index f822e1c8..2b73dd85 100644 --- a/workflow/quickstarts/fastapi.mdx +++ b/workflow/quickstarts/fastapi.mdx @@ -53,7 +53,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```bash .env export QSTASH_URL="http://127.0.0.1:8080" -export QSTASH_TOKEN= +export QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -67,7 +67,7 @@ Alternatively, you can set up a local tunnel. For this option: ```bash .env export QSTASH_TOKEN="***" -export UPSTASH_WORKFLOW_URL= +export UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/flask.mdx b/workflow/quickstarts/flask.mdx index 32c06365..1e37ba1a 100644 --- a/workflow/quickstarts/flask.mdx +++ b/workflow/quickstarts/flask.mdx @@ -53,7 +53,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```bash .env export QSTASH_URL="http://127.0.0.1:8080" -export QSTASH_TOKEN= +export QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -67,7 +67,7 @@ Alternatively, you can set up a local tunnel. For this option: ```bash .env export QSTASH_TOKEN="***" -export UPSTASH_WORKFLOW_URL= +export UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/hono.mdx b/workflow/quickstarts/hono.mdx index 3194b53b..cecffd13 100644 --- a/workflow/quickstarts/hono.mdx +++ b/workflow/quickstarts/hono.mdx @@ -62,7 +62,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```txt .dev.vars QSTASH_URL="http://127.0.0.1:8080" -QSTASH_TOKEN= +QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -76,7 +76,7 @@ Alternatively, you can set up a local tunnel. For this option: ```txt .dev.vars QSTASH_TOKEN="***" -UPSTASH_WORKFLOW_URL= +UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/nextjs-fastapi.mdx b/workflow/quickstarts/nextjs-fastapi.mdx index c98e1ae0..9635e424 100644 --- a/workflow/quickstarts/nextjs-fastapi.mdx +++ b/workflow/quickstarts/nextjs-fastapi.mdx @@ -62,7 +62,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```bash .env export QSTASH_URL="http://127.0.0.1:8080" -export QSTASH_TOKEN= +export QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -76,7 +76,7 @@ Alternatively, you can set up a local tunnel. For this option: ```bash .env export QSTASH_TOKEN="***" -export UPSTASH_WORKFLOW_URL= +export UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/nextjs-flask.mdx b/workflow/quickstarts/nextjs-flask.mdx index cfdaebf4..ae5dc3cd 100644 --- a/workflow/quickstarts/nextjs-flask.mdx +++ b/workflow/quickstarts/nextjs-flask.mdx @@ -62,7 +62,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```bash .env export QSTASH_URL="http://127.0.0.1:8080" -export QSTASH_TOKEN= +export QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -76,7 +76,7 @@ Alternatively, you can set up a local tunnel. For this option: ```bash .env export QSTASH_TOKEN="***" -export UPSTASH_WORKFLOW_URL= +export UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/nuxt.mdx b/workflow/quickstarts/nuxt.mdx index 1a2e8f6a..feda7802 100644 --- a/workflow/quickstarts/nuxt.mdx +++ b/workflow/quickstarts/nuxt.mdx @@ -63,7 +63,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```txt QSTASH_URL="http://127.0.0.1:8080" -QSTASH_TOKEN= +QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -77,7 +77,7 @@ Alternatively, you can set up a local tunnel. For this option: ```txt QSTASH_TOKEN="***" -UPSTASH_WORKFLOW_URL= +UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/platforms.mdx b/workflow/quickstarts/platforms.mdx index b897885f..84d1bcd6 100644 --- a/workflow/quickstarts/platforms.mdx +++ b/workflow/quickstarts/platforms.mdx @@ -4,25 +4,56 @@ title: "Supported Platforms" Upstash Workflow natively supports the following platforms: -**workflow-js** +### Javascript / Typescript -- [Next.js](/workflow/quickstarts/vercel-nextjs) -- [Cloudflare Workers](/workflow/quickstarts/cloudflare-workers) -- [Nuxt (H3)](/workflow/quickstarts/nuxt) -- [Solidjs](/workflow/quickstarts/solidjs) -- [Svelte](/workflow/quickstarts/svelte) -- [Hono](/workflow/quickstarts/hono) -- [Express.js](/workflow/quickstarts/express) -- [Astro](/workflow/quickstarts/astro) + + + **Next.js** + + + **Cloudflare Workers** + + + **Nuxt (H3)** + + + **Solid.js** + + + **Svelte** + + + **Hono** + + + **Express.js** + + + **Astro** + + -**workflow-py** -- [FastAPI](/workflow/quickstarts/fastapi) -- [Next.js & FastAPI](/workflow/quickstarts/nextjs-fastapi) -- [Flask](/workflow/quickstarts/flask) -- [Next.js & Flask](/workflow/quickstarts/nextjs-flask) +### Python -## Using Workflow with another platform + + + **FastAPI** + + + **Next.js & FastAPI** + + + **Flask** + + + **Next.js & Flask** + + + + + +## Using Upstash Workflow with another platform If you'd like to use Upstash Workflow for a platform not listed above, you can do so using the base `serve` method: @@ -38,4 +69,5 @@ from upstash_workflow import serve, async_serve -and adjust this method for your platform. For details, see our platform-specific method implementations in [workflow-js](https://github.com/upstash/workflow-js/tree/main/platforms) or [workflow-py](https://github.com/upstash/workflow-py/tree/master/upstash_workflow). +and adjust this method for your platform. For details, see our platform-specific method implementations in [Javascript](https://github.com/upstash/workflow-js/tree/main/platforms) or [Python](https://github.com/upstash/workflow-py/tree/master/upstash_workflow). + diff --git a/workflow/quickstarts/solidjs.mdx b/workflow/quickstarts/solidjs.mdx index 0cd2cc41..8645648e 100644 --- a/workflow/quickstarts/solidjs.mdx +++ b/workflow/quickstarts/solidjs.mdx @@ -59,7 +59,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```txt QSTASH_URL="http://127.0.0.1:8080" -QSTASH_TOKEN= +QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -73,7 +73,7 @@ Alternatively, you can set up a local tunnel. For this option: ```txt QSTASH_TOKEN="***" -UPSTASH_WORKFLOW_URL= +UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/svelte.mdx b/workflow/quickstarts/svelte.mdx index 672dd8c9..0fdaf9ab 100644 --- a/workflow/quickstarts/svelte.mdx +++ b/workflow/quickstarts/svelte.mdx @@ -63,7 +63,7 @@ Once the command runs successfully, you’ll see `QSTASH_URL` and `QSTASH_TOKEN` ```txt QSTASH_URL="http://127.0.0.1:8080" -QSTASH_TOKEN= +QSTASH_TOKEN="" ``` This approach allows you to test workflows locally without affecting your billing. However, runs won't be logged in the Upstash Console. @@ -77,7 +77,7 @@ Alternatively, you can set up a local tunnel. For this option: ```txt QSTASH_TOKEN="***" -UPSTASH_WORKFLOW_URL= +UPSTASH_WORKFLOW_URL="" ``` - Replace `***` with your actual QStash token. diff --git a/workflow/quickstarts/vercel-nextjs.mdx b/workflow/quickstarts/vercel-nextjs.mdx index 262803d4..247a9964 100644 --- a/workflow/quickstarts/vercel-nextjs.mdx +++ b/workflow/quickstarts/vercel-nextjs.mdx @@ -83,10 +83,10 @@ Next, you need to configure your Next.js app to connect with the local QStash se In the root of your project, create a `.env.local` file (or update your existing one) and add the values printed by the QStash local server: ```txt -QSTASH_URL=http://127.0.0.1:8080 -QSTASH_TOKEN=eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0= -QSTASH_CURRENT_SIGNING_KEY=sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r -QSTASH_NEXT_SIGNING_KEY=sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs +QSTASH_URL="http://127.0.0.1:8080" +QSTASH_TOKEN="eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" +QSTASH_CURRENT_SIGNING_KEY="sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r" +QSTASH_NEXT_SIGNING_KEY="sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs" ``` @@ -195,8 +195,8 @@ import { Client } from "@upstash/workflow"; const client = Client() const { workflowRunId } = await client.trigger({ - url: `http://localhost:3000/api/workflow`, - retries: 3, + url: `http://localhost:3000/api/workflow`, + retries: 3, }); ``` @@ -225,8 +225,8 @@ const BASE_URL = process.env.VERCEL_URL : `http://localhost:3000` const { workflowRunId } = await client.trigger({ - url: `${BASE_URL}/api/workflow`, - retries: 3, + url: `${BASE_URL}/api/workflow`, + retries: 3, }); ``` diff --git a/workflow/rest/flow-control/get.mdx b/workflow/rest/flow-control/get.mdx index 2af22034..ea38e499 100644 --- a/workflow/rest/flow-control/get.mdx +++ b/workflow/rest/flow-control/get.mdx @@ -8,7 +8,7 @@ authMethod: "bearer" ## Request - The key of the flow control. See the [flow control](/workflow/howto/flow-control) for more details. + The key of the flow control. See the [flow control](/workflow/features/flow-control) for more details. ## Response diff --git a/workflow/rest/flow-control/list.mdx b/workflow/rest/flow-control/list.mdx index 2a391b73..ecbc8820 100644 --- a/workflow/rest/flow-control/list.mdx +++ b/workflow/rest/flow-control/list.mdx @@ -9,7 +9,7 @@ authMethod: "bearer" - The key of the flow control. See the [flow control](/workflow/howto/flow-control) for more details. + The key of the flow control. See the [flow control](/workflow/features/flow-control) for more details. diff --git a/workflow/rest/runs/logs.mdx b/workflow/rest/runs/logs.mdx index d76af9a7..9c26076a 100644 --- a/workflow/rest/runs/logs.mdx +++ b/workflow/rest/runs/logs.mdx @@ -69,281 +69,7 @@ To uniquely identify a single workflow run, include the `workflowCreatedAt` time workflow runs. - - - - The ID of the workflow run. - - - - The URL address of the workflow server. - - - - The current state of the workflow run at this point in time - - | Value | Description | - | -------------- | -------------------------------------------------------------- | - | `RUN_STARTED` | The workflow has started to run and currently in progress. | - | `RUN_SUCCESS` | The workflow run has completed succesfully. | - | `RUN_FAILED` | Some errors has occured and workflow failed after all retries. | - | `RUN_CANCELED` | The workflow run has canceled upon user request. | - - - - - The Unix timestamp (in milliseconds) when the workflow run started. - - - - The Unix timestamp (in milliseconds) when the workflow run was completed, if applicable. - - - - The label of the run assigned by the user on trigger. - - - - The details of the failure callback message, if a failure function was defined for the workflow. - - - - The ID of the failure callback message - - - - The URL address of the failure function - - - - The state of the failure callback - - | Value | - | --------------------- | - | `CALLBACK_INPROGRESS` | - | `CALLBACK_SUCCESS` | - | `CALLBACK_FAIL` | - - - - - The HTTP headers of the message that triggered the failure function. - - - - The HTTP response status of the message that triggered the failure function. - - - - The response body of the message that triggered the failure function. - - - - The DLQ ID of the workflow run. - - - - Response body of the failure function/url. - When [failure function](https://upstash.com/docs/workflow/basics/serve#failurefunction) is used, this contains - the returned message from the failure function. - - - - Reponse headers of the failure function/url. This is valuable when the call to run the failure function/url is rejected - because of a platform limit. - - - - Reponse status of the failure function/url. This is valuable when the call to run the failure function/url is rejected - because of a platform limit. - - - - A call to failure url/function can be retried as `maxRetries` time. This array contains errors of all retry - attempts. - - - - Response status of the endpoint that caused the error - - - - Response Headers of the endpoint that caused the error - - - - Response Body of the endpoint that caused the error if available - - - - An error message that happened before/after calling the user's endpoint. - - - - The time of the error happened in Unix time milliseconds - - - - - - - Max number of retries configured when seeing an error. - - - - - - - - - - The type of grouped steps - - | Value | Description | - | ------------ | ---------------------------------------------------------------- | - | `sequential` | Indicates only one step is excuted sequentially | - | `parallel` | Indicates multiple steps being executed in parallel. | - | `next` | Indicates there is information about currently executing step(s) | - - - - - - - The ID of the step which increases monotonically. - - - - The name of the step. It is specified in workflow by user. - - - - Execution type of the step which indicates type of the context function. - - | Value | Function | - | ------------ | -------------------------------------------- | - | `Initial` | The default step which created automatically | - | `Run` | context.run() | - | `Call` | context.call() | - | `SleepFor` | context.sleepFor() | - | `SleepUntil` | context.sleepUntil() | - | `Wait` | context.waitForEvent() | - | `Notify` | context.notify() | - | `Invoke` | context.invoke() | - - - - The ID of the message associated with this step. - - - - The output returned by the step - - - - The total number of concurrent steps that is running alongside this step - - - - The state of this step at this point in time - - | Value | - | --------------- | - | `STEP_SUCCESS` | - | `STEP_RETRY` | - | `STEP_FAILED` | - | `STEP_CANCELED` | - - - - The unix timestamp in milliseconds when the message associated with this step has created. - - - - The unix timestamp in milliseconds when this step will be retried. - This is set only when the step state is `STEP_RETRY` - - - **The following fields are set only when a specific type of step is executing. These fields are not available for all step types.** - - - The duration in milliseconds which step will sleep. Only set if stepType is `SleepFor`. - - - - The unix timestamp (in milliseconds) which step will sleep until. Only set if stepType is `SleepUntil`. - - - - The event id of the wait step. Only set if stepType is `Wait`. - - - - The unix timestamp (in milliseconds) when the wait will time out. - - - - The duration of timeout in human readable format (e.g. 120s, 1m, 1h). - - - - Set to true if this step is cause of a wait timeout rather than notifying the waiter. - - - - The URL of the external address. Available only if stepType is `Call`. - - - - The HTTP method of the request sent to the external address. Available only if stepType is `Call`. - - - - The HTTP headers of the request sent to the external address. Available only if stepType is `Call`. - - - - The body of the request sent to the external address. Available only if stepType is `Call`. - - - - The HTTP status returned by the external call. Available only if stepType is `Call`. - - - - The body returned by the external call. Available only if stepType is `Call`. - - - - The HTTP headers returned by the external call. Available only if stepType is `Call`. - - - - The ID of the invoked workflow run if this step is an invoke step. - - - - The URL address of the workflow server of invoked workflow run if this step is an invoke step. - - - - The Unix timestamp (in milliseconds) when the invoked workflow was started if this step is an invoke step. - - - - The body passed to the invoked workflow if this step is an invoke step. - - - - The HTTP headers passed to invoked workflow if this step is an invoke step. - - - - - - - - + ```sh curl diff --git a/workflow/rest/runs/message-logs.mdx b/workflow/rest/runs/message-logs.mdx deleted file mode 100644 index 1cb0052b..00000000 --- a/workflow/rest/runs/message-logs.mdx +++ /dev/null @@ -1,346 +0,0 @@ ---- -title: "List message logs" -description: "Fetch flat event logs of workflow runs" -api: "GET https://qstash.upstash.io/v2/workflows/messageLogs" -authMethod: "bearer" ---- - - - The message logs provide a more detailed version of the [List Workflow Runs - API](/workflow/rest/runs/logs). If you need an overview of a workflow run, - that API is more useful, as it returns grouped and processed data. - - -Each step in a workflow is associated with a message. During its lifecycle, a message generates multiple logs. - -For example, when you trigger a workflow, a message is created to execute the first step of the workflow. This message logs a `CREATED` state and then waits to be processed by the server. Once successfully processed, it logs a `DELIVERED` state. Some data may be duplicated across these events. - -In addition to logs generated by messages, we also generate some logs at the workflow run level. For instance, when a workflow is triggered, we log a `RUN_STATED` state. Similarly, we log `RUN_FAILED`, `RUN_SUCCESS`, and other states to track execution status. - -The flat view returns all logs generated by a workflow run in their raw format. By checking the flat view, you can see all the lifecycle of workflow run. - -## Request - - - By providing a cursor you can paginate through all of the workflow runs. - - - - Filters logs by workflow run ID. - - - - Filters logs by the workflow server URL. - - - - Filters logs by creation date of workflow run (Unix timestamp in - milliseconds). - - - - Filter logs by state, either the state of an individual message or by a state that is generated at the workflow level. - -Refer to [Debug Logs](/qstash/howto/debug-logs) page for the list of states of an individual message. - -The list of states at the workflow level: - -| Value | Description | -| -------------- | -------------------------------------------------------------- | -| `RUN_STARTED` | The workflow has started to run and currently in progress. | -| `RUN_SUCCESS` | The workflow run has completed succesfully. | -| `RUN_FAILED` | Some errors has occured and workflow failed after all retries. | -| `RUN_CANCELED` | The workflow run has canceled upon user request. | -| `WAITER_ADDED` | A new waiter is registered for this workflow run. | -| `INVOKED` | A new workflow run started by this workflow. | - - - - - Filter logs by message ID of a particular step - - - - Filters logs by creation date of workflow run (Unix timestamp in - milliseconds). - - - - Filters logs starting from this date (Unix timestamp in milliseconds, - inclusive). - - - - Filters logs ending at this date (Unix timestamp in milliseconds, inclusive). - - - - Specifies the number of logs to return (default and max: 1000). - - -## Response - -Each field in the logs is associated with the workflow run and the steps that flushed it. - - - A cursor which you can use in subsequent requests to paginate through all - logs. If no cursor is returned, you have reached the end of the logs. - - - - - - The Unix timestamp (in milliseconds) indicating when the log was flushed. - - - - The state of the step or workflow run, depending on the log type. - - - - The ID of the workflow run. - - - - The URL address of the workflow server. - - - - The Unix timestamp (in milliseconds) when the workflow run started. - - - - The HTTP headers of the message associated with this step. Only set if the state is `ERROR`. - - - - The error text if this message has failed. Only set if the state is `ERROR`. - - - - The HTTP response status returned by the workflow server. Only set if the state is `ERROR`. - - - - The HTTP response body returned by the workflow server. Only set if the state is `ERROR`. - - - - The HTTP response headers returned by the workflow server. Only set if the state is `ERROR`. - - - - The ID of the invoker workflow, if this workflow was triggered by another workflow. Only set if the state is `RUN_STARTED`. - - - - The unix timestamp in milliseconds when the invoker workflow was started, if this workflow was triggered by another workflow. Only set if the state is `RUN_STARTED`. - - - - The URL of invoker workflow server, if this workflow was triggered by another workflow. Only set if the state is `RUN_STARTED`. - - - - The response returned by workflow if it has succesfully terminated. Only set if the state is `RUN_SUCCESS` - - - - The details of the step executed by this message. Available only if this log was flushed for a message, not at the workflow level like `RUN_STARTED`. - - - - The ID of the step which increases monotonically. - - - - The name of the step. It is specified in workflow by user. - - - - Execution type of the step which indicates type of the context function. - - | Value | Function | - | ------------ | -------------------------------------------- | - | `Initial` | The default step which created automatically | - | `Run` | context.run() | - | `Call` | context.call() | - | `SleepFor` | context.sleepFor() | - | `SleepUntil` | context.sleepUntil() | - | `Wait` | context.waitForEvent() | - | `Notify` | context.notify() | - | `Invoke` | context.invoke() | - - - - - The ID of the message associated with this step. - - - - The output returned by the step - - - - The total number of concurrent steps that is running alongside this step - - - - The unix timestamp in milliseconds when the message associated with this step has created. - - - **The following fields are set only when a specific type of step is executing. These fields are not available for all step types.** - - - The unix timestamp (in milliseconds) which step will sleep until. Only set if stepType is `SleepUntil`. - - - - The duration in milliseconds which step will sleep. Only set if stepType is `SleepFor`. - - - - The URL of the external address. Available only if stepType is `Call`. - - - - The HTTP method of the request sent to the external address. Available only if stepType is `Call`. - - - - The HTTP headers of the request sent to the external address. Available only if stepType is `Call`. - - - - The body of the request sent to the external address. Available only if stepType is `Call`. - - - - The HTTP status returned by the external call. Available only if stepType is `Call`. - - - - The body returned by the external call. Available only if stepType is `Call`. - - - - The HTTP headers returned by the external call. Available only if stepType is `Call`. - - - - The event id of the wait step. Only set if stepType is `Wait`. - - - - The unix timestamp (in milliseconds) when the wait will time out. - - - - The duration of timeout in human readable format (e.g. 120s, 1m, 1h). - - - - Set to true if this step is cause of a wait timeout rather than notifying the waiter. - - - - The ID of the invoked workflow run if this step is an invoke step. - - - - The URL address of the workflow server of invoked workflow run if this step is an invoke step. - - - - The Unix timestamp (in milliseconds) when the invoked workflow was started if this step is an invoke step. - - - - The body passed to the invoked workflow if this step is an invoke step. - - - - The HTTP headers passed to invoked workflow if this step is an invoke step. - - - - - - - - - ```sh curl - curl https://qstash.upstash.io/v2/workflows/messageLogs \ - -H "Authorization: Bearer " - ``` - -```javascript Node -const response = await fetch( - "https://qstash.upstash.io/v2/workflows/messageLogs", - { - headers: { - Authorization: "Bearer ", - }, - } -); -``` - -```python Python -import requests -headers = { - 'Authorization': 'Bearer ', -} - -response = requests.get( - 'https://qstash.upstash.io/v2/workflows/messageLogs', - headers=headers -) -``` - -```go Go -req, err := http.NewRequest("GET", "https://qstash.upstash.io/v2/workflows/messageLogs", nil) -if err != nil { - log.Fatal(err) -} -req.Header.Set("Authorization", "Bearer ") -resp, err := http.DefaultClient.Do(req) -if err != nil { - log.Fatal(err) -} -defer resp.Body.Close() -``` - - - - - ```json 200 OK - { - "cursor": "", - "events": [ - { - "time": 1738788333107, - "state": "CREATED", - "workflowRunId": "wfr_6MXE3GfddpBMWJM7s5WSRPqwcFm8", - "workflowUrl": "http://my-workflow-server.com/workflowEndpoint", - "workflowCreatedAt": 1738788333105, - "stepInfo": { - "stepName": "init", - "stepType": "Initial", - "callType": "step", - "messageId": "msg_2KxeAKGVEjwDjNK1TVPormoRf7shRyNBpPThVbpvkuZNqri4cXp5nwSajNzAs6UWakvbco3qEPvtjQU3qxqjWarm2kisK", - "concurrent": 1, - "createdAt": 1738788333106 - }, - "nextDeliveryTime": 1738788333106 - }, - { - "time": 1738788333107, - "state": "RUN_STARTED", - "workflowRunId": "wfr_6MXE3GfddpBMWJM7s5WSRPqwcFm8", - "workflowUrl": "http://my-workflow-server.com/workflowEndpoint", - "workflowCreatedAt": 1738788333105 - } - ] - } - ``` - diff --git a/workflow/roadmap.mdx b/workflow/roadmap.mdx index 1768cc4a..be77b4ce 100644 --- a/workflow/roadmap.mdx +++ b/workflow/roadmap.mdx @@ -2,8 +2,8 @@ title: "Roadmap" --- -- Enhance the Upstash Workflow SDK to expose and configure all QStash features: - - Ordered execution of workflow runs. +- A dashboard to monitor and manage flow control feature. +- Ordered execution of workflow runs. - Expand language support with additional SDKs: Go and other languages based on user requests. - Achieve feature parity between `workflow-py` and `workflow-js`, bringing Python SDK capabilities up to match TypeScript implementation - Human in the loop in [the Agents API](/workflow/agents/overview). diff --git a/workflow/troubleshooting/general.mdx b/workflow/troubleshooting/general.mdx index a9cd54df..379f4d29 100644 --- a/workflow/troubleshooting/general.mdx +++ b/workflow/troubleshooting/general.mdx @@ -300,4 +300,16 @@ If you’re using Express.js behind a front-facing proxy, an alternative solutio ```javascript app.set("trust proxy", true) -``` \ No newline at end of file +``` + +## Workflow Stuck in First Step + +Imagine that you trigger your workflow with [client.trigger](/workflow/basics/client/trigger) and the workflow starts (you see the run in the dashboard), but it doesn't proceed beyond the first step. + +If it appears like the initial step has failed: +- Expand the step in the dashboard to see detailed logs to check for any error messages or stack traces that can provide insights into what went wrong. +- Ensure that the workflow endpoint is accessible. You can test by sending a `curl` request to see if the request lands on the endpoint. +- Ensure that the URL you passed to the `client.trigger` method matches the workflow endpoint URL. + +If it appears like the initial step has completed but the workflow is still stuck: +- Workflow SDK could be unable to correctly infer the workflow URL due to a proxy or an issue in the request object. To check if this is the issue, try passing the [`baseUrl` parameter to the `serve` method](/workflow/basics/serve/advanced#param-base-url). This will override the automatic URL inference and use the provided base URL instead.