Skip to content

5.5.2.1.1. Action Node

Mikołaj Milewski edited this page Nov 12, 2026 · 1 revision

Action Node

An Action Node performs application work when its incoming flows make it eligible to execute. It is the main executable node in a UML Activity. Incoming object flows provide typed input tokens; the action can produce output tokens and outgoing flows route execution onward.

Define an action

The lambda overload receives an action context and an optional node builder for outgoing flows:

.AddInputPin<Request>(pin => pin.AddFlow("Validate"))
.AddAction(
    "Validate",
    async c =>
    {
        var request = c.GetTokensOfType<Request>().Single();
        var validated = await validator.ValidateAsync(request, c.CancellationToken);
        c.Output(validated);
    },
    node => node.AddFlow<ValidatedRequest>("Persist"))
.AddAction(
    "Persist",
    async c => await repository.SaveAsync(
        c.GetTokensOfType<ValidatedRequest>().Single(),
        c.CancellationToken),
    node => node.AddControlFlow<FinalNode>())
.AddFinal()

This fragment receives Request tokens through SendInputAsync. Validate is activated by that input flow; no additional initial control flow is needed for this input-driven path.

Output and OutputRange add object tokens for outgoing flows. GetTokensOfType<T>() reads the input tokens routed to this action. An action can have multiple outgoing control and object flows; use explicit Fork, Decision, or Merge nodes when those routing semantics should be visible in the graph.

Streaming and reentrant actions

On Orleans, streamed input can reach the same network while an action is awaiting work. Firings of one node in the same execution thread scope are serialized by default; independent nodes and nested partitions can still run concurrently.

Use .Reentrant() only for an action implementation that is safe for bounded overlapping firings:

.AddAction(
    "Process",
    async context =>
    {
        foreach (var item in context.GetTokensOfType<WorkItem>())
            await processor.ProcessAsync(item, context.CancellationToken);
    },
    node => node.Reentrant())

This sets NodeOptions.Reentrant; it does not make regular transactions overlap and is not supported for structural Activity nodes. Every invocation owns an input snapshot, trigger, cancellation context, and DI scope. Shared services and mutable token payloads remain the application's concurrency responsibility.

A firing consuming any streamed token is streamed, including when the streamed token is control-only. Its outputs keep that provenance. Pure transactional firings can hand off mixed successors and let the regular caller return before those descendants finish.

Both colors can access live Behavior values while streaming. Pass context.CancellationToken to asynchronous operations; immediate finalization acknowledges cancellation before non-cooperative code necessarily drains. Activity Checkpoints wait for action bodies to reach their firing boundaries rather than serializing their running tasks.

Class-based actions

For reusable or dependency-injected logic, define an IActionNode. Constructor parameters can include application services and token interfaces such as IInputTokens<T> and IOutputTokens<T>. The typed class implements ExecuteAsync(CancellationToken); it does not receive a lambda action context parameter.

See Tokens, Flows, and Nodes.

Clone this wiki locally