Repository navigation
5.6. Actions
An Action is a Stateflows Behavior for a discrete operation that does not need a state-machine lifecycle or an Activity graph. It can receive input tokens, execute code, and return output tokens. An Action is addressable by its behavior name and instance ID and participates in the shared Behavior messaging and persistence model.
Actions are not UML models. Use a State Machine for event-driven lifecycle logic and an Activity when work is best represented as a graph of token-processing nodes.
The lambda form receives an IActionContext, which exposes the Action identity, input tokens, output-token methods, and cancellation token:
using System.Linq;
using Stateflows;
using Stateflows.Common;
using Stateflows.Actions;
builder.Services.AddStateflows(stateflows => stateflows
.AddActions(actions => actions
.AddAction("CalculateTotal", async context =>
{
var prices = context.GetTokensOfType<decimal>();
context.Output(prices.Sum());
})));For reusable logic or constructor-injected dependencies, define a class that implements IAction and register it by type:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Stateflows.Common;
using Stateflows.Actions;
public sealed class DatabaseCleanup : IAction
{
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
// Perform the cleanup operation.
await Task.CompletedTask;
}
}Register the type in the application's startup code:
builder.Services.AddStateflows(stateflows => stateflows
.AddActions(actions => actions
.AddAction<DatabaseCleanup>("DatabaseCleanup")));IAction.ExecuteAsync receives a cancellation token. When input and output tokens are needed, use the lambda context or the Action's input/output token interfaces as appropriate. Stateflows can also register observers, interceptors, exception handlers, and visitors for Actions; see Observability.
Action definitions can be versioned. Registering a new definition version does not change already-running Behavior instances; see Definition reuse and versions.
Typed Actions can inject Stateflows.Actions.IActionContext, Stateflows.Common.IBehaviorContext, Stateflows.Common.IExecutionContext, and available parent/owner contexts alongside application services. They can also inject IInputTokens<T>, IOutputTokens<T>, and the other token-interface families from Stateflows.Activities. An Action is not an Activity node and cannot inject its parent's state/node context. See Dependency Injection for an example and execution-lifetime restrictions.
Locate an Action through IActionLocator. The located IActionBehavior extends IBehavior and IInputOutput with token-based execution:
| Method | Result | Purpose |
|---|---|---|
ExecuteAsync() |
Task<RequestResult<TokensOutput>> |
Execute with an empty input-token batch. |
SendInputAsync(tokensAction, headers) |
Task<RequestResult<TokensOutput>> |
Build an input batch containing one or more token types, with optional headers. |
SendInputAsync<TToken>(params TToken[] tokens) |
Task<RequestResult<TokensOutput>> |
Execute with a batch of one token type. |
WatchOutputAsync(Action<ITokensOutput>) |
Task<IWatcher> |
Watch newly published output batches of all token types. |
WatchOutputAsync<TToken>(Action<IEnumerable<TToken>>) |
Task<IWatcher> |
Watch newly published output batches for one token type. |
SendInputAsync submits tokens to the Action and returns its output-token response. The builder's Add<TToken> and AddRange<TToken> methods can combine different token types. The Action reads them through its input-token interfaces or lambda context.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Stateflows;
using Stateflows.Common;
using Stateflows.Actions;
public sealed class CalculationService(IActionLocator locator)
{
public async Task<decimal> CalculateAsync(
string instanceId,
IEnumerable<decimal> prices)
{
if (!locator.TryLocateAction(
new ActionId("CalculateTotal", instanceId),
out var action))
{
throw new InvalidOperationException(
$"Action instance '{instanceId}' could not be located.");
}
var result = await action.SendInputAsync(input =>
{
foreach (var price in prices)
{
input.Add(price);
}
});
if (result.Status != EventStatus.Consumed || result.Response is null)
{
throw new InvalidOperationException("The calculation failed.");
}
return result.Response.GetOfType<decimal>().Single();
}
}The builder overload accepts an optional header dictionary. The generic params overload does not accept headers.
For a batch of one type, the generic overload is equivalent to adding each token through the builder:
var result = await action.SendInputAsync<decimal>(10m, 25m);Both overloads expose the batch contents as individual Action input tokens and return a TokensOutput response.
For an operation such as DatabaseCleanup, use ExecuteAsync():
if (!locator.TryLocateAction(
new ActionId("DatabaseCleanup", "cleanup-42"), out var cleanup))
{
throw new InvalidOperationException("Cleanup Action could not be located.");
}
var result = await cleanup.ExecuteAsync();
if (result.Status != EventStatus.Consumed || result.Response is null)
{
throw new InvalidOperationException(
$"Cleanup did not return its execution result: {result.Status}.");
}This convenience method requests execution with an empty TokensInput. The response is still TokensOutput, even when the Action produces no tokens. Read concrete output responses with GetOfType<TToken>(); an empty result is not itself an execution failure.
Both WatchOutputAsync overloads come from IInputOutput. The untyped callback receives ITokensOutput, read with GetAllOfType<TToken>(); the typed callback receives IEnumerable<TToken>.
Actions automatically publish nonempty output batches after successful execution, in addition to returning output in token-input request responses. This applies to ExecuteAsync, both SendInputAsync overloads, and execution triggered by ordinary events or initialization.
await using var watcher = await action.WatchOutputAsync<decimal>(
totals =>
{
foreach (var total in totals)
{
Console.WriteLine(total);
}
});
var result = await action.SendInputAsync<decimal>(10m, 25m);Register watches before triggering execution. Each batch is published as TokensOutput and as typed TokensOutput<TToken> notifications for its runtime token types and applicable base classes and interfaces. Typed watches receive only matching tokens, while the untyped watch receives the complete batch. No output notification is generated for an empty batch or when the Action throws.
The returned IWatcher is a volatile client subscription. Keep the handle and watcher alive while watching, and dispose the watcher when finished; it is not a durable behavior-to-behavior subscription.
Actions do not add an Action-specific status information type, status query/watch overload, or GetOutputAsync method. They inherit the general GetStatusAsync and WatchStatusAsync operations for BehaviorInfo; do not interpret these as progress reporting for an in-flight Action.
Action handles also support SendAsync, RequestAsync, general notification watches, ResetAsync, and FinalizeAsync. Ordinary application events are made available to the Action as input tokens; token-batch execution uses the dedicated input contract described above. See Messaging Patterns for shared messaging semantics and Observability for execution hooks.
Home page Support Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0. © by Mikołaj Milewski, 2026