Skip to content

Behavioral Groups

Mikołaj Milewski edited this page Oct 27, 2026 · 1 revision

Behavioral Groups

A State Machine can coordinate other Behaviors by associating them with its states. The embedding APIs create a relationship between the State Machine and the Activity or Action it starts. Stateflows records the immediate parent and the group owner on the embedded Behavior, making it possible to communicate with related Behaviors and share state through owned Entities.

State-lifetime Behaviors: AddDoActivity and AddDoAction

AddDoActivity and AddDoAction attach a do-Behavior to a State. It is initialized when that State is entered and finalized when the State is exited. While the State is active, the do-Behavior remains part of the active configuration and can receive events. Stateflows offers an event to the embedded Behavior first; if it does not consume the event, the event can bubble to the parent State Machine, subject to the event's no-bubbling settings.

.AddInitialState("Working", state => state
    .AddDoActivity(activity => activity
        .AddAcceptEventAction<Refresh>(_ => Task.CompletedTask)
    )
    .AddTransition<Complete>("Done")
)
.AddState("Done")

This is the right form when the Activity or Action represents ongoing work associated with a State, rather than a single entry, exit, guard, or transition operation. The Behavior instance is tied to the active state configuration; on state entry Stateflows initializes the state-associated Behavior, and on exit it finalizes it.

Submachines

AddSubmachine embeds another State Machine in a state. Like a do-Activity or do-Action, the submachine is a state-lifetime Behavior: it is initialized when the containing state is entered and finalized when that state is exited. It is not a fresh invocation for every event or transition. The submachine is a first-class member of the same behavioral group, so it can use parent/owner context APIs and Entities owned by the group.

.AddInitialState("Processing", state => state
    .AddSubmachine<PaymentWorkflow>()
    .AddTransition<PaymentCompleted>("Complete")
)
.AddState("Complete")

For each event delivered to the containing State Machine, Stateflows first offers the event to an active embedded submachine that accepts it. If the submachine does not consume it, the event can be retransmitted to the containing State Machine. NoForwarding on the event type or in its headers can suppress this automatic forwarding. This lets a parent provide transitions for events not handled by the active submachine while keeping submachine-specific transitions local to the submachine.

Submachines can themselves embed further submachines or other state-lifetime Behaviors. In a nested group, the immediate containing Behavior is the parent, while the outer root remains the owner; this is why owner-scoped Entities and owner context APIs are useful when shared data must be addressable across nesting.

Invocation-scoped Behaviors

Other Add*Activity and Add*Action methods run a child Behavior for a particular invocation, not as a state-lifetime do-Behavior. Examples include AddOnEntryActivity / AddOnEntryAction, AddOnExitActivity / AddOnExitAction, AddGuardActivity / AddGuardAction, and AddEffectActivity / AddEffectAction (with corresponding default-transition and deferral variants).

These are invocation-scoped Behaviors, not persistent do-Behaviors that stay active with the State. Entry and exit invocations use generated instance IDs; guard and effect invocations use the current event ID. For example, an entry Behavior runs when the state is entered, while an effect Behavior runs for a transition effect. They still carry parent and owner relationship metadata. Use these forms for work that belongs to that lifecycle hook or transition operation.

Form When it starts Lifetime relationship
AddDoActivity / AddDoAction State entry Associated with the active State; finalized on state exit
AddSubmachine State entry Embedded State Machine remains active with the containing State; finalized on state exit
Entry / exit / guard / effect / deferral Add*Activity and Add*Action Each relevant hook or operation Invocation-scoped Behavior, not a state-lifetime child

Parent and owner

The parent is the immediate Behavior that started the embedded Behavior. The owner is the root Behavior that owns the behavioral group. If a State Machine is itself embedded, its children have that State Machine as parent but inherit the same group owner. For a directly embedded child of a top-level State Machine, that State Machine is both parent and owner.

In an Activity node callback or an Action delegate, use TryGetParentBehaviorContext or TryGetOwnerBehaviorContext to obtain the dedicated context. These methods return false when the relationship is not available:

async context =>
{
    if (context.TryGetParentBehaviorContext(out var parent))
    {
        parent.Send(new SomeEvent());
        parent.Publish(new SomeNotification());
    }

    if (context.TryGetOwnerBehaviorContext(out var owner))
    {
        await owner.TryMutateAsync<ISharedOrder, AddUnits>(new AddUnits(1));
    }
}

