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 (
FlowEventrecords) - 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.