Dynamic Workflows
This example demonstrates how to build dynamic, configurable workflows using Upstash Workflow, while safely handling ordering, naming, and versioning constraints. The workflow dynamically executes a list of steps provided at runtime, allowing different customers or versions to run different flows, without breaking the workflow resolution mechanism.
Use Case
Our workflow will:
- Receive a list of steps to execute
- Execute each step in order, one by one
- Persist step results between requests
- Support multiple workflow versions with different step orders
- Ensure workflows do not break when retried or resumed
This pattern is useful when:
- Customers want dynamic workflows with different type of steps and ordering
- Workflow logic is driven by configuration
- You need safe retries, resumes, and idempotency
Code Example
import { WorkflowNonRetryableError } from "@upstash/workflow";import { serve } from "@upstash/workflow/nextjs";const addOne = (data: number): number => { return data + 1}const multiplyWithTwo = (data: number): number => { return data * 2}type FunctionName = "AddOne" | "MultiplyWithTwo"const functions: Record<FunctionName, (data: number) => number> = { AddOne: addOne, MultiplyWithTwo: multiplyWithTwo,}interface WorkflowPayload { version: string functions: FunctionName[]}export const { POST } = serve<WorkflowPayload>(async (context) => { const { functions: steps } = context.requestPayload let lastResult = 0 for (let i = 0; i < steps.length; i++) { const stepName = steps[i] lastResult = await context.run( `step-${i}:${stepName}`, async () => { const fn = functions[stepName] if (!fn) throw new WorkflowNonRetryableError("Unknown step") return fn(lastResult) } ) }})Code Breakdown
1. Dynamic Step Configuration
Instead of hardcoding the workflow, we accept a list of step names from the request payload.
interface WorkflowPayload { version: string functions: FunctionName[]}This allows different customers or versions to define workflow flows such as:
- Run X:
AddOne → MultiplyWithTwo - Run Y:
MultiplyWithTwo → AddOne → AddOne - Run Z:
AddOne
2. Executing Steps One by One
At first glance, the for loop looks like a normal synchronous loop. However, in Upstash Workflow, each iteration of the loop (in other terms, every step) is executed across multiple HTTP requests, not in a single function invocation.
let lastResult = 0for (let i = 0; i < steps.length; i++) { const stepName = steps[i] lastResult = await context.run( `step-${i}:${stepName}`, async () => { const fn = functions[stepName] if (!fn) throw new WorkflowNonRetryableError("Unknown step") return fn(lastResult) } )}Here is what actually happens behind the scenes:
First request
- The workflow endpoint is called with the initial payload.
- The loop starts at
i = 0. context.run("step-0:AddOne")is encountered.- Since this step has never run before, Upstash executes the function body.
- The result is stored in durable state.
- The HTTP request ends immediately after this step completes.
Second request
- Upstash triggers the workflow endpoint again.
- The request payload now includes the result of
step-0. - The loop runs again from the beginning.
context.run("step-0:AddOne")is encountered, but it is skipped because it already exists in state.- The loop continues to
i = 1. context.run("step-1:MultiplyWithTwo")executes.- The result is persisted, and the request ends.
Subsequent requests
- This process repeats until every step in the loop has been executed exactly once.
- Each iteration of the loop corresponds to a separate HTTP execution.
This is critical — each logical step must be isolated in its own context.run call so the workflow engine can:
- Resume execution safely
- Skip completed work
- Retry failed steps independently
- Guarantee exactly-once execution semantics
If you place multiple logical operations inside a single context.run, the engine cannot resume partway through that logic.
3. Step Naming and Ordering
Upstash Workflow identifies steps using:
- The order of
context.runcalls - The step name passed to
context.run
For a given workflow execution:
- Step names must not change between retries
- Step order must remain the same
Changing either will break the resolve mechanism.
4. Versioning Workflows Safely
If you want to change:
- Step order
- Step names
- Number of steps
You must create a new version:
- Keep old versions immutable
- Route versions inside the same endpoint if needed
- Ensure each version always executes the same flow
Example:
version = v1→AddOne → MultiplyWithTwoversion = v2→MultiplyWithTwo → AddOne
As long as each version is internally consistent, the workflow will work correctly.
5. How the Step Result Resolve Mechanism Works
Behind the scenes, the workflow endpoint is called multiple times.
On each request:
- The request contains the initial payload
- Plus results of already executed steps
- The engine determines which step is next
- Only the next step is executed
As long as the workflow definition does not change, execution resumes correctly.
6. Common Pitfalls
Avoid the following:
- ❌ Running multiple logical steps inside a single
context.run - ❌ Changing step names and order between executions
- ❌ Conditional execution based on non-deterministic logic (
Math.random,Date.now)
All workflow logic must be idempotent.