Job Continuations
Continuations let you build multi-step workflows where a child job runs only after its parent succeeds. If the parent fails permanently or is cancelled, the children are automatically cancelled too.
Creating a continuation
Use ContinueWithAsync to enqueue a job that waits for another job to complete:
var orderId = await jobs.EnqueueAsync<CreateOrder>(
new CreateOrderPayload { ProductId = "prod_42", Quantity = 1 });
var receiptId = await jobs.ContinueWithAsync<SendReceipt>(
orderId,
new ReceiptPayload { OrderId = orderId, Email = "user@example.com" });
The child job is created immediately and assigned an ID, but it stays in Scheduled state until the parent succeeds.
State lifecycle
The child transitions through these states:
| Phase | Child state | Trigger |
|---|---|---|
| Created with parent still running | Scheduled | ContinueWithAsync call |
| Parent succeeds | Pending | Server activates child on parent success ack |
| Worker picks up child | Processing | Normal poll/claim cycle |
| Child completes | Succeeded | Worker ack |
What happens when the parent fails
Parent dead-letters
When a parent exhausts all retry attempts and moves to DeadLetter, all children in Scheduled state are automatically cancelled with their CompletedAt set:
Parent is explicitly cancelled
Same behavior — cancelling a parent cancels all Scheduled children:
await jobs.CancelAsync(parentJobId);
// Parent → Cancelled
// All Scheduled children → Cancelled
Parent fails but has retries left
When a parent fails and retries, the children stay in Scheduled state. They are only activated when the parent eventually succeeds, or cancelled if it dead-letters.
Late continuation
If you call ContinueWithAsync after the parent has already succeeded, the child starts as Pending immediately — no waiting:
// Parent already succeeded 5 minutes ago
var childId = await jobs.ContinueWithAsync<GenerateInvoice>(
parentJobId,
new InvoicePayload { OrderId = parentJobId });
// childId is Pending immediately, ready for processing
Blocked continuation
If the parent is already in a terminal failure state (Cancelled or DeadLetter), the API rejects the continuation with 409 parent_terminal:
try
{
await jobs.ContinueWithAsync<SendReceipt>(cancelledParentId, payload);
}
catch (FlareConflictException ex) when (ex.ErrorCode == "parent_terminal")
{
logger.LogWarning("Cannot add continuation — parent is {State}", ex.Message);
}
If the parent job ID does not exist at all, the API returns 422 parent_not_found.
Fan-out pattern
Multiple children can depend on the same parent. When the parent succeeds, all children are activated simultaneously:
var order = new OrderPayload { CustomerId = "cust_1", Email = "user@example.com",
Items = new[] { "sku_100", "sku_200" }, Total = 59.99m };
var orderId = await jobs.EnqueueAsync<ProcessOrder>(order);
// Fan-out: three independent jobs that all run after the order is processed
var emailId = await jobs.ContinueWithAsync<SendConfirmationEmail>(
orderId, new { OrderId = orderId, order.Email });
var inventoryId = await jobs.ContinueWithAsync<UpdateInventory>(
orderId, new { OrderId = orderId, order.Items });
var analyticsId = await jobs.ContinueWithAsync<TrackOrderEvent>(
orderId, new { OrderId = orderId, order.Total });
When ProcessOrder succeeds, all three children move from Scheduled to Pending and are processed independently by available workers.
Multi-level chains
You can chain continuations across multiple levels. Each level waits for the previous one:
var order = new OrderPayload { Id = "ord_99", Total = 149.00m,
ShippingAddress = "123 Main St", Email = "buyer@example.com" };
var step1 = await jobs.EnqueueAsync<ValidateOrder>(order);
var step2 = await jobs.ContinueWithAsync<ChargePayment>(
step1, new { order.Id, order.Total });
var step3 = await jobs.ContinueWithAsync<ShipOrder>(
step2, new { order.Id, order.ShippingAddress });
var step4 = await jobs.ContinueWithAsync<SendShippingNotification>(
step3, new { order.Id, order.Email });
This creates a pipeline: ValidateOrder → ChargePayment → ShipOrder → SendShippingNotification. Each step only runs after the previous step succeeds.
Different job types
Parent and child jobs can be completely different IJob<T> implementations with different payload types. The continuation relationship is purely based on job IDs — there is no type coupling:
// Parent: processes an image (IJob<ImagePayload>)
var uploadId = await jobs.EnqueueAsync<ProcessImage>(imagePayload);
// Child: sends a notification (IJob<NotificationPayload>)
await jobs.ContinueWithAsync<NotifyUser>(
uploadId,
new NotificationPayload { UserId = "usr_42", Message = "Your image is ready" });
Continuations with options
You can pass JobOptions to ContinueWithAsync for queue assignment, timeouts, idempotency, and more:
await jobs.ContinueWithAsync<SendReceipt>(parentJobId, payload, new JobOptions
{
Queue = "email",
MaxAttempts = 5,
IdempotencyKey = $"receipt:{parentJobId}",
Timeout = TimeSpan.FromMinutes(2)
});
Limitations
-
No fan-in — a child can only have one parent. There is no built-in way to wait for multiple jobs to complete before triggering a child.
-
Success-only activation — children are activated only on parent success. There are no conditional continuations (e.g., "run this child only if the parent fails").
-
Cancelled children are not restored — when a parent dead-letters or is cancelled, its children are cancelled. If you later retry the parent via
RetryAsync, the previously cancelled children are not automatically restored. You would need to re-enqueue them as new continuations. -
No payload passing — the parent's result is not automatically passed to the child. The child receives its own payload that you specify at enqueue time. If you need data from the parent, store it in a shared database or include it in the child's payload.
See also
- IJobClient —
ContinueWithAsyncand other methods - Error Handling — handling
FlareConflictExceptionfor blocked continuations - Retry Strategies — how parent retries interact with child activation