diff --git a/SUMMARY.md b/SUMMARY.md index 9ef5e92..972ca40 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -67,6 +67,7 @@ * [Running Workflows](guides/running-workflows/README.md) * [Using Elsa Studio](guides/running-workflows/using-elsa-studio.md) * [Using a Trigger](guides/running-workflows/using-a-trigger.md) + * [Timer and Scheduled Workflows](guides/running-workflows/timer-and-scheduled-workflows.md) * [Dispatch Workflow Activity](guides/running-workflows/dispatch-workflow-activity.md) * [Bulk Dispatch Workflows Activity](guides/running-workflows/bulk-dispatch-workflows.md) * [Studio User Guide](guides/studio/README.md) diff --git a/docs/meta/backlog.md b/docs/meta/backlog.md index 564eb59..929b526 100644 --- a/docs/meta/backlog.md +++ b/docs/meta/backlog.md @@ -4,7 +4,7 @@ This backlog is prioritized by user impact and frequency of complaints based on gap analysis from 161 issues across elsa-studio and elsa-gitbook. -## Slice Inventory (2026-06-23) +## Slice Inventory (2026-06-27) This inventory reflects the current GitBook contents before selecting the next automation slice. "Covered" means the repository now includes a @@ -36,6 +36,7 @@ acceptance criterion below is already complete. - `DOC-028` Studio customization - `DOC-029` Custom UI hints - `DOC-030` Custom UI components +- `DOC-040` Timer and scheduled workflows - `DOC-041` Loading workflows from JSON - `DOC-042` Bulk dispatch workflows activity - `DOC-049` Studio custom-elements embedding cookbook @@ -51,7 +52,6 @@ acceptance criterion below is already complete. - `DOC-027` Execution model - `DOC-038` Distributed tracing - `DOC-039` Performance tuning -- `DOC-040` Timer and scheduled workflows - `DOC-043` Hangfire integration - `DOC-047` API reference - `DOC-048` Activity reference @@ -59,9 +59,9 @@ acceptance criterion below is already complete. ### Recommended next slice -- `DOC-019` HTTP endpoint security: the HTTP workflow guides now explain how - to build endpoints, but the auth surface is still spread across multiple - pages. A single release-backed guide should connect `Authorize`, +- `DOC-019` HTTP endpoint security: still the highest-value next slice once the + current in-progress auth and security edits are either committed or moved to a + clean worktree. A single release-backed guide should connect `Authorize`, `HttpEndpoint`, API permissions, public vs authenticated endpoints, and Studio-facing troubleshooting. @@ -82,12 +82,10 @@ acceptance criterion below is already complete. ### Current slice note -- `DOC-042` Bulk dispatch workflows activity: - add a dedicated, release-backed guide for `BulkDispatchWorkflows` that - explains how item input mapping works, when `WaitForCompletion` blocks the - parent workflow, how `ChildCompleted` and `ChildFaulted` execute per child, - what Studio users should configure, and when to use this activity instead of - `DispatchWorkflow`, `ForEach`, or the workflow-definition bulk-dispatch API. +- `DOC-040` Timer and scheduled workflows: + add a dedicated release-backed guide that separates `Delay`, `StartAt`, + `Timer`, and `Cron`, explains bookmark vs trigger scheduling, and points + users to durable scheduler options for clustered deployments. ### DOC-001: V2 to V3 Migration Guide - **Persona**: Backend Integrator, Architect diff --git a/guides/running-workflows/bulk-dispatch-workflows.md b/guides/running-workflows/bulk-dispatch-workflows.md index 3b9182a..21d23d1 100644 --- a/guides/running-workflows/bulk-dispatch-workflows.md +++ b/guides/running-workflows/bulk-dispatch-workflows.md @@ -21,9 +21,10 @@ For each item in `Items`, Elsa: 1. Resolves the published version of the child workflow definition. 2. Creates a new child workflow instance. -3. Adds `ParentInstanceId` to the child input and workflow properties. -4. Merges the current item into the child input. -5. Dispatches the child workflow through the selected channel. +3. Sets `ParentWorkflowInstanceId` on the dispatch request so the runtime can track the parent-child relationship. +4. Adds `ParentInstanceId` to the child input and dispatch properties. +5. Merges the current item into the child workflow input. +6. Dispatches the child workflow through the selected channel. If `WaitForCompletion` is `true`, the parent workflow creates a bookmark and resumes only after all dispatched child workflows finish. If `Items` is empty, the activity completes immediately. @@ -36,6 +37,8 @@ If `WaitForCompletion` is `true`, the parent workflow creates a bookmark and res Elsa also merges any values from the activity's `Input` property into every child workflow input. +If the current item contains the same key as `Input`, the current item wins because Elsa applies the item dictionary last. + This means each child workflow receives: - the shared `Input` values @@ -47,6 +50,7 @@ This means each child workflow receives: The following example is grounded in the `release/3.8.0` component tests. The parent workflow dispatches one child workflow for each employee record and waits for all of them to finish. {% code title="GreetEmployeesWorkflow.cs" %} + ```csharp using System.Collections.Generic; using Elsa.Workflows; @@ -84,9 +88,11 @@ public class GreetEmployeesWorkflow : WorkflowBase } } ``` + {% endcode %} {% code title="EmployeeGreetingWorkflow.cs" %} + ```csharp using Elsa.Extensions; using Elsa.Workflows; @@ -105,6 +111,7 @@ public class EmployeeGreetingWorkflow : WorkflowBase } } ``` + {% endcode %} When the child workflow expects a single simple value instead of a dictionary, keep the default `DefaultItemInputKey = "Item"` and read that input from the child workflow. @@ -128,6 +135,14 @@ Use this when each dispatched child workflow should get its own predictable corr When Elsa waits for completion, each child workflow gets a `WaitForCompletion` marker in its properties so the runtime can resume the parent workflow when that child finishes. +## Activity outcomes + +`BulkDispatchWorkflows` exposes the `Completed`, `Canceled`, and `Done` outcomes in its flow node metadata. + +- When `WaitForCompletion` is `false`, Elsa dispatches the child workflows and completes this activity with `Done`. +- When `WaitForCompletion` is `true` and `Items` is empty, Elsa also completes immediately with `Done`. +- When `WaitForCompletion` is `true` and at least one child workflow was dispatched, Elsa waits until all tracked child workflows finish, then completes with both `Completed` and `Done`. + ## Child completion and fault ports When `WaitForCompletion` is `true`, the activity can schedule extra work for each child result: @@ -144,6 +159,8 @@ While those ports run, Elsa provides: This makes the activity useful for fan-out/fan-in orchestration where the parent needs to count, aggregate, or compensate for per-child outcomes. +If `WaitForCompletion` is `false`, Elsa never schedules these ports because the parent workflow does not wait for child completion events. + ## Using it in Elsa Studio In Elsa Studio, configure **Bulk Dispatch Workflows** with these fields: @@ -154,6 +171,7 @@ In Elsa Studio, configure **Bulk Dispatch Workflows** with these fields: - **Correlation ID Function**: an optional expression evaluated for each item. - **Input**: shared input values added to every child workflow. - **Wait For Completion**: whether the parent should block until all child workflows finish. +- **Start New Trace**: start a new OpenTelemetry trace context for each dispatched child workflow. - **Channel**: optional dispatcher channel. Leaving it empty uses the default channel. If you connect the `Child Completed` or `Child Faulted` ports, expect them to run once per child workflow, not once for the entire batch. @@ -162,4 +180,5 @@ If you connect the `Child Completed` or `Child Faulted` ports, expect them to ru - Elsa dispatches published workflow definitions only. If no published child workflow definition exists, the parent workflow faults. - `BulkDispatchWorkflows` does not aggregate child outputs into one collection automatically. Handle that in the parent workflow through variables, `ChildCompleted`, or `ChildFaulted`. -- The parent-child relationship is tracked through `ParentWorkflowInstanceId` and `ParentInstanceId`, which lets Elsa resume the waiting parent workflow after child completion. +- `ParentWorkflowInstanceId` links the runtime parent-child relationship, while `ParentInstanceId` is also added to the child input and dispatch properties for workflow logic and resume handling. +- When you use both shared `Input` values and dictionary-shaped items, item keys overwrite the same keys from `Input`. diff --git a/guides/running-workflows/timer-and-scheduled-workflows.md b/guides/running-workflows/timer-and-scheduled-workflows.md new file mode 100644 index 0000000..7d04930 --- /dev/null +++ b/guides/running-workflows/timer-and-scheduled-workflows.md @@ -0,0 +1,187 @@ +--- +description: Release-backed guidance for Delay, StartAt, Timer, and Cron workflows. +--- + +# Timer and Scheduled Workflows + +Elsa ships with four scheduling-oriented activities that look similar in the designer but serve different jobs: + +| Activity | What it does | Typical use | +| --- | --- | --- | +| `Delay` | Pauses the current workflow instance for a `TimeSpan` | Wait 10 minutes, 2 days, or 30 seconds before continuing | +| `StartAt` | Starts a workflow at a specific `DateTimeOffset`, or waits until that instant inside a running workflow | Launch at a known timestamp such as `2026-07-01T09:00:00Z` | +| `Timer` | Starts new workflow instances on a fixed interval; inside a running workflow it waits one interval before continuing | Poll every 5 minutes or run a recurring maintenance workflow | +| `Cron` | Starts new workflow instances from a cron expression; inside a running workflow it waits until the next matching occurrence | Run every weekday at 06:00 UTC | + +This behavior is grounded in the `Elsa.Scheduling` module on `release/3.8.0`: + +* `Delay` reads `TimeSpan` and calls `context.DelayFor(...)`. +* `StartAt` stores a `StartAtPayload` and completes immediately if the configured time is already in the past. +* `Timer` inherits `TimerBase`, which calls `context.RepeatWithInterval(...)`. +* `Cron` computes the next occurrence from `ICronParser` and creates a cron bookmark. + +## Choose the right activity + +Use `Delay` when you are already inside a workflow instance and want to pause it for a relative duration. + +Use `StartAt` when you need a one-time absolute schedule. This is the best fit for "run exactly at this timestamp". + +Use `Timer` when you want a recurring interval. As a workflow trigger, it schedules repeated new workflow runs. Inside a workflow body, it waits one interval and then continues. + +Use `Cron` when you need calendar-style recurrence such as weekdays, first day of month, or every 15 minutes. + +## How Elsa schedules them + +When you enable `UseScheduling(...)`, Elsa registers the scheduling module and, by default, an in-process `LocalScheduler`. + +For workflow bookmarks, `DefaultBookmarkScheduler` translates scheduling bookmarks into scheduler requests: + +* `Delay`, `StartAt`, and `Timer` bookmarks are scheduled with `ScheduleAtAsync(...)`. +* `Cron` bookmarks are scheduled with `ScheduleCronAsync(...)`. + +For workflow triggers, `DefaultTriggerScheduler` does the equivalent work for published workflow definitions: + +* `Timer` triggers call `ScheduleRecurringAsync(...)`. +* `StartAt` triggers call `ScheduleAtAsync(...)`. +* `Cron` triggers call `ScheduleCronAsync(...)`. + +This distinction matters: + +* bookmarks resume an existing workflow instance +* triggers start a new workflow instance from a published definition + +## Delay + +`Delay` is the simplest option when the workflow is already running and only needs to pause for a duration. + +```csharp +using Elsa.Workflows.Activities; +using Elsa.Scheduling.Activities; + +builder.Root = new Sequence +{ + Activities = + { + new WriteLine("Waiting 30 minutes"), + new Delay(TimeSpan.FromMinutes(30)), + new WriteLine("Continuing after the delay") + } +}; +``` + +In `release/3.8.0`, the public input is named `TimeSpan`. Older examples that show `Duration` are not correct for Elsa 3.8. + +## StartAt + +`StartAt` is a one-time absolute schedule. + +```csharp +using Elsa.Scheduling.Activities; + +builder.Root = new StartAt(new DateTimeOffset(2026, 7, 1, 9, 0, 0, TimeSpan.Zero)); +``` + +Important behavior from source: + +* the input is `DateTime`, not `StartAt` or `ExecuteAt` +* if the configured time is already in the past when the activity executes inside a running workflow, Elsa completes the activity immediately +* when used as a trigger, Elsa stores a `StartAtPayload` and schedules a single workflow start + +Prefer UTC timestamps. The scheduling code uses `ISystemClock.UtcNow`, so using `DateTimeOffset` in UTC avoids avoidable timezone confusion. + +## Timer + +`Timer` is interval-based, not timestamp-based. + +```csharp +using Elsa.Scheduling.Activities; + +builder.Root = new Timer(TimeSpan.FromMinutes(5)); +``` + +The public input is `Interval`. + +Two common ways to use it: + +1. As the first activity in a published workflow definition, where it acts as a recurring trigger and starts a new workflow instance every interval. +2. Inside a running workflow, where it creates a timer bookmark for `UtcNow + Interval` and resumes that same instance once the interval elapses. + +If you need "run every day at 09:00", prefer `Cron` or `StartAt`, not `Timer`. + +## Cron + +`Cron` is for calendar-based recurring schedules. + +```csharp +using Elsa.Scheduling.Activities; + +builder.Root = new Cron("0 0 6 ? * MON-FRI *"); +``` + +The public input is `CronExpression`. + +In `release/3.8.0`: + +* core scheduling uses `CronosCronParser` by default +* `Cron` stores the next occurrence in journal data as `ExecuteAt` +* `DefaultTriggerScheduler` skips empty cron payloads and logs warnings for invalid cron expressions + +Use cron when your schedule is tied to the calendar rather than a fixed interval. + +## Elsa Studio guidance + +For Studio users, the practical choice is: + +* drag `Delay` into the workflow body for relative waits +* use `StartAt`, `Timer`, or `Cron` as the first activity when you want the workflow definition itself to start on a schedule +* publish the workflow after configuring the trigger, otherwise the trigger will not be scheduled + +If you are evaluating a workflow and need to see whether the trigger is starting new instances or resuming an existing one, inspect the workflow instance list and execution logs in Studio after publishing. + +## Single-node vs durable scheduling + +By default, `SchedulingFeature` registers `LocalScheduler`, which keeps schedules in memory. That is fine for local development and single-node scenarios, but it is not durable across process restarts. + +For durable or clustered scheduling, switch the workflow scheduler implementation: + +```csharp +services.AddElsa(elsa => +{ + elsa.UseScheduling(scheduling => scheduling.UseQuartzScheduler()); + elsa.UseQuartz(quartz => quartz.UsePostgreSql(connectionString)); +}); +``` + +Quartz support in `release/3.8.0` replaces the scheduling feature's `WorkflowScheduler` with `QuartzWorkflowScheduler` and swaps the cron parser to `QuartzCronParser`. + +Hangfire is also available: + +```csharp +using Hangfire.SqlServer; + +services.AddElsa(elsa => +{ + elsa.UseHangfire(hangfire => hangfire.UseJobStorage(new SqlServerStorage(connectionString))); + elsa.UseScheduling(scheduling => scheduling.UseHangfireScheduler()); +}); +``` + +Hangfire support replaces the scheduling feature's `WorkflowScheduler` with `HangfireWorkflowScheduler`. + +Choose Quartz or Hangfire when you need restart durability or multi-node coordination. Keep the default scheduler when you only need lightweight in-process scheduling. + +## Common mistakes + +| Mistake | What to do instead | +| --- | --- | +| Using `Timer` to mean "at 09:00 tomorrow" | Use `StartAt` for one time or `Cron` for recurring calendar schedules | +| Using old `Delay { Duration = ... }` examples | Use `Delay { TimeSpan = ... }` or `new Delay(TimeSpan.FromMinutes(...))` | +| Forgetting to publish a trigger-based workflow | Publish the workflow definition so Elsa can schedule its triggers | +| Using in-memory scheduling in a clustered deployment | Use Quartz or Hangfire-backed scheduling | + +## Related guides + +* [Using a Trigger](using-a-trigger.md) +* [Clustering](../clustering/README.md) +* [Workflow Dispatcher Architecture](../architecture/workflow-dispatcher.md) +* [Blocking Activities & Triggers](../../activities/blocking-and-triggers/README.md) diff --git a/guides/running-workflows/using-a-trigger.md b/guides/running-workflows/using-a-trigger.md index f5aa28c..5684325 100644 --- a/guides/running-workflows/using-a-trigger.md +++ b/guides/running-workflows/using-a-trigger.md @@ -7,10 +7,12 @@ A trigger is represented by an activity, which provides trigger details to servi Elsa ships with various triggers out of the box, such as: * HTTP Endpoint: triggers the workflow when a given HTTP request is sent to the workflow server. -* Timer: triggers the workflow each given interval based on a TimeSpan expression. -* Cron: triggers the workflow each given interval based on a CRON expression. +* Timer: triggers the workflow on a fixed recurring interval based on a `TimeSpan` expression. +* Cron: triggers the workflow on a recurring cron schedule. * Event: triggers when a given event is received by the workflow server. +For the scheduling-specific differences between `StartAt`, `Timer`, `Cron`, and `Delay`, see [Timer and Scheduled Workflows](timer-and-scheduled-workflows.md). + We will use the HTTP Endpoint trigger as an example. ## Using Code @@ -57,4 +59,3 @@ public class HelloWorldHttpWorkflow : WorkflowBase Follow this guide to see step-by-step how to create a simple HTTP workflow using the HTTP Endpoint trigger. {% embed url="https://dubble.so/guides/http-trigger-zuc6xtxdvoo49omo5oly" %} -