Skip to content

Invoking functions from other functions

InvokeAsync runs another function and waits for its output, as a step. The other function runs as a run of its own, with its own retries, flow control and history, and its output comes back as the step’s result.

var score = await context.Step.InvokeAsync<ScoreAccount, AccountScore>("score-acme",
new ScoreRequest("acme"), timeout: TimeSpan.FromMinutes(5));
context.Logger.LogInformation("acme scored {Score}", score.Score);

ScoreAccount is the class of the function to run. The step looks up the id the app registered it under, from options in code or from its attributes, and the compiler checks that ScoreAccount returns an AccountScore.

Any function can be invoked. Give it a result type with InngestFunction<TEventData, TResult>, and one that should only ever be invoked has no trigger:

ScoreAccount.cs
using Inngest;
public sealed record ScoreRequest(string AccountId);
public sealed record AccountScore(string AccountId, int Score);
public sealed class ScoreAccount(IScoring scoring) : InngestFunction<ScoreRequest, AccountScore>
{
public override async Task<AccountScore> ExecuteAsync(FunctionContext<ScoreRequest> context, CancellationToken cancellationToken)
{
var accountId = context.Event.Data.AccountId;
var score = await context.Step.RunAsync("compute-score", ct => scoring.ScoreAsync(accountId, ct));
return new AccountScore(accountId, score);
}
}
public interface IScoring
{
Task<int> ScoreAsync(string accountId, CancellationToken cancellationToken);
}
[InngestFunction("score-account")]

The attribute goes on ScoreAccount, and inngest.AddFunction<ScoreAccount>() registers it.

Inngest starts the invoked run with an inngest/function.invoked event whose data is the data you passed, so the invoked function reads it as its event data. Its return value is what InvokeAsync returns.

A class that derives from InngestFunction<TEventData> returns object?, so a caller can only read its result as object. Give it a result type, or invoke it by its id.

To invoke a function of another app, a delegate, or a function whose class the caller cannot reference, name it by the id it was registered with. The type argument is then only the type to read its output as:

var score = await context.Step.InvokeAsync<AccountScore>("score-acme",
new InvokeOptions("score-account") { Data = new { accountId = "acme" }, Timeout = TimeSpan.FromMinutes(5) });
Option Meaning
data The invoked run’s event data. Serialized with the app’s serializer options. In InvokeOptions, Data.
timeout How long to wait for the invoked function. When it passes, the step fails; the invoked run keeps going. In InvokeOptions, Timeout.
FunctionId InvokeOptions only: the id of the function to run, as registered.
AppId InvokeOptions only: the app the function belongs to. Defaults to the current app, so set it only to invoke a function of another app in the same Inngest environment.
User InvokeOptions only: optional user data for the invoked run’s event.

Without a timeout Inngest waits a long time; see step.invoke in the Inngest docs for its default.

Invoke one function per item and await them together. Each invocation is a separate run, so each item gets its own retries, and the invoked function’s concurrency limit caps how many run at once:

string[] accountIds = ["acme", "globex", "initech"];
var scores = await Task.WhenAll(accountIds.Select(accountId =>
context.Step.InvokeAsync<ScoreAccount, AccountScore>($"score-{accountId}",
new ScoreRequest(accountId), timeout: TimeSpan.FromMinutes(5))));
await context.Step.SendEventAsync("announce-scores",
new InngestEvent("reports/scores.ready", new { total = scores.Sum(score => score.Score) }));

The fan-out example builds on this. For a long list, MapInChunksAsync invokes one function per chunk of items and gathers the results in order.

If the invoked function fails all its attempts, or the timeout passes first, the step fails at once without retries of its own, and InvokeAsync throws StepFailedException. Retrying the step would run the whole invoked function again, and in a chain of invocations the retries would multiply. Catch the exception to carry on without that result:

AccountScore? score;
try
{
score = await context.Step.InvokeAsync<ScoreAccount, AccountScore>("score-acme",
new ScoreRequest("acme"), timeout: TimeSpan.FromMinutes(5));
}
catch (StepFailedException exception)
{
context.Logger.LogWarning("Scoring failed: {Error}", exception.Error.Message);
score = null;
}

Left uncaught, StepFailedException fails the run without retrying it. If the invoked function’s rate limit skips the run, the step fails in the same way.

An invoked function does not receive the user who caused the calling run by itself, because invocations do not pass through middleware. With Inngest.UserContext, WithUser adds the run’s user to the options.

Invoke when the caller needs the result or must know that the work finished. Send an event with SendEventAsync when the caller should not wait, or when any number of functions may react to what happened.

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