Workflow

by get-convex6ca54f6e2e75No licenseListed Oct 8, 2026Updated Oct 8, 2026

Build a durable multi-step pipeline on Convex where each step runs in order and is retried independently on failure (transcribe→summarize→email, ETL, order fulfillment, any 'do A then B then C, retry each' job). Use @convex-dev/workflow — do NOT hand-roll a chain of scheduler calls or a custom jobs table. TRIGGER on multi-step / pipeline / 'retry each step' / long-running orchestration requests.

AI-generated overview

Guides building durable multi-step pipelines on Convex with the @convex-dev/workflow component, retrying each step independently.

What it does
Explains how to wire the @convex-dev/workflow component into a Convex app and define a workflow whose stages each run as a durable, independently retried step. It shows configuration of default and per-step retry behavior, starting a workflow and checking its status, and the rule that handlers receive step rather than ctx. It also lists anti-patterns such as hand-rolled job tables with chained scheduler calls.
When to use it
Use when a task needs ordered multi-step orchestration on Convex, such as transcribe then summarize then email, ETL, or order fulfillment, where each step must be retried independently and survive crashes. It is intended for requests described as pipelines, multi-step, retry-each-step, or long-running orchestration.
Requirements
Convex project with the @convex-dev/workflow component installed; instructions only, no scripts. Related components such as @convex-dev/resend are referenced for email.

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

ts
// convex/convex.config.tsimport { defineApp } from "convex/server";import workflow from "@convex-dev/workflow/convex.config";const app = defineApp();app.use(workflow);export default app;

Define the workflow — one step.run* call per stage, retried independently

ts
// convex/workflows.tsimport { WorkflowManager } from "@convex-dev/workflow";import { components, internal } from "./_generated/api";import { v } from "convex/values";
export const workflow = new WorkflowManager(components.workflow, {  // Per-step default: retry each failed step independently with backoff.  defaultRetryBehavior: { maxAttempts: 4, initialBackoffMs: 1000, base: 2 },  retryActionsByDefault: true,});
export const transcribeAndSummarize = workflow.define({  args: { url: v.string(), userEmail: v.string() },  handler: async (step, args): Promise<void> => {    // Each step.runAction is durable + independently retried. If summarize fails    // 3× then succeeds, transcribe is NOT re-run — completed steps are memoized.    const transcript = await step.runAction(internal.youtube.transcribe, { url: args.url });    const summary = await step.runAction(internal.llm.summarize, { transcript });    await step.runAction(internal.email.sendSummary, { to: args.userEmail, summary });  },});
  • The handler's first arg is step, not ctx. Call step.runAction / step.runMutation / step.runQuery with a codegen'd internal.* reference — never ctx.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 target internalAction/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 (see convex-external-apis), email via @convex-dev/resend (see crons).

Start it (and optionally track status)

ts
// from a public mutation/action the client calls:const workflowId = await workflow.start(  ctx,  internal.workflows.transcribeAndSummarize,  { url, userEmail },);// status later: await workflow.status(ctx, workflowId)  → cleanup: workflow.cleanup(ctx, workflowId)

Don't

  • ❌ A custom jobs/pipeline table + ctx.scheduler.runAfter chain to fake retries/ordering — that's what the component exists to replace.
  • ❌ ctx.runAction inside the workflow handler — use step.runAction or 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.

Source and attribution

Source:get-convex/convex-backend-skillinskills/workflowat commit6ca54f6

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal