Skip to content

5.5.2.1.16. Iterative Activity Node

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

Iterative Activity Node

An Iterative Activity Node executes a nested Activity repeatedly over partitions of its incoming tokens. AddIterativeActivity<TToken>(..., chunkSize) selects tokens of type TToken, groups them into chunks, and runs the nested graph once per chunk. Other token types are also available inside each nested run.

flowchart LR
    source(Load records) -->|Record tokens| iterate{{Iterative Activity}}
    subgraph body["Nested Activity, once per chunk"]
        input["Input pin"] --> total(Calculate batch total)
        total --> output["Output pin"]
    end
    iterate -->|Batch totals| report(Report totals)
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.

Definition

.AddInitial(initial => initial.AddControlFlow("LoadRecords"))
.AddAction(
    "LoadRecords",
    async c => c.OutputRange(await records.LoadAsync(c.CancellationToken)),
    node => node.AddFlow<Record>("Batch"))
.AddIterativeActivity<Record>("Batch", nested => nested
    .AddInputPin<Record>(pin => pin.AddFlow("CalculateBatch"))
    .AddAction(
        "CalculateBatch",
        async c =>
        {
            var batchTotal = c.GetTokensOfType<Record>().Sum(record => record.Amount);
            c.Output(batchTotal);
        },
        node => node.AddFlow<decimal, OutputPin<decimal>>())
    .AddOutputPin<decimal>()
    .AddFlow<decimal>("ReportBatch"),
    chunkSize: 25)
.AddAction("ReportBatch", async c =>
    await reports.AddBatchTotalsAsync(
        c.GetTokensOfType<decimal>(), c.CancellationToken))

Output pins declared with AddOutputPin<TToken>() collect aggregate boundary output from the nested executions. Without an output declaration the nested computation can still perform side effects or use the explicit outgoing crossings below, but contributes no aggregate boundary tokens. A FinalNode inside the nested graph ends the iteration early; otherwise all chunks are processed. Iterations run sequentially, unlike the Parallel Activity Node, which runs chunks concurrently. Choose a chunk size appropriate to the amount of data each nested execution should process.

Interleaving arrivals

.AddIterativeActivity<Record>("Batch", nested => nested
    .Interleave()
    .AddInputPin<Record>(pin => pin.AddFlow("CalculateBatch"))
    .AddAction("CalculateBatch", async context =>
        context.Output(context.GetTokensOfType<Record>().Sum(record => record.Amount)),
        action => action.AddFlow<decimal, OutputPin<decimal>>())
    .AddOutputPin<decimal>(),
    chunkSize: 25)

Parameterless .Interleave() sets NodeOptions.Interleave: initialize once and append chunks from later arrivals to the same active invocation's sequential queue. At most one partition executes at a time. Chunk each arrival separately; its final partial chunk is included immediately, never combined with a later arrival. Non-selection/common input is supplied only from that same arrival, not inherited from earlier input; existing partitions keep their inputs.

Completed partitions and their disposed scopes are released while the invocation remains active. The shared iterative execution identity survives cleanup and checkpoints. Unfinished partitions remain recoverable; aggregate boundary output is retained until completion. Neither queued input nor buffered output has a capacity limit.

When no executable/running work remains, finalize once, aggregate ordinary boundary output/control and discard stranded join/weight tokens. An existing Final node retains its early-termination behavior. No explicit close is required and an empty invocation does not stay initialized for future arrivals.

Admission and drain are atomic. Arrivals after finalization starts wait for OnFinalize and completion-output routing, then start a fresh invocation. Checkpoint recovery and mixed streaming handoffs retain queue order, partition progress and per-arrival token provenance without repeating initialization. Nested partition scopes do not share child invocations. Existing event-wait restrictions, cancellation and failures remain unchanged; .Interleave() and .Reentrant() together fail registration.

Interleaving does not keep an idle invocation open or publish boundary output early. JSON supports "interleave" in existing options; PlantUML annotates it <<interleave>>. For a configurable active-partition limit use a parallel activity's maxConcurrency; iterative execution is always sequential.

Outgoing crossings

An internal producer can call AddFlow<Result>("record") or AddControlFlow("notify") using the destination's globally unique registered name. The target must belong directly to the IterativeActivity's immediately enclosing scope. Grandparent and sibling-interior destinations are invalid.

Incoming crossings across an IterativeActivity boundary, including routes to deeper descendants, are rejected. This restriction does not prohibit local iteration flows or ordinary structured crossings entirely within one iteration.

Outgoing work belongs to the enclosing invocation, not the current iteration's marking, cancellation, or selection. Offered tokens retain streamed provenance; checkpoint continuation uses destination ownership. Inner termination does not cancel work already accepted outside. Output pins still aggregate partition results before the container's outgoing flows run. Direct crossings neither change aggregation nor introduce interleaving.

See Flows for crossing-flow activation and ownership rules.

See also Structured Activity Node, Tokens, and Output Pin.

Clone this wiki locally