Repository navigation
Observability
Stateflows exposes four kinds of extension points for inspecting or influencing behavior execution and registration:
| Extension point | When it runs | Main purpose |
|---|---|---|
| Observer | Around an internal behavior operation | Measure, log, or otherwise observe the operation while allowing it to proceed |
| Interceptor | Around incoming event processing | Apply cross-cutting policy, short-circuit processing, or adjust its status |
| Exception handler | After behavior code throws | Inspect and acknowledge an exception; this does not recover the failed execution |
| Visitor | While a registered behavior definition is traversed | Inspect its structure and build derived functionality |
These APIs exist for State Machines, Activities, Actions, and Entities, though their hooks and convenience base classes differ by behavior type.
Each behavior family has registration methods for global hooks and hooks scoped to one behavior definition. Global hooks apply to every behavior of that family; local hooks apply only to the definition where they are registered. For example:
using Stateflows;
services.AddStateflows(stateflows => stateflows
.AddStateMachines(machines => machines
.AddObserver<AuditObserver>()
.AddStateMachine<Order>(
buildAction: machine => machine
.AddObserver<OrderObserver>()
.AddInterceptor<OrderEventInterceptor>()
.AddExceptionHandler<OrderExceptionHandler>())));The family-specific builders follow the same pattern: AddActivities, AddActions, and AddEntities accept global hooks, while AddActivity, AddAction, and AddEntity configure hooks on an individual definition. AddObserver<T>, AddInterceptor<T>, and AddExceptionHandler<T> are available where that hook type is supported. The registration APIs also accept factories; State Machine and Entity factories receive the service provider and a behavior action context, while Activity and Action factories receive the service provider.
For observers and interceptors, global hooks wrap local hooks. Within either group, the first registered hook is the outermost wrapper. Exception-handler order is not identical across the families: State Machine, Activity, and Action definitions append their local handlers before global handlers, whereas Entity processing runs global handlers before Entity-local handlers. Handlers are all awaited sequentially; a handler returning true acknowledges the exception but does not change the failed outcome.
Typed registrations create hook instances through Stateflows' model-element activator, allowing constructor dependencies to be supplied by the service provider. Use a factory when construction needs explicit logic or execution context.
An observer wraps one of the behavior's internal operations and receives a next continuation for the operation being observed. Use try/finally for measurements or cleanup that must also happen when the operation fails:
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Stateflows.StateMachines;
using Stateflows.StateMachines.Context.Interfaces;
public sealed class StateEntryTimingObserver : StateMachineObserver
{
private readonly ILogger<StateEntryTimingObserver> logger;
public StateEntryTimingObserver(ILogger<StateEntryTimingObserver> logger)
=> this.logger = logger;
public override async Task OnStateEntryAsync(
IStateActionContext context,
Func<Task> next)
{
var timer = Stopwatch.StartNew();
try
{
await next();
}
finally
{
logger.LogInformation("State entry took {Elapsed}", timer.Elapsed);
}
}
}Observers are observational hooks, not alternate implementations of the operation:
- Call and await
nextexactly once. Skipping it, invoking it more than once, or returning before it finishes fails execution. - For operations that produce a result, the result is available through the continuation, but must not be replaced. Do not alter behavior state, Activity tokens, or returned values from an observer.
- Do not catch and suppress a failure from
next. If failure-safe work is needed, put it infinallyand allow the failure to propagate.
The observer callbacks cover different internal operations in each family:
| Behavior | Observer callbacks |
|---|---|
| State Machine | Initialization and finalization; State initialization, finalization, entry, and exit; transition guards and effects |
| Activity | Initialization and finalization; node initialization, finalization, activation, and execution; flow activation, guards, and transformations |
| Action | Action initialization and finalization |
| Entity | Mutation execution; computed-field evaluation; projection evaluation |
Entity observers run when a computed field or projection is evaluated, including recalculation. They do not wrap reads of cached projection values. The Entity observer context exposes operation-specific information for inspection; it is not permission to mutate Entity state or substitute a result.
An interceptor wraps processing of an incoming event, including initialization events. Its continuation represents the rest of event processing. Unlike an observer, an interceptor is allowed not to invoke next: it can short-circuit by returning an EventStatus. It can also inspect and replace the resulting status, or catch an exception from the continuation when that is an intentional policy.
using System;
using System.Threading.Tasks;
using Stateflows.Common;
using Stateflows.StateMachines;
using Stateflows.StateMachines.Context.Interfaces;
public sealed class EventAuditInterceptor : StateMachineInterceptor
{
public override async Task<EventStatus> ProcessEventAsync<TEvent>(
IEventContext<TEvent> context,
Func<Task<EventStatus>> next)
{
var status = await next();
return status;
}
}Call and await the continuation at most once. Repeated calls are rejected, and returning while the continuation is still running is an invalid interceptor implementation. Returning without calling it is valid for a deliberate short-circuit. An interceptor that catches a downstream exception is responsible for deciding what EventStatus to return.
Exception handlers are notified when an operation in behavior code throws. They receive the relevant context and exception and return true to acknowledge it. Acknowledgement is diagnostic/handling information, not recovery: the interrupted operation still fails, no success result is synthesized, and the failed behavior execution does not commit its pending state changes. Entity execution rolls back. Exception handlers should therefore report, log, or perform other appropriate failure-side work—not assume they can resume the interrupted operation.
The hooks are specific to each behavior:
| Behavior | Exception sources exposed to handlers |
|---|---|
| State Machine | State Machine initialization/finalization; transition guards/effects; State initialization/finalization/entry/exit |
| Activity | Activity initialization/finalization; node initialization/finalization/execution; flow guards/transformations |
| Action | Action delegate execution |
| Entity | Entity initialization; mutations; computed fields; projections |
Each registered handler is awaited in sequence. A true result acknowledges the exception, but does not prevent later handlers from running. A failure inside a handler is recorded/logged by the framework and does not convert the original failed operation into success. IDE break-on-thrown settings are independent: a debugger may stop at the original throw before handlers run.
Activity exception-handler nodes are a separate modeling feature: a matching graph handler can take an alternate path and handle the Activity failure. IActivityExceptionHandler is a runtime hook; it does not recover an Activity execution. See Activity Exception Handling for graph-handler matching, scope, and examples.
Visitors inspect the registered definition, not a live execution. The registration interfaces expose VisitStateMachinesAsync, VisitActivitiesAsync, VisitActionsAsync, and VisitEntitiesAsync (as well as methods to visit an individual named/versioned definition). A visitor receives asynchronous callbacks as the framework traverses the model:
| Visitor | Definition details exposed |
|---|---|
IStateMachineVisitor |
Vertices, transitions, attached behaviors, initializers/finalizers, and accepted/custom events |
IActivityVisitor |
Nodes, control/data flows, event nodes, initializers/finalizers, and custom events |
IActionVisitor |
Action type and consumed/custom events |
IEntityVisitor |
Entity type, fields and access, initializers, mutations, and projections with publish scopes |
Use visitors to build derived functionality from a model's structure rather than instrumenting event execution. For example, the Minimal APIs extension visits registered behavior definitions to discover accepted events and generate endpoints.
Several public abstract base classes supply default implementations, so a derived class can override only the callbacks it needs:
| Family | Observer | Interceptor | Exception handler | Visitor |
|---|---|---|---|---|
| State Machine |
StateMachineObserver — calls next
|
StateMachineInterceptor — calls next
|
StateMachineExceptionHandler — returns false
|
StateMachineVisitor — completes callbacks as no-ops |
| Activity |
ActivityObserver — calls next
|
ActivityInterceptor — calls next
|
ActivityExceptionHandler — returns false
|
ActivityVisitor — completes callbacks as no-ops |
| Action |
ActionObserver — calls next
|
ActionInterceptor — calls next
|
No base class; implement IActionExceptionHandler
|
ActionVisitor — completes callbacks as no-ops |
| Entity | No base class; implement IEntityObserver
|
No base class; implement IEntityInterceptor
|
No base class; implement IEntityExceptionHandler
|
EntityVisitor — completes callbacks as no-ops |
The default observer implementations preserve the required continuation behavior; the interceptor defaults transparently pass through to event processing. The exception-handler defaults do not acknowledge exceptions. Visitor defaults do nothing. If no base class is listed, implement the interface methods directly.
The OpenTelemetry extension uses Stateflows observers and interceptors to emit traces and metrics. Visitors are used by structural integrations such as Minimal APIs; they are not runtime telemetry hooks.
Home page Support Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0. © by Mikołaj Milewski, 2026
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