Skip to content

5.5.2.1.15. Structured Activity Node

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

Structured Activity Node

A Structured Activity Node contains a nested Activity graph. It groups steps behind one node in the containing Activity and gives the nested graph its own Initial node and typed input/output pins. This is the UML structured activity node pattern; it does not create a separately addressed Activity behavior instance.

flowchart LR
    outer(Parent Activity)
    outer --> input["Input pin"]
    subgraph nested["Structured Activity"]
        input --> normalize(Normalize)
        normalize --> output["Output pin"]
    end
    output --> next(Parent continuation)
Loading

Mermaid approximation: this diagram shows the model's routing, not a complete UML Activity notation. Rounded boxes represent actions; edge labels identify token types or guards. Labeled boxes represent Stateflows boundary pins, not UML activity-parameter notation. Mermaid does not model pins, activity parameters, or expansion regions natively.

Passing data through the boundary

The parent sends tokens to the structured node through its incoming object flow. Inside the node, AddInputPin<TToken> declares supported input types and routes each type to nested nodes. To return a value, connect a nested producer to OutputPin<TToken>, declare AddOutputPin<TToken>(), and connect the structured node's outgoing object flow to a parent node.

.AddInitial(initial => initial.AddControlFlow("ReadAmount"))
.AddAction(
    "ReadAmount",
    async c => c.Output(125m),
    node => node.AddFlow<decimal>("Normalize"))
.AddStructuredActivity("Normalize", nested => nested
    .AddInputPin<decimal>(pin => pin.AddFlow("Round"))
    .AddAction(
        "Round",
        async c => c.Output(decimal.Round(
            c.GetTokensOfType<decimal>().Single(), 2)),
        node => node.AddFlow<decimal, OutputPin<decimal>>())
    .AddOutputPin<decimal>()
    .AddFlow<decimal>("RecordAmount"))
.AddAction("RecordAmount", async c =>
    await ledger.RecordAsync(
        c.GetTokensOfType<decimal>().Single(), c.CancellationToken))

The nested definition also supports actions, control nodes, event actions, and further structured nodes. Its configuration and active nodes are scoped under the containing node. A structured node can remain active across event processing; use Behavioral Groups when referring to state-machine-embedded behaviors, which are separate behavior instances rather than nested Activity nodes.

Declare multiple input/output types with separate pins. The containing object flows are validated against these boundary contracts, and execution collects all output pins. ICurrentNodeContext.InputPins and .OutputPins describe this nested scope. These APIs are available on normal, reactive, and inherited/override builders, including iterative and parallel boundaries.

Interleaving arrivals

Call parameterless .Interleave() on the structured builder to feed later arrivals into an already active invocation:

.AddStructuredActivity("Normalize", nested => nested
    .Interleave()
    .AddOnInitialize(async context => await PrepareAsync(context.CancellationToken))
    .AddInputPin<decimal>(pin => pin.AddFlow("Round"))
    .AddAction("Round", async context =>
        context.Output(decimal.Round(context.GetTokensOfType<decimal>().Single(), 2)),
        action => action.AddFlow<decimal, OutputPin<decimal>>())
    .AddOutputPin<decimal>())

OnInitialize and Initial-node control execute only once per invocation. Ordinary structured lifecycle and event waits still apply: interleaving does not make a finished scope permanently idle. Admission is atomic with drain. Arrivals after finalization starts wait until OnFinalize and completion-output routing finish, then start a fresh invocation. Inherited and reactive structured builders also expose this method. NodeOptions.Interleave is the underlying flag; combining it with Reentrant is a registration error. Existing action-only reentrancy is unchanged; actions do not expose .Interleave().

The invocation is identified by its enclosing execution scope, not only its globally unique node name. Checkpoint recovery and mixed streaming/transactional handoffs preserve lifecycle progress, queued input and token provenance rather than initializing again. Cancellation fences late framework-managed routing; application side effects still require application-level cancellation handling.

Parallel/iterative interleaving creates additional partitions rather than feeding new selection tokens into existing partitions; see the linked node pages. Interleaving admits new input while an invocation is active; it does not keep an invocation open after its work drains or publish boundary output early.

Direct crossing flows

Ordinary structured boundaries also support direct incoming and outgoing flows. All Activity node names are globally unique. Ordinary node names supplied to node builders are used as-is; Initial and input boundaries use scope-qualified names, while output pins and Final nodes have configurable names. Use the existing AddFlow<TToken>("name") or AddControlFlow("name") with the destination's registered name; a target outside the source's containing scope is automatically routed across the boundary. This also applies to weighted and else flows.

An offer to an internal action does not start the structure. It remains pending until the containing structure, and all nested destination scopes, have been activated normally. Offers can also arrive while a structure executes or is idle but still active. Sibling routing observes the destination sibling's activation. Incoming crossings into reentrant structured scopes are rejected; one eligible invocation per enclosing execution scope is required.

Outgoing crossings route immediately in the destination's enclosing invocation. They do not inherit cancellation or partition selection from the emitting inner action. Tokens retain their streamed provenance, and checkpoint-ready work is owned by the destination continuation. Inner termination does not cancel work already accepted outside. Output pins still collect aggregate boundary output; direct crossings do not add interleaving or change that aggregation. The source structure can finish independently of an escaped continuation; the destination scope drains that work and observes its failures.

Parallel and iterative boundaries have stricter crossing rules; see Flows.

See Input Pin, Output Pin, and Parallel Activity Node.

Clone this wiki locally