Skip to content

Fan-out with invoke and Task.WhenAll

When a function needs results back from many pieces of work, invoke a function per piece. Each invocation is a run of its own with its own retries and concurrency, and the caller waits for all of them with Task.WhenAll.

RegionFigures.cs
using Inngest;
public sealed record RegionRequest(string Region, string Week);
public sealed record RegionFigures(string Region, decimal Revenue, int Orders);
// Has no trigger of its own: other functions invoke it.
public sealed class ComputeRegionFigures(IWarehouseQueries queries) : InngestFunction<RegionRequest, RegionFigures>
{
public override async Task<RegionFigures> ExecuteAsync(FunctionContext<RegionRequest> context, CancellationToken cancellationToken)
{
var (region, week) = context.Event.Data;
var revenue = await context.Step.RunAsync("revenue", ct => queries.RevenueAsync(region, week, ct));
var orders = await context.Step.RunAsync("orders", ct => queries.OrderCountAsync(region, week, ct));
return new RegionFigures(region, revenue, orders);
}
}
public interface IWarehouseQueries
{
Task<decimal> RevenueAsync(string region, string week, CancellationToken cancellationToken);
Task<int> OrderCountAsync(string region, string week, CancellationToken cancellationToken);
}
WeeklyReport.cs
using Inngest;
public sealed record ReportRequested(string Week) : IInngestEventData
{
public static string EventName => "reports/weekly.requested";
}
public sealed class WeeklyReport(IReportMailer mailer) : InngestFunction<ReportRequested>
{
private static readonly string[] Regions = ["eu-west", "eu-central", "us-east", "us-west", "ap-south"];
public override async Task<object?> ExecuteAsync(FunctionContext<ReportRequested> context, CancellationToken cancellationToken)
{
var week = context.Event.Data.Week;
var results = await Task.WhenAll(Regions.Select(region => FiguresOrNullAsync(context, region, week)));
var figures = results.OfType<RegionFigures>().ToList();
var missing = Regions.Except(figures.Select(f => f.Region)).ToList();
await context.Step.RunAsync("send-report", ct => mailer.SendAsync(week, figures, missing, ct));
return new { week, regions = figures.Count, missing };
}
// A region that fails all its attempts, or does not finish within the timeout, is reported as missing
// instead of failing the whole report.
private static async Task<RegionFigures?> FiguresOrNullAsync(FunctionContext context, string region, string week)
{
try
{
return await context.Step.InvokeAsync<ComputeRegionFigures, RegionFigures>($"figures-{region}",
new RegionRequest(region, week), timeout: TimeSpan.FromMinutes(30));
}
catch (StepFailedException exception)
{
context.Logger.LogWarning("No figures for {Region}: {Error}", region, exception.Error.Message);
return null;
}
}
}
public interface IReportMailer
{
Task SendAsync(string week, IReadOnlyList<RegionFigures> figures, IReadOnlyList<string> missing, CancellationToken cancellationToken);
}

The app registers both with their options as attributes on the classes, or with the options in code:

On ComputeRegionFigures, which has no trigger attribute:

[InngestFunction("compute-region-figures", Retries = 3)]
[Concurrency(2)]

On WeeklyReport:

[InngestFunction("weekly-report")]
[EventTrigger<ReportRequested>]

AddFunction reads the attributes of both classes once at startup:

Program.cs (registration)
inngest
.AddFunction<WeeklyReport>()
.AddFunction<ComputeRegionFigures>();
  • Parallel invocations. All five InvokeAsync steps start before any is awaited, so the SDK reports them together and Inngest starts five runs of compute-region-figures. The concurrency limit lets two of them query the warehouse at once.
  • Results come back typed. ComputeRegionFigures derives from InngestFunction<RegionRequest, RegionFigures>, so InvokeAsync<ComputeRegionFigures, RegionFigures> returns a RegionFigures and the compiler checks the result type. The step finds the function’s id by its class, so the id is written once, where the function is registered or in its attribute. To invoke a function of another app, or a delegate function, pass InvokeOptions with its id instead.
  • Partial failure. An invoke step fails when the invoked run fails for good or the timeout passes. Catching StepFailedException per region turns that into a gap in the report rather than a failed report. The helper takes the base FunctionContext, which has the step tools and the logger.
  • Retries stay where the work is. The region function retries its own queries. The invoke step itself is not retried, so retries never multiply across the two levels.

Invoking functions covers the options, and Parallel steps how parallel steps execute.

inngest-dotnet is a community Inngest SDK for .NET. It is not affiliated with or endorsed by Inngest Inc.