Table of Contents

Triggers

Triggers define how a flow is started. Each flow can declare any number of triggers in its manifest — all of them can fire independently to create new runs.

Trigger Types

Manual

The simplest trigger. A run is created when someone clicks Trigger in the dashboard or calls the REST API.

["manual"] = new TriggerMetadata { Type = TriggerType.Manual }
POST /flows/api/flows/{flowId}/trigger
Content-Type: application/json

{ "orderId": "ORD-123" }

Any JSON body you include becomes the trigger payload, accessible in steps via @triggerBody().


Cron

Registers a recurring job with the active runtime adapter that fires the flow on a cron schedule.

  • Hangfire runtime — maps to a Hangfire RecurringJob driven by IRecurringJobManager.
  • InMemory runtime — driven by an in-process PeriodicTimer (1 s tick) using Cronos for expression parsing. No external scheduler needed.
  • ServiceBus runtime (v1.22+) — schedules each cron firing as a Service Bus message with ScheduledEnqueueTime on the flow-cron-triggers queue. The cron consumer fires the trigger and self-perpetuates the next message. Single-delivery semantics across replicas — no leader election needed.

All three runtimes implement the same IRecurringTriggerDispatcher / IRecurringTriggerSync abstractions, so cron behaviour (overrides, pause/resume, dashboard controls) is identical across them.

["nightly"] = new TriggerMetadata
{
    Type = TriggerType.Cron,
    Inputs = new Dictionary<string, object?>
    {
        ["cronExpression"] = "0 2 * * *"  // every day at 02:00 UTC
    }
}

Cron expression format: standard 5-field Quartz/Hangfire cron (min hour day month weekday).

Common patterns:

Expression Fires
* * * * * Every minute
*/5 * * * * Every 5 minutes
0 9 * * 1-5 Weekdays at 09:00
0 0 1 * * First day of every month
Tip

Cron overrides written via the dashboard or PUT /flows/api/schedules/{jobId}/cron are persisted when Scheduler.PersistOverrides = true and survive process restarts.

Disabled flows

A flow whose IFlowStore record has IsEnabled = false (toggled via the dashboard's disable button or PUT /flows/api/flows/{id}/disable) silently rejects ALL trigger paths — manual, cron, webhook, and re-trigger — at the engine layer (v1.22+). The engine consults IFlowStore.GetByIdAsync(flowId).IsEnabled at the top of TriggerAsync and returns { runId: null, disabled: true } without dispatching, emitting EventId 1010 TriggerRejectedDisabledFlow and tagging the trigger activity with flow.disabled = true.

Cron jobs are additionally removed from the scheduler when a flow is disabled, so cron ticks don't even reach the engine in normal operation; the engine-level gate catches the narrow race window where a tick fired just before the disable propagated. In-flight runs (those already started before disable) are NOT cancelled — disable is for stopping NEW work, not killing live work. To cancel a live run, use the dashboard's Cancel run action.


Webhook

Registers a webhook endpoint. External systems POST to the URL to trigger a run.

["order_received"] = new TriggerMetadata
{
    Type = TriggerType.Webhook,
    Inputs = new Dictionary<string, object?>
    {
        ["webhookSlug"]   = "new-order",       // endpoint: POST /flows/api/webhook/new-order
        ["webhookSecret"] = "my-secret-key"    // required in X-Webhook-Key header
    }
}
POST /flows/api/webhook/new-order
Content-Type: application/json
X-Webhook-Key: my-secret-key

{ "orderId": "ORD-456", "total": 129.99 }
  • webhookSlug — URL path segment. Must be unique across all registered webhooks.
  • webhookSecret — When set, every incoming request must include X-Webhook-Key: {secret}. Requests without or with the wrong secret receive 401 Unauthorized.
  • If webhookSecret is omitted, the endpoint is unauthenticated.

Enterprise hardening (v1.25)

The dashboard webhook endpoint ships an opt-in security pipeline: HMAC signature verification → IP allow / deny → rate limit → replay protection. With the default WebhookEnforcementMode.Off the endpoint behaves exactly as in v1.24; flip to Audit for one release to dry-run, then Enforce to start rejecting.

Activate the pipeline in DI:

services.AddFlowDashboard(opts => opts.UseWebhookSecurity(sec =>
{
    sec.UseEnforcementMode(WebhookEnforcementMode.Enforce)
       .UseReplayProtection(toleranceSeconds: 300)
       .UseRateLimit(permitsPerSecond: 50, perIp: true)
       .UseForwardedHeaders(depth: 1)
       .UseMaxBodyBytes(1_048_576);
}));

Activate per flow via manifest inputs. The full set of v1.25 fields:

Field Purpose
webhookSignatureScheme Selects a built-in dialect: GitHub, GitHubLegacy, Bitbucket, Stripe, Slack, Shopify, Twilio, Square, Zoom, Linear, Dropbox, Mailgun, MicrosoftTeams, Atlassian, Calendly, Generic, or Custom.
webhookHmacKey Signing key used for HMAC verification. Falls back to webhookSecret when absent.
webhookHmacKeyPrevious / webhookSecretPrevious Optional rotated-out key. Successful matches against the previous key emit EventId 4010 (WebhookSecretRotationUsedPrevious).
webhookSignatureHeader, webhookSignatureAlgorithm, webhookSignatureEncoding, webhookSignaturePrefix, webhookSignatureMultiValueDelimiter, webhookSignatureKeyValueSeparator, webhookSignatureValueKey, webhookTimestampValueKey, webhookTimestampHeader, webhookSignedPayloadStrategy, webhookSignedPayloadDelimiter, webhookSignedPayloadVersion, webhookHeaderValuePrefix, webhookCustomStrategyName Drive the Custom scheme directly from the manifest (every aspect of the wire format is controllable).
webhookReplayToleranceSeconds Max clock skew between publisher timestamp and server time. 0 disables replay protection.
webhookNonceHeader Optional explicit nonce header (e.g. X-GitHub-Delivery). Default: SHA-256 of timestamp \|\| body.
webhookRateLimitPermitsPerSecond, webhookRateLimitBurstSize, webhookRateLimitPerIp Token-bucket overrides. Per-IP keying is opt-in.
webhookIpAllowList, webhookIpDenyList IP entries — CIDR (10.0.0.0/8, 2001:db8::/32), inclusive range (10.0.0.10-10.0.0.42), wildcard (10.0.*.*), single address, or any combination. Accepts an array OR a comma-delimited string. Allow takes precedence.
webhookIpAllowListPreset Single curated publisher CIDR set. Presets: github, stripe, shopify, twilio, square, atlassian, bitbucket, slack, mailgun, zoom, local / private.
webhookIpAllowListPresets Multiple presets combined (array or comma-delimited string). Merges with webhookIpAllowListPreset and the explicit webhookIpAllowList.

Per-publisher cookbook example — GitHub:

["webhook"] = new TriggerMetadata
{
    Type = TriggerType.Webhook,
    Inputs = new Dictionary<string, object?>
    {
        ["webhookSlug"] = "github-events",
        ["webhookHmacKey"] = "<the secret you configured at github.com/.../settings/hooks>",
        ["webhookSignatureScheme"] = "GitHub",
        ["webhookReplayToleranceSeconds"] = 300,
        ["webhookNonceHeader"] = "X-GitHub-Delivery",
        ["webhookIpAllowListPreset"] = "github",
    }
}

The dashboard exposes a "Webhooks" tab that surfaces the recent-deliveries log and a 24-hour reason histogram (GET /flows/api/webhooks/recent, GET /flows/api/webhooks/stats). Rejected requests are persisted to the in-memory ring buffer (last 1 000 entries by default); a Sql / Postgres backend ships in a follow-up release for long retention.


Multiple Triggers per Flow

A single flow can declare any combination of triggers:

Triggers = new FlowTriggerCollection
{
    ["manual"]   = new TriggerMetadata { Type = TriggerType.Manual },
    ["nightly"]  = new TriggerMetadata
    {
        Type = TriggerType.Cron,
        Inputs = new Dictionary<string, object?> { ["cronExpression"] = "0 1 * * *" }
    },
    ["webhook"]  = new TriggerMetadata
    {
        Type = TriggerType.Webhook,
        Inputs = new Dictionary<string, object?>
        {
            ["webhookSlug"]   = "process-batch",
            ["webhookSecret"] = "batch-secret"
        }
    }
}

Each trigger type fires independently and creates a separate run.


Idempotency

To prevent duplicate runs when the same event is delivered more than once (at-least-once delivery from upstream systems):

POST /flows/api/flows/{id}/trigger
Content-Type: application/json
Idempotency-Key: batch-2026-04-19-001

{ "batchId": "BATCH-001" }

If a run with the same Idempotency-Key value already exists for this flow, FlowOrchestrator returns the existing runId without creating a new run.

The header name is configurable:

options.RunControl.IdempotencyHeaderName = "Idempotency-Key";  // default

Idempotency keys work for both manual triggers and webhook triggers.


Trigger Payload Access

The JSON body and headers of the triggering request are persisted and available in any step's inputs via expressions:

// In StepMetadata.Inputs:
["orderId"]    = "@triggerBody()?.orderId"
["requestId"]  = "@triggerHeaders()['X-Request-Id']"
["allOrders"]  = "@triggerBody()?.orders"

See Expressions for the full expression reference.