Configuration Reference
Complete reference for all options available on FlowOrchestratorBuilder and FlowDashboardOptions.
FlowOrchestratorBuilder
Storage
| Method | Description |
|---|---|
options.UseSqlServer(string connStr) |
Configure SQL Server persistence (Dapper) |
options.UsePostgreSql(string connStr) |
Configure PostgreSQL persistence (Npgsql) |
options.UseInMemory() |
Configure in-process storage (no persistence) |
Exactly one of these must be called. Omitting all three throws InvalidOperationException on startup.
Flows
options.AddFlow<TFlow>()
Registers a flow class. TFlow must implement IFlowDefinition and have a public parameterless constructor. Call once per flow.
Runtime Adapter
Choose exactly one runtime adapter and register it before calling AddFlowOrchestrator():
Hangfire runtime (SQL Server or PostgreSQL persistence, distributed workers):
builder.Services.AddHangfire(...);
builder.Services.AddHangfireServer();
builder.Services.AddFlowOrchestrator(options =>
{
options.UseSqlServer(connStr); // or UsePostgreSql(...)
options.UseHangfire(); // wires HangfireStepDispatcher
options.AddFlow<MyFlow>();
});
InMemory runtime (no Hangfire packages required — Channel<T> step dispatcher + PeriodicTimer cron):
builder.Services.AddFlowOrchestrator(options =>
{
options.UseInMemory(); // in-process storage
options.UseInMemoryRuntime(); // Channel<T> dispatcher + PeriodicTimer cron
options.AddFlow<MyFlow>(); // do NOT call UseHangfire() here
});
Note
When using the InMemory runtime, Hangfire packages (Hangfire.Core, Hangfire.InMemory, etc.) are not needed and should not be added. Cron parsing is handled by Cronos.
Tip
Storage and runtime are independent. UseInMemoryRuntime() works equally with UseSqlServer() or UsePostgreSql() if you want in-process step execution but durable run state.
Azure Service Bus runtime (v1.22+ — cloud-native, multi-replica, multi-region):
builder.Services.AddFlowOrchestrator(options =>
{
options.UseSqlServer(connStr); // (or UsePostgreSql / UseInMemory)
options.UseAzureServiceBusRuntime(sb =>
{
sb.ConnectionString = builder.Configuration.GetConnectionString("ServiceBus")!;
sb.AutoCreateTopology = true; // creates topic + sub-per-flow at startup
// sb.StepTopicName = "flow-steps"; // defaults shown
// sb.CronQueueName = "flow-cron-triggers";
// sb.SubscriptionPrefix = "flow-";
// sb.MaxConcurrentCallsPerSubscription = 8;
});
options.AddFlow<MyFlow>();
});
The Service Bus runtime publishes step messages to a shared topic (flow-steps) with one
SQL-filtered subscription per registered flow (filter: FlowId = '{flowId}'), and runs cron
triggers as self-perpetuating scheduled messages on a dedicated queue (flow-cron-triggers).
The engine's Dispatch many, Execute once invariant (dispatch ledger + claim guard) makes
the at-least-once delivery model of Service Bus correct.
AutoCreateTopology = true (default) creates topic / queue / subscriptions via
ServiceBusAdministrationClient at startup — requires Manage rights on the connection
string. Set to false when topology is provisioned via IaC (Bicep / Terraform) and the
deploy identity has only Send / Listen rights.
For local development, the included Aspire AppHost wires the official Microsoft Service Bus
emulator via AddAzureServiceBus("servicebus").RunAsEmulator() — see
FlowOrchestrator.AppHost/Program.cs
for the wiring; run dotnet run --project ./FlowOrchestrator.AppHost and the
flow-servicebus instance comes up on port 5104. Note: Aspire 13.2's emulator integration
cannot yet declare SQL filters on subscriptions (dotnet/aspire#11708),
so dev-mode topic broadcast is mitigated by an in-process dedup map in the SB processor —
production with AutoCreateTopology = true uses real filtered subscriptions and has no
broadcast overhead.
FlowSchedulerOptions
options.Scheduler.PersistOverrides = true;
| Property | Type | Default | Description |
|---|---|---|---|
PersistOverrides |
bool |
false |
Persist cron overrides written via dashboard or API to IFlowScheduleStateStore. When true, overrides survive process restarts; when false, the manifest cron expression is restored on each restart. |
FlowRunControlOptions
options.RunControl.DefaultRunTimeout = TimeSpan.FromMinutes(10);
options.RunControl.IdempotencyHeaderName = "Idempotency-Key";
| Property | Type | Default | Description |
|---|---|---|---|
DefaultRunTimeout |
TimeSpan? |
null |
Global timeout for all runs. Runs exceeding this are marked TimedOut. Set per-flow via IFlowRunControlStore.SetTimeoutAsync for finer control. null = no timeout. |
IdempotencyHeaderName |
string |
"Idempotency-Key" |
Header name checked on trigger requests. If present, the value is used as a deduplication key — a second trigger with the same key returns the existing run instead of creating a new one. |
FlowObservabilityOptions
options.Observability.EnableEventPersistence = true;
options.Observability.EnableOpenTelemetry = true;
| Property | Type | Default | Description |
|---|---|---|---|
EnableEventPersistence |
bool |
false |
Write FlowEvent records to IOutputsRepository (requires the storage backend to implement IFlowEventReader). Powers the step timeline in the dashboard. |
EnableOpenTelemetry |
bool |
false |
Register FlowOrchestratorTelemetry ActivitySource and Meter. Use AddFlowOrchestratorInstrumentation() on your OTel pipeline to consume them. |
FlowRetentionOptions
options.Retention.Enabled = true;
options.Retention.DataTtl = TimeSpan.FromDays(30);
options.Retention.SweepInterval = TimeSpan.FromHours(1);
| Property | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
false |
Start FlowRetentionHostedService. Disabled by default; opt in explicitly for production. |
DataTtl |
TimeSpan |
30 days | Age threshold — runs older than this are deleted on each sweep. |
SweepInterval |
TimeSpan |
1 hour | Interval between sweep passes. |
FlowDashboardOptions
builder.Services.AddFlowDashboard(options =>
{
options.Title = "My App Workflows";
options.Subtitle = "Production";
options.LogoUrl = "/logo.svg";
options.BasicAuth.Enabled = true;
options.BasicAuth.Username = "admin";
options.BasicAuth.Password = "changeme";
});
Or via appsettings.json:
{
"FlowDashboard": {
"Title": "My App Workflows",
"Subtitle": "Production",
"LogoUrl": "/logo.svg",
"BasicAuth": {
"Enabled": true,
"Username": "admin",
"Password": "changeme"
}
}
}
| Property | Type | Default | Description |
|---|---|---|---|
Title |
string |
"FlowOrchestrator" |
Displayed in the browser tab and navigation bar |
Subtitle |
string? |
— | Small label next to the title (environment, version, etc.) |
LogoUrl |
string? |
— | URL of a custom logo image shown in the navbar |
BasicAuth.Enabled |
bool |
false |
Protect all dashboard routes with HTTP Basic Auth |
BasicAuth.Username |
string? |
— | Required when BasicAuth.Enabled = true |
BasicAuth.Password |
string? |
— | Required when BasicAuth.Enabled = true |
WebhookSecurity |
WebhookSecurityOptions |
(Off) | Enterprise webhook hardening pipeline. Configure via UseWebhookSecurity(b => …) — see below. |
WebhookSecurityOptions
Opt-in HMAC signature, replay, rate-limit, IP allow/deny, body cap, and DLQ
gates for the dashboard webhook receive endpoint. The default
EnforcementMode = Off keeps the endpoint behaving exactly as in v1.24.
builder.Services.AddFlowDashboard(opts => opts.UseWebhookSecurity(sec =>
{
sec.UseEnforcementMode(WebhookEnforcementMode.Audit) // dry-run first
.UseMaxBodyBytes(1_048_576)
.UseReplayProtection(toleranceSeconds: 300)
.UseRateLimit(permitsPerSecond: 50, burstSize: 100, perIp: true)
.UseForwardedHeaders(depth: 1)
.AllowLegacySha1(); // GitHub legacy X-Hub-Signature opt-in
}));
// Storage backends (multi-replica) — replaces in-memory defaults:
builder.Services.AddFlowOrchestrator(options =>
{
options.UseSqlServer(sqlConn);
options.AddSqlServerWebhookHardening(sqlConn);
});
| Property | Type | Default | Description |
|---|---|---|---|
EnforcementMode |
WebhookEnforcementMode |
Off |
Off skips every gate. Audit runs gates + writes DLQ + emits metrics but always returns 202. Enforce rejects failing requests. |
AllowLegacySha1 |
bool |
false |
Allow HMAC-SHA1 signatures (legacy GitHub X-Hub-Signature). Off by default. |
MaxBodyBytes |
long |
1_048_576 |
Hard cap; oversized requests return 413 before JSON parsing. |
ReplayToleranceSeconds |
int |
0 |
Maximum allowed clock skew. 0 disables replay protection. |
DefaultTimestampHeader |
string? |
— | Global timestamp header default (e.g. "X-Webhook-Timestamp"). |
DefaultNonceHeader |
string? |
— | Global nonce / delivery-id header default. |
RateLimit.PermitsPerSecond |
double |
0 |
Sustained throughput. 0 disables rate limiting. |
RateLimit.BurstSize |
int? |
— | Optional burst capacity; defaults to PermitsPerSecond. |
RateLimit.PerIp |
bool |
false |
Key the limiter on flowId\|clientIp instead of flowId. |
ForwardedHeaderDepth |
int |
0 |
Trusted reverse-proxy depth for X-Forwarded-For. 0 uses RemoteIpAddress directly. |
Manifest-level overrides are documented in Triggers — Enterprise hardening (v1.25).
For the full per-publisher cookbook (GitHub, Stripe, Slack, Shopify, …) see the dedicated Webhook hardening article.
Complete Examples
Hangfire Runtime (SQL Server)
builder.Services.AddHangfire(c => c
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(connectionString));
builder.Services.AddHangfireServer();
builder.Services.AddFlowOrchestrator(options =>
{
options.UseSqlServer(connectionString);
options.UseHangfire();
// Scheduler
options.Scheduler.PersistOverrides = true;
// Run control
options.RunControl.DefaultRunTimeout = TimeSpan.FromMinutes(10);
options.RunControl.IdempotencyHeaderName = "Idempotency-Key";
// Observability
options.Observability.EnableEventPersistence = true;
options.Observability.EnableOpenTelemetry = true;
// Retention
options.Retention.Enabled = true;
options.Retention.DataTtl = TimeSpan.FromDays(30);
options.Retention.SweepInterval = TimeSpan.FromHours(1);
// Flows
options.AddFlow<HelloWorldFlow>();
options.AddFlow<OrderFulfillmentFlow>();
options.AddFlow<OrderBatchFlow>();
});
// Step handlers
builder.Services.AddStepHandler<LogMessageHandler>("LogMessage");
builder.Services.AddStepHandler<QueryDatabaseHandler>("QueryDatabase");
// Dashboard
builder.Services.AddFlowDashboard(builder.Configuration);
var app = builder.Build();
app.UseHangfireDashboard("/hangfire");
app.MapFlowDashboard("/flows");
InMemory Runtime (Dev / Testing — no Hangfire required)
builder.Services.AddFlowOrchestrator(options =>
{
options.UseInMemory();
options.UseInMemoryRuntime(); // Channel<T> dispatcher + PeriodicTimer cron
options.RunControl.IdempotencyHeaderName = "Idempotency-Key";
options.Observability.EnableEventPersistence = true;
options.AddFlow<HelloWorldFlow>();
});
builder.Services.AddStepHandler<LogMessageHandler>("LogMessage");
builder.Services.AddFlowDashboard(builder.Configuration);
var app = builder.Build();
app.MapFlowDashboard("/flows");