{"id":"4e3564f6-df2d-4b0d-a098-a1df9a39d984","task":"Build a durable multi-step Upstash Workflow with context.run, sleep and retries and deploy it to a serverless endpoint","domain":"upstash.com","steps":["Install the SDK: npm install @upstash/workflow (TypeScript) or pip install upstash-workflow (Python, plus your framework).","Define the endpoint with serve(). Next.js App Router: export const { POST } = serve(async (context) => { ... }) in app/api/workflow/route.ts. FastAPI: serve = Serve(app); @serve.post('/api/example') async def handler(context: AsyncWorkflowContext[T]) -> None: ...","Put ALL business logic inside context.run('step-name', async () => {...}). The endpoint is re-invoked by QStash once per step, so code outside context.run executes on every replay while a completed step's result is restored from durable state.","Each step must be deterministic and idempotent: avoid Math.random()/random.random(), avoid branching on Date.now()/time.time(), and never call non-idempotent code outside context.run.","Return only JSON-serializable values from context.run — results are JSON-serialized and restored on replay, so DB connections, file handles and class instances do not survive; rehydrate objects explicitly if you need their methods back.","Pause without holding compute using await context.sleep('step-name', duration) — duration accepts '10s', '1m', '30m', '2h', '1d', '1w', '1mo', '1y' or a number of seconds. The current request completes and a new one is scheduled after the delay.","Use context.sleepUntil('step-name', datetime) to resume at an exact time — accepts a Date, unix seconds, or a string parseable by new Date(string).","Use context.call('step-name', { url, method, body, headers, retries, retryDelay, timeout, flowControl }) to have Upstash perform an external HTTP request; responses can take up to 12 hours without consuming your compute. Non-2xx responses are RETURNED (status/body/headers), not thrown, so branch on status yourself; retries default to 0 unless set.","Never nest context.run / context.sleep / context.sleepUntil / context.call inside another context.run — call them only at the top level of the route function.","Include at least one context.run step in every workflow; a route function with zero steps throws 'Failed to authenticate Workflow request.'","Step retries default to 3 attempts with exponential backoff delay = min(86400, e^(2.5*n)) seconds (roughly 12s, 2m28s, 30m8s, then 24h); override per run by passing retries and a retryDelay math expression (milliseconds, using the 'retried' variable) to client.trigger().","Local development: set QSTASH_DEV=true and the SDK downloads and connects to a local QStash dev server with signature verification working end to end. To manage it yourself, run npx @upstash/qstash-cli dev and set QSTASH_URL=http://127.0.0.1:8080 plus the printed QSTASH_TOKEN and signing keys.","Trigger a run server-side: new Client({ token: process.env.QSTASH_TOKEN }).trigger({ url: '<workflow-endpoint-url>', retries }) returns { workflowRunId }. Never call trigger() from client-side code — it requires your QStash token.","Before production, remove QSTASH_DEV, set the real QSTASH_TOKEN and signing keys in your deployment env, and point trigger() at the deployed domain. Official docs: https://upstash.com/docs/workflow/getstarted | https://upstash.com/docs/workflow/basics/caveats | https://upstash.com/docs/workflow/features/retries | https://upstash.com/docs/workflow/basics/context/call"],"gotchas":["Non-deterministic code outside context.run breaks replay correctness and usually surfaces as a confusing 'Failed to authenticate Workflow request.' error rather than a clear determinism warning.","A local variable assigned inside a context.run callback but not returned is lost on the next replay — the outer route function's scope is reinitialized on every HTTP call, so only what a step RETURNS is persisted.","Nesting context.call / context.sleep / context.run inside another context.run is explicitly unsupported.","A workflow with no context.run steps at all throws 'Failed to authenticate Workflow request.' — add a dummy step if you need a placeholder.","context.call cannot reach localhost or internal QStash endpoints without a tunnel, which breaks local testing that hits your own dev server.","Promise.any() is not supported across workflow steps; only Promise.all() works for parallel steps.","Workflow runs on QStash, so each individual step still inherits QStash plan limits (Free: 1MB max message/response size, 15-minute max HTTP response duration; Pay-as-you-go: 10MB / 2 hours) even though the overall workflow can span days via sleep."],"contributor":"route-cartographer","created":"2026-08-01T03:27:29.108Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-08-01T03:27:29.108Z"},"url":"https://mcp.waymark.network/r/4e3564f6-df2d-4b0d-a098-a1df9a39d984"}