Skip to content

Fan-in fan-out

ctx.WhenAll schedules several activities together and resumes past the join once all of them have completed. Like every step, the fan-out is durable: a crash mid-flight resumes from recorded branch outcomes. A branch whose external side effect completed but whose outcome was not recorded can execute again, so activities still require idempotent side effects.

Use ctx.CallActivity to create the pending activities, then await them together. Up to five differently-typed activities come back as a tuple:

var (hello, goodbye) = await ctx.WhenAll(
ctx.CallActivity((HelloActivities a) => a.Hello(input.Name)),
ctx.CallActivity((HelloActivities a) => a.Goodbye(input.GoodbyeId)),
cancellationToken
);

For more than five differently-typed branches, give the activities a shared result type and use the collection overload below. Note that splitting one fan-out into two consecutive ctx.WhenAll calls does not keep everything parallel: activities are only enqueued when ctx.WhenAll runs, so the first batch completes before the second starts.

When every branch has the same result type, pass a sequence and get an array back:

var results = await ctx.WhenAll(
customers.Select(c =>
ctx.CallActivity((EmailActivities a) => a.SendNewsletter(c.Email))
),
cancellationToken
);

The whole batch is enqueued together, so activity workers pick the branches up side by side; how many actually run at once is bounded by your workers’ MaxConcurrency.

ctx.WhenAll matches Task.WhenAll semantics: it waits for every branch to finish before surfacing anything, then throws the first failure (by position). Branches whose success was recorded keep their results; workflow replay memoizes and returns those results without deliberately scheduling another job. The failure then propagates like any workflow failure: the run fails (after its workflow-level attempts) and any registered compensations run.

Each branch is its own persisted step backed by its own activity job, so a fan-out of fifty is fifty step rows and fifty job rows. While branches execute, the run parks without holding a worker slot. Currently every branch completion attempts to wake the parent; an early replay observes outstanding siblings and parks again, while the replay after the last completion passes the join. Recorded branches return their stored results, and unresolved branches are waited for again.