Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 9 additions & 11 deletions docs/meta/backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -51,17 +52,16 @@ 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
- `DOC-053` Alterations operational guide hardening

### 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.

Expand All @@ -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
Expand Down
27 changes: 23 additions & 4 deletions guides/running-workflows/bulk-dispatch-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -84,9 +88,11 @@ public class GreetEmployeesWorkflow : WorkflowBase
}
}
```

{% endcode %}

{% code title="EmployeeGreetingWorkflow.cs" %}

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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.
Expand All @@ -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`.
187 changes: 187 additions & 0 deletions guides/running-workflows/timer-and-scheduled-workflows.md
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 4 additions & 3 deletions guides/running-workflows/using-a-trigger.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" %}