Skip to content

Chain Patterns

Chains support various execution patterns via continueWith:

  • LinearcontinueWith returns the next type
  • BranchedcontinueWith returns one of a union of types
  • LoopcontinueWith returns the same type until a terminal output
  • Go-tocontinueWith jumps to a different type, skipping intermediates
Diagram

Jobs execute one after another: create-subscription -> activate-trial

const jobTypes = defineJobTypes<{
'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 },
});
});
},
},
Diagram

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 },
});
});
},
},
Diagram

Jobs continue to the same type: charge-billing -> charge-billing -> ... -> done

const jobTypes = defineJobTypes<{
'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() };
});
},
},
Diagram

Jobs jump to a different type mid-chain: charge-billing -> cancel-subscription

const jobTypes = defineJobTypes<{
'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 },
});
});
},
},

All examples above use nominal references{ typeName: "..." }. Queuert also supports structural references ({ input: {...} }) that match any job type with a compatible input shape, enabling loose coupling. See Job Type References for details and examples.

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 Chain Model reference.