Table of Contents

Core Concepts

Flow

A flow is a named, versioned workflow definition. It is the top-level unit that FlowOrchestrator manages, schedules, and monitors.

IFlowDefinition

public interface IFlowDefinition
{
    Guid Id { get; }
    string Version { get; }
    FlowManifest Manifest { get; set; }
}
Property Purpose
Id Stable identifier used as the database primary key and job routing key across all runtime adapters. Never change after deployment.
Version Display label shown in the dashboard. Increment it when the manifest structure changes significantly.
Manifest Declares all triggers and steps for this flow.

Why the ID must be stable

The Id is persisted in FlowDefinitions and referenced by FlowRuns and FlowSteps. Changing it after flows have run creates orphaned records and breaks the run history view. Use a hardcoded new Guid("...") literal, not Guid.NewGuid().


FlowManifest

public sealed class FlowManifest
{
    public FlowTriggerCollection Triggers { get; set; }
    public StepCollection Steps { get; set; }
}

Triggers and Steps are both string-keyed dictionaries. The key is the logical name used in runAfter references and in run history.


TriggerMetadata

Each entry in Manifest.Triggers declares one trigger:

new TriggerMetadata
{
    Type = TriggerType.Cron,
    Inputs = new Dictionary<string, object?> { ["cronExpression"] = "0 9 * * 1-5" }
}
Type Required Inputs How it fires
TriggerType.Manual Dashboard button or POST /flows/api/flows/{id}/trigger
TriggerType.Cron cronExpression Recurring job registered with the active runtime adapter on the given cron schedule
TriggerType.Webhook webhookSlug, optionally webhookSecret POST /flows/api/webhook/{webhookSlug}

A flow can declare multiple triggers in the same manifest. Any of them can start a run independently.


StepMetadata

Each entry in Manifest.Steps declares one step:

new StepMetadata
{
    Type = "LogMessage",              // resolves to a registered IStepHandler
    RunAfter = new RunAfterCollection
    {
        ["previous_step"] = [StepStatus.Succeeded]
    },
    Inputs = new Dictionary<string, object?>
    {
        ["message"] = "@triggerBody()?.orderId"  // expression resolved at runtime
    }
}
Field Purpose
Type Name used to look up the IStepHandler registered with AddStepHandler<T>("Name"). Built-in step types include ForEach (see ForEach Loops) and WaitForSignal (see WaitForSignal).
RunAfter Zero or more step keys → required statuses, optionally combined with a When boolean expression. Empty = entry step (runs immediately after trigger). See Conditional Execution for the full syntax.
Inputs Key-value pairs passed to the handler. Values may be literals or @ expressions — see Expressions.

LoopStepMetadata

For ForEach loops, use LoopStepMetadata (extends StepMetadata):

new LoopStepMetadata
{
    Type = "ForEach",
    ForEach = "@triggerBody()?.orderIds",  // collection source
    ConcurrencyLimit = 2,                  // max parallel iterations
    Steps = new StepCollection             // child steps per iteration
    {
        ["validate_item"] = new StepMetadata { Type = "ValidateOrder" }
    }
}

See ForEach Loops for the full reference.


RunAfter Semantics

RunAfter controls when a step becomes ready:

// Run after step_a AND step_b both succeed
RunAfter = new RunAfterCollection
{
    ["step_a"] = [StepStatus.Succeeded],
    ["step_b"] = [StepStatus.Succeeded]
}

// Run after step_a succeeds OR fails (any terminal status)
RunAfter = new RunAfterCollection
{
    ["step_a"] = [StepStatus.Succeeded, StepStatus.Failed]
}

// Entry step — runs immediately when the flow is triggered
RunAfter = null  // or omit entirely

When multiple predecessors are listed, all must reach one of their declared statuses before the step becomes ready (AND-join semantics). This enables fan-in after parallel branches.

RunAfterCondition also supports a When boolean expression evaluated against trigger payload and upstream step outputs. A step whose When evaluates to false is recorded as Skipped and the dashboard surfaces the evaluation trace under the Why skipped panel. See Conditional Execution.


Step Statuses

Status Meaning
Pending Waiting for predecessors, or polling (reschedule pending)
Running The runtime adapter is executing this step
Succeeded Handler returned without error
Failed Handler threw an exception, or returned StepStatus.Failed
Skipped The step did not run. Either every path to it ended in statuses not listed in its RunAfter, or its When condition evaluated to false (see Conditional Execution). The dashboard displays this as Blocked.

Run Statuses

Status Meaning
Running At least one step is still executing or pending
Succeeded All steps reached a terminal status; no failures blocking downstream
Failed One or more steps failed and no downstream path could complete
Cancelled Cooperative cancellation was requested and the in-flight steps acknowledged it
TimedOut The run exceeded RunControl.DefaultRunTimeout

RunId

A RunId is a Guid generated by FlowOrchestratorEngine.TriggerAsync for every new run. It scopes everything:

  • Trigger headers and body
  • Step statuses and attempt records
  • Step inputs and outputs (IOutputsRepository)
  • Run events (FlowEvent records)
  • Control state (timeout, cancellation, idempotency)

Downstream steps access it via IExecutionContext.RunId.


IExecutionContext

The execution context is available in every IStepHandler.ExecuteAsync call:

public interface IExecutionContext
{
    Guid RunId { get; }
    string FlowId { get; }
    string TriggerKey { get; }
    ClaimsPrincipal? Principal { get; }
    CancellationToken CancellationToken { get; }
}

It is also injectable from DI via IExecutionContextAccessor.Current for services called indirectly from a step.