Job Chain Patterns
Chains support various execution patterns via continueWith:
Linear
Section titled “Linear”Jobs execute one after another: create-subscription -> activate-trial
type Definitions = { 'create-subscription': { entry: true; input: { userId: string; planId: string }; continueWith: { typeName: 'activate-trial' }; }; 'activate-trial': { input: { subscriptionId: number; trialDays: number }; continueWith: { typeName: 'trial-decision' }; };};
// In processor'create-subscription': { attemptHandler: async ({ job, complete }) => { return complete(async ({ sql, continueWith }) => { const [sub] = await sql`INSERT INTO subscriptions ... RETURNING id`; return continueWith({ typeName: "activate-trial", input: { subscriptionId: sub.id, trialDays: 7 }, }); }); },},Branched
Section titled “Branched”Jobs conditionally continue to different types: trial-decision -> convert-to-paid | expire-trial
'trial-decision': { input: { subscriptionId: number }; continueWith: { typeName: 'convert-to-paid' | 'expire-trial' }; // Union type};
// In processor - choose path based on condition'trial-decision': { attemptHandler: async ({ job, complete }) => { const shouldConvert = userWantsToConvert; return complete(async ({ continueWith }) => { return continueWith({ typeName: shouldConvert ? "convert-to-paid" : "expire-trial", input: { subscriptionId: job.input.subscriptionId }, }); }); },},Jobs continue to the same type: charge-billing -> charge-billing -> ... -> done
type Definitions = { 'charge-billing': { input: { subscriptionId: number; cycle: number }; output: { finalCycle: number; totalCharged: number }; // Terminal output continueWith: { typeName: 'charge-billing' }; // Self-reference for looping };};
// In processor - loop or terminate with output'charge-billing': { attemptHandler: async ({ job, complete }) => { await chargePayment(job.input.subscriptionId); return complete(async ({ continueWith }) => { if (job.input.cycle < MAX_CYCLES) { return continueWith({ typeName: "charge-billing", input: { subscriptionId: job.input.subscriptionId, cycle: job.input.cycle + 1 }, }); } return { finalCycle: job.input.cycle, totalCharged: calculateTotal() }; }); },},Jobs jump to different types: charge-billing -> cancel-subscription
type Definitions = { 'charge-billing': { input: { subscriptionId: number; cycle: number }; output: { finalCycle: number; totalCharged: number }; continueWith: { typeName: 'charge-billing' | 'cancel-subscription' }; // Loop or jump }; 'cancel-subscription': { input: { subscriptionId: number; reason: string }; output: { cancelledAt: string }; };};
// In processor - jump to cancel when max cycles reached'charge-billing': { attemptHandler: async ({ job, complete }) => { return complete(async ({ continueWith }) => { if (job.input.cycle >= MAX_CYCLES) { return continueWith({ typeName: "cancel-subscription", input: { subscriptionId: job.input.subscriptionId, reason: "max_billing_cycles_reached" }, }); } return continueWith({ typeName: "charge-billing", input: { subscriptionId: job.input.subscriptionId, cycle: job.input.cycle + 1 }, }); }); },},See examples/showcase-chain-patterns for a complete working example demonstrating all four patterns through a subscription lifecycle workflow. See also Job Blockers for parallel dependencies and Job Chain Model reference.