Durable multi-step workflows → @convex-dev/workflow
When the task is "do step A, then B, then C, and retry each step independently if it fails" — a pipeline, ETL, or orchestration that must survive crashes — use the workflow component. Do NOT hand-roll it with a jobs table + chained ctx.scheduler.runAfter calls: that reinvents durability, loses per-step retry/backoff, and (measured) scores worse than a plain implementation. Copy this pattern.
Wire the component
Define the workflow — one step.run* call per stage, retried independently
- The handler's first arg is
step, notctx. Callstep.runAction/step.runMutation/step.runQuerywith a codegen'dinternal.*reference — neverctx.run*inside a workflow (that breaks durability/memoization). - Each
step.run*is a durable checkpoint. On crash or retry, completed steps are replayed from their stored result, not re-executed — so steps must targetinternalAction/internalMutations that do the real work. - Override retry per step when one stage is flakier:
step.runAction(ref, args, { retry: { maxAttempts: 6, initialBackoffMs: 500, base: 2 } }). Set{ retry: false }for a step that must not repeat (already-idempotent external charge). - The actual work (the YouTube fetch, the LLM call, the email send) lives in ordinary
internalActions — external APIs go in actions (seeconvex-external-apis), email via@convex-dev/resend(seecrons).
Start it (and optionally track status)
Don't
- ❌ A custom
jobs/pipelinetable +ctx.scheduler.runAfterchain to fake retries/ordering — that's what the component exists to replace. - ❌
ctx.runActioninside the workflow handler — usestep.runActionor you lose durability. - ❌ Long synchronous work in one action to dodge steps — you lose independent retry and the 10-min action ceiling still applies per step.