Send is a one-way event send and returns no completion task. Publish publishes a notification as the selected parent or owner Behavior. The parent and owner contexts also provide subscription operations and Entity operations. Entity operations such as TryMutateAsync, TryGetProjectionAsync, TryGetFieldAsync, and TrySetFieldAsync are asynchronous and report whether they succeeded.

Choose the relationship deliberately: use parent for the immediate containing Behavior; use owner when the operation or data belongs to the whole group and must remain addressable through nested embeddings. If both identify the same Behavior, either API targets that Behavior.

Typed runtime workers can constructor-inject IParentBehaviorContext or IOwnerBehaviorContext directly when the relationship is required. Resolution fails when that relationship is absent; standalone-capable workers should use the callback's TryGet... methods instead. An embedded worker receives its own behavior/execution context, not its parent's current state or node context. See Dependency Injection.

Sharing data through Entities

Behaviors in a group do not share mutable CLR objects or execution context. Put shared durable state in an Entity owned by the group owner, then let children read its projections or apply mutation events through the owner context. The State Machine builder can register an Entity alongside its states:

using Stateflows.Common;
using Stateflows.Activities;
using Stateflows.Entities.Attributes;
using Stateflows.StateMachines;

public sealed record AddUnits(int Amount);

public interface ISharedOrder
{
    [Field]
    int Units { get; set; }

    [Mutation]
    void Add(AddUnits mutation) => Units += mutation.Amount;

    [Projection]
    int CurrentUnits => Units;
}
public sealed class Order : IStateMachine
{
    public static void Build(IStateMachineBuilder machine) => machine
        .AddEntity<ISharedOrder>()
        .AddInitialState("Working", state => state
            .AddDoActivity(activity => activity
                .AddInitial(node => node.AddControlFlow("update"))
                .AddAction("update", async context =>
                    {
                        if (context.TryGetOwnerBehaviorContext(out var owner))
                        {
                            await owner.TryMutateAsync<ISharedOrder, AddUnits>(new AddUnits(1));
                        }
                    }, node => node.AddControlFlow<FinalNode>()
                )
                .AddFinal()
            )
        )
        .AddState("Done");
}

The Entity name is derived from the template type because registration omits a custom name; each owner instance has its own Entity instance. Another Behavior in that group can address the same Entity through IOwnerBehaviorContext:

if (context.TryGetOwnerBehaviorContext(out var owner))
{
    var (success, units) =
        await owner.TryGetProjectionAsync<ISharedOrder, int>();
}

Entity operations still follow the Entity contract: mutations must be registered, fields have configured access, and projections define the read model. See Entities and Interacting with Entities for definition, access, and publication details. For constructor-injected values, [FromProjection<TTemplate>(EntityScope.Owner)] and [FromField<TTemplate>(..., EntityScope.Owner)] can resolve data from the group owner; the corresponding EntityScope.Parent targets the immediate parent.

Projection notifications can also be routed to a group's parent or owner. Configure [Projection(PublishScope = PublishScope.Owner)] when owner subscribers should receive a changed projection, or PublishScope.Parent for the immediate parent. These scopes select the behavior that publishes; they are distinct from [FromProjection(...)] constructor injection. See Entity Projections.

Overview
Installation
Runtimes
   Orleans runtime
Concepts
   Events and Structural Events
   Messaging Patterns
   Validation
   Scheduling
   Observability
Behaviors
   Behavior Values
   Dependency Injection
   Behavioral Groups
   State Machines
       Concepts
           State Identity
           In-State Event Deferrals
           Evaluation of Transitions
           Exception Handling
       Building blocks
           Regions
           States
               State
               Composite State
               Orthogonal State
               Final State
           Pseudostates
               Choice
               Junction
               Fork
               Join
               History
           Transitions
               Transition
               Default Transition
               Internal Transition
   Activities
       Concepts
           Exception Handling
           Implicit Fork and Join
           Tokens
       Building blocks
           Nodes
               Action Node
               Decision Node
               Merge Node
               Initial Node
               Final Node
               Input Node
               Output Node
               Fork Node
               Join Node
               Accept Event Action Node
               Send Event Action Node
               Publish Event Action Node
               Time Event Action Node
               Data Store Node
               Structured Activity Node
               Iterative Activity Node
               Parallel Activity Node
           Flows
               Data Flow
               Control Flow
   Actions
   Entities
       Fields
       Mutations
       Projections
Extensions
   ClearScript
   FluentValidation
   JSON definitions
   Minimal APIs
   OneOf
   OpenTelemetry
   PlantUML
   Scheduling

Clone this wiki locally