Starting a workflow on a schedule¶
A workflow can say for itself when it starts: at set times. "Every Monday, start the weekly review", "on the first of each month, start the invoicing run", "on 1 December, start the campaign". The schedule lives on the start node, so it is exported, versioned and shown with the workflow. No module or ECA model is needed next to it.
Two start types read a schedule:
| Start type | Schedule | Module |
|---|---|---|
Start on a schedule (scheduled_start) |
A first date and time, and an optional repeat interval | orchestra_engine |
Start on a cron expression (cron_start) |
A cron expression, such as 0 9 * * 1 |
orchestra_cron |
Both are read on the site's clock, the time zone of the regional settings.
Every Monday at 09:00¶
The weekly review starts on the first Monday, then every week after it:
n_start:
type: scheduled_start
config:
first: '2026-11-09T09:00'
repeat: P1W
slot_variable: week_of
Each start is a slot. The instance started for the slot of 16 November
holds week_of = 2026-11-16T09:00:00+01:00, so its steps know which week
the review is for. The slot variable is optional; leave it empty when the
steps do not need it.
first is a date and time on the site's clock: 2026-11-09T09:00 is 09:00 in
Paris on a Paris site, in winter and in summer alike. One written with an
offset, such as 2026-11-09T09:00+01:00, is read at that offset. A date alone,
such as 2026-11-09, is midnight.
Every month, every day, once¶
repeat is an ISO 8601 duration:
repeat |
Starts |
|---|---|
PT1H |
Every hour |
P1D |
Every day, at the time of first |
P1W |
Every week, on the day and at the time of first |
P1M |
Every month, on the day of first, or the last day of a shorter month |
P3M |
Every quarter |
P1Y |
Every year |
| empty | Once, at first |
The invoicing run starts on the first of each month at 06:00:
n_start:
type: scheduled_start
config:
first: '2026-12-01T06:00'
repeat: P1M
The campaign starts once, on 1 December. The settings form refuses to set a single start to a date that has already passed, since it would never start; once the campaign has started, its workflow still saves with that date:
n_start:
type: scheduled_start
config:
first: '2026-12-01T08:00'
Each slot is counted from first on the calendar, not from the slot before
it. A month stops at the last day of a shorter month, so a monthly schedule
starting on 31 January runs on 28 February, 31 March, 30 April and 31 May:
one slot each month, back on the 31st whenever the month has one. A day stays
a day when the clocks change, so a daily 09:00 stays at 09:00. To start on the
last day of each month, use a cron expression.
Cron expressions¶
The optional orchestra_cron submodule adds Start on a cron expression. It
reads cron expressions with the dragonmantank/cron-expression library, which
Orchestra suggests but does not require:
composer require dragonmantank/cron-expression
drush en orchestra_cron
The module refuses to install without the library, and the status report says when the library is gone.
n_start:
type: cron_start
config:
cron: '0 9 * * 1'
slot_variable: week_of
The five fields are the minute, the hour, the day of the month, the month and the day of the week:
cron |
Starts |
|---|---|
0 9 * * 1 |
Every Monday at 09:00 |
0 6 1 * * |
The first of each month at 06:00 |
0 18 L * * |
The last day of each month at 18:00 |
30 8 * * 1-5 |
Every weekday at 08:30 |
0 7 1 1,4,7,10 * |
The first day of each quarter at 07:00 |
@daily |
Every day at midnight |
When an instance starts¶
The engine's drain starts the slots, on cron or from the drain endpoint. On each run, it asks each enabled workflow with a schedule for its latest slot since the time it last recorded for that workflow: the last slot it started, or the save that changed the schedule. It starts one instance for that slot. So a slot starts at the first drain after it: with cron every five minutes, a 09:00 slot starts by 09:05, and with the drain endpoint called every minute, by 09:01. Run the drain at least as often as the slots need.
| Situation | What starts |
|---|---|
| A slot passed since the last drain | One instance, for that slot |
| Several slots passed, because no drain ran for a while | One instance, for the latest slot only |
| The workflow was just created, or its start node's type or settings changed | Only the slots after the save. Saving notes the time, so a weekly schedule saved on a Wednesday does not start at once for the Monday before, and a slot between the save and the next drain still starts |
| The workflow was saved without touching its start node, such as a new label | Nothing changes: a slot due since the last drain still starts |
| Two drains at once, on one server or two | One instance: one drain runs at a time, and each slot is also claimed in the database before it starts, so only one run can claim it |
| The workflow is disabled, or deleted | Nothing. Enabled again, it starts from the next slot after it was enabled, without the slots it missed |
| Manual versioning mode, and the schedule is edited | Nothing changes until the workflow is published: the drain reads the schedule from the published version. Publishing a new schedule counts as changing it, so only the slots after the publish start |
| The versioning mode changes, and with it which schedule is in effect | Only the slots after the next drain |
| The start fails | Nothing, and the failure is logged. The slot is spent all the same: the start may have saved its instance before failing, and starting it again would start a second one |
A workflow scoped to a tenant starts in that tenant. A workflow shared by every tenant starts in the default tenant.
One instance at a time¶
Tick One instance at a time to skip a slot while an instance of the workflow started with the same data still runs. It is the unique start every way in offers, and the start data of a scheduled start is its slot variable:
- Without a slot variable, every slot starts with the same data, none. A slot is skipped while the instance of an earlier slot still runs. A weekly review that is not done by the next Monday is not started a second time.
- With a slot variable, every slot starts with different data. No slot is ever skipped.
n_start:
type: scheduled_start
config:
first: '2026-11-09T09:00'
repeat: P1W
unique: true
A skipped slot is logged. The next slot starts as usual, once the earlier instance has ended.
In the workflow list¶
The workflow list names the schedule in each workflow's components:
Start (scheduled_start: R/2026-11-09T09:00/P1W), in the ISO 8601 notation of
a repeated interval, or Start (cron_start: 0 9 * * 1). The list shows the
workflow as saved: in manual versioning mode, a schedule edited but not
published yet is listed, while cron still follows the published one.
Other start types¶
A new workflow is seeded with a plain Start. A scheduled start, like an event start, replaces it on the start node: a workflow has one start node. A workflow with a scheduled start can still be started by hand, from the Start operation or the API, outside its schedule.
For a code-defined start type, implement ScheduledStartTypeInterface:
getLastSlot() gives the latest slot in a span of time, and
ScheduledStartConfigTrait adds the slot variable and the unique setting. The
cron sweep finds every start type implementing it, without knowing its name.