Composing sagas
.saga() inlines another saga's steps into the one you're building.
const paymentSaga = defineSaga<PaymentInput, Services>("payment")
.task({ name: "authorize", /* ... */ })
.task({ name: "capture", /* ... */ })
.build(services);
const checkout = defineSaga<OrderInput, Services>("checkout")
.task({ name: "validate", /* ... */ })
.saga(paymentSaga)
.task({ name: "confirm", /* ... */ })
.build(services);The child's result type is merged into the parent's, so results.authorize and results.capture are available to confirm and typed.
It is inlining, not nesting
The child's nodes are copied into the parent. Nothing about the child survives:
- Its context is discarded. The nodes run against the parent's context. You still have to call
.build()on the child, because that is what produces theSagaobject.saga()takes, but whatever you pass is thrown away. - Its task hooks come along, scoped. A child's
onTaskStartand friends fire only for the tasks that came from the child; the parent's fire for everything, and run first. ItsonSagaStart/onSagaCompleteare dropped; there is no child saga left to start or finish. A later.use()on the parent replaces the parent's own hooks, never an inlined child's. - Its name is discarded. It shows up nowhere in the result.
- Rollback is one flat sequence. A failure in
confirmcompensatescapture,authorizeandvalidatein that order. There is no "the child saga rolled back" boundary.
The child's task names share one namespace with the parent's, so a collision is a build() error, including inlining the same child twice.
Adapting input
The child was written against its own input shape. adapt maps the parent's input and results into it:
.saga(paymentSaga, {
adapt: ({ input, results, context }) => ({
amount: results.validate.total + results.tax.amount,
currency: input.currency,
reference: input.orderId,
}),
})adapt runs per callback, at the moment that callback runs, not once up front. So it sees results as of that point in the saga, and a child task late in the sequence gets a fresher view than an early one. Keep adapt pure and cheap; it is called for every execute, compensate, iterator and key in the child.
Without adapt, the child's nodes receive the parent's input unchanged. That type-checks when the parent's input satisfies the child's, and is the reason adapt is optional.
A single reused task has the same mismatch without a child saga to wrap it, so every step method takes adapt too. See adapt.
Why bother
Two honest reasons: a payment sequence written once and reused by checkout, renewal and top-up; or a long saga split into named files so each is readable.
If it is neither (if the "child" is used exactly once, in one parent), the indirection is buying you nothing. Just write the tasks in the parent.