Skip to content

5.5.2.1.7. Output Pin

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

Output Pin

An output pin declares one type of object token leaving an Activity scope. Root-level pins publish each arriving token as a raw notification. Pins inside structured, iterative, or parallel Activities forward tokens to the containing node's outgoing object flows instead. An output pin does not terminate the Activity.

Root-level publication

Connect a producer to OutputPin<TEvent> and register AddOutputPin<TEvent>() at the Activity root:

using Stateflows.Activities;
using Stateflows.Common;
using Stateflows.Entities.Enums;

.AddActivity("Totals", activity => activity
    .AddInputPin<decimal>(pin => pin.AddFlow("Calculate"))
    .AddAction("Calculate", context =>
    {
        context.Output(context.GetTokensOfType<decimal>().Sum());
        return Task.CompletedTask;
    }, node => node.AddFlow<decimal, OutputPin<decimal>>())
    .AddOutputPin<decimal>(pin => pin
        .SetPublishScope(PublishScope.Self)
        .SetTimeToLive(300)
        .SetRetain(true)
        .SetImmediatePublish(false)))

Root pins use Publish Event Action and OutputPinNode<TEvent> internally. That node implements IPublishEventActionNode<TEvent> and ITokenConsumer<TEvent>, and publishes every arriving token. Independent incoming flows do not implicitly join at the pin.

By default, an output pin is named OutputPin<TToken>.Name, so its typed target marker works unchanged. Supply an optional globally unique name to distinguish pins of the same type across scopes; target a custom-named pin with the string flow overload instead of OutputPin<TToken>:

.AddAction("Calculate", async c => c.Output(125m),
    node => node.AddFlow<decimal>("nested-total"))
.AddOutputPin<decimal>("nested-total")
Setting Default Meaning
SetPublishScope PublishScope.Self Publish on behalf of self, parent, owner, or a combination of these flags. None disables publication; absent parent/owner scopes are skipped.
SetTimeToLive 0 No history or subscription retries unless retained; positive values bound notification lifetime in seconds.
SetRetain false Keep only the latest retained notification of the declared type for requests and retries.
SetImmediatePublish false Publish during execution rather than waiting for the transactional call to commit. Streamed/mixed firings already publish immediately.

Publication uses the pin's declared event type. It does not synthesize separate notifications for payload subtypes, base classes, or interfaces. Notification attributes also apply. See notification retention for the complete TTL/retention policy.

Observe and request output

await using var watcher = await activity.WatchOutputAsync<decimal>(
    total => Console.WriteLine(total));

// Latest unexpired retained value; empty when retention is not enabled.
var latest = await activity.GetOutputAsync<decimal>();

// Available history plus the latest retained value.
var history = await activity.GetOutputAsync<decimal>(since);

// Named overloads expose EventHolder envelopes.
var envelopes = await activity.GetOutputAsync(
    new[] { Event<decimal>.Name }, since);

Each typed watch callback receives one raw payload, not a token batch. Register watches before sending input. Root pins do not maintain an output-token archive or contribute notification payloads to SendInputAsync response tokens. GetOutputAsync delegates to notification retrieval; its results follow notification retention and expiry, not Activity completion.

Default pins are live-only (Retain = false, TTL 0). Retained output can remain requestable after Activity finalization, subject to its TTL and notification store lifetime. A client watch is volatile and does not make output durable.

flowchart LR
    subgraph nested["Structured Activity"]
        calculate(Calculate total) -->|decimal| output["Output pin"]
    end
    output -->|decimal| parent(Record total)
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. The labeled box represents a Stateflows boundary pin, not a UML final node. Mermaid does not model pins, activity parameters, or expansion regions natively.

Nested token routing

Connect the inner producer to OutputPin<TToken> when the nested pin has its default name, or use its custom string name. Declare AddOutputPin<TToken>(name) in the nested definition, then connect the containing structured node to a parent consumer:

.AddInitial(initial => initial.AddControlFlow("CalculateTotal"))
.AddStructuredActivity("CalculateTotal", nested => nested
    .AddInitial(initial => initial.AddControlFlow("Calculate"))
    .AddAction(
        "Calculate",
        async c => c.Output(125m),
        node => node.AddFlow<decimal, OutputPin<decimal>>())
    .AddOutputPin<decimal>()
    .AddFlow<decimal>("Record"))
.AddAction("Record", async c =>
    await ledger.RecordAsync(
        c.GetTokensOfType<decimal>().Single(), c.CancellationToken))

Declare a separate output pin for each supported output type. All nested output pins are collected, including results from iterative and parallel partitions. Exported boundary tokens are drained from the node buffer, so later inputs do not export them again. A declared typed pin may remain unconnected, but a connected flow must have a destination type assignable to the pin; transformations are validated using their result type. A typed-only structured scope's outgoing flows must also match its declared output pins.

Output pins are boundary connectors, not general-purpose nodes needed for flows within one Activity level. They preserve transactional/streamed provenance and work with checkpoint recovery. They do not introduce a new completion or durability guarantee. See Input Pin, Structured Activity Node, and Data Flow.

HTTP endpoints and metadata

The Minimal APIs extension maps each root output pin to GET /stateflows/activities/{name}/{instance}/output/{tokenType}. It waits for the next raw notification by default, or watches over SSE with stream=true; it does not query stored output. SSE envelope payloads contain the raw token at payload. See Activity input and output HTTP endpoints.

Observers can inspect ICurrentNodeContext.OutputPins for a containing scope. The collection uses IPinContext with the declared TokenType, name, parent, and connected flows. Registration visitors receive OutputPinAddedAsync<TToken>. Existing node lifecycle callbacks continue to observe pin execution.

Root publishing pins execute as NodeType.PublishEventAction; nested output pins execute as NodeType.OutputPin. The publication builder callback is available only on root-level AddOutputPin<TEvent> declarations.

Clone this wiki locally