Two nodes put time in your hands: the Planification (cron) trigger starts a workflow on a schedule, and the Attendre node pauses a running one. Both are durable — a pause is not a held connection or a sleeping process, it is state in the execution engine, so it survives restarts and deploys.
Schedule a workflow
Pick Planification (cron) as the trigger, then add rules. A rule is one of seven kinds:
| Kind | Fields | Fires |
|---|---|---|
| Every N seconds | interval (≥ 30) | aligned on the epoch |
| Every N minutes | interval | aligned on the epoch |
| Every N hours | interval, at minute | */N hours, restarting at midnight |
| Every N days | interval, at hour, at minute | */N days, restarting on the 1st |
| Every N weeks | interval, weekdays, at hour, at minute | the chosen weekdays, one week in N |
| Every N months | interval, day of month, at hour, at minute | */N months; a month too short is skipped |
| Cron expression | expression | 5 fields, or 6 with seconds first |
Several rules can coexist — the earliest one fires, and the trigger's rule
output carries its index (0-based) so you can route on which one it was. The
panel previews the next three occurrences as you type, computed with the very
code the scheduler runs.
trigger:
type: trigger.schedule
config:
timezone: Europe/Paris # optional — defaults to the instance's
rules:
- { kind: days, interval: 1, atHour: 7, atMinute: 30 }
- { kind: weeks, interval: 1, onWeekdays: [1, 4], atHour: 18 }
Weekdays follow the cron convention (0 = Sunday … 6 = Saturday); the editor
shows them Monday-first with names, so you rarely type the numbers.
Times are local, and stay local across daylight-saving changes: a 09:00 rule
fires at 09:00 before and after the transition, which means the real interval
that day is 23 or 25 hours. The timezone comes from the rule, or failing that
from the instance manifest (timezone, default Europe/Paris).
An empty rule list means "not configured yet" — the pause is unpublishing
A schedule fires as soon as its rules exist, so a new schedule trigger starts
with rules empty and you add them last. But an empty list is not a pause: it is
an unfinished schedule.
To pause a schedule that is already live, unpublish the workflow — the
platform's single pause, whatever the trigger (see
Manage workflow storage).
An unpublished workflow is disarmed on the scheduler, so it stops firing, and
republishing it (a rollback) re-arms the schedule as it was. Emptying rules
instead would lose the rules you wrote.
Minimum frequency: 30 seconds
Anything faster is refused when you save. The reason is not load: the run index is audit history and is never purged, so an over-frequent schedule leaves permanent rows behind. Clinically meaningful periods run from minutes to months; for a one-off test, call a webhook instead.
Make a run wait
Add an Attendre node anywhere in the graph. It has one optional value
input, re-emitted on value after the wait, so data flows through it. With
nothing wired, use the implicit après/terminé pins to sequence: the node
after the wait simply starts later.
For a duration
attente:
type: flow.wait
config: { mode: interval, amount: 24, unit: hours }
Units: seconds, minutes, hours, days.
Until a date
attente:
type: flow.wait
config: { mode: until }
expose: [until] # wired from a DateTime computed upstream
until can be fixed in the config or exposed as a port and wired — an
appointment date read from the record, a deadline computed from an admission.
A date already in the past does not wait; it is traced and the run continues.
Until an HTTP call
attente:
type: flow.wait
config:
mode: webhook
auth: header # none | basic | header
credential: cred-123 # required unless auth is none
limit: { kind: interval, amount: 7, unit: days } # optional
The run pauses and the console shows its resume URL, also served on
GET /api/runs/<runId> as waiting.resumeUrl:
curl -X POST http://localhost:4000/api/resume/<runId>/attente \
-H 'content-type: application/json' \
-d '{"decision":"go"}'
The body comes out on the resume port. Authentication is the same mechanism as
an inbound webhook, using a credential from the Credentials module — the endpoint
is public, so it is closed by default and only auth: none leaves it open.
limit bounds the wait ({kind: interval, amount, unit} or
{kind: until, at}). When it expires, the node emits timedOut only — no
value, no resume — so wiring an escalation branch off timedOut needs no
condition: absence propagation mutes the normal branch on its own. Without a
limit, the run waits indefinitely.
What a waiting run looks like
Its status is waiting, deliberately distinct from suspended, which means
a human owes a decision. The waiting node wears a grey hourglass in the run
graph, and the step-by-step names it, with its deadline and — in webhook mode —
the resume URL. So the "à trancher" queue stays a list of things people must
act on, and never fills up with countdowns.
Testing without waiting
Simulation never suspends. A wait is reported instead of endured: the report
lists every wait the run would have taken (waits), so a 24-hour pause comes
back instantly. For webhook mode, script the outcome — a payload to resume with,
or a timeout to take the escalation branch.
Limits worth knowing
- In a loop body, duration waits work but run in series: ten items with a one-hour wait is a ten-hour run. A webhook resume is refused there outright, since one URL cannot address a single iteration.
- Renaming a waiting node while a run is paused on it makes its resume URL unreachable; the run then ends on its wait limit, or waits forever.
- The step-by-step of a finished run is kept for the engine's retention window (24 h by default). A long wait is not affected — a paused run is still live — but the detail of a completed one eventually reduces to the index summary.
See also
- Trigger a workflow with a webhook — the inbound URL, typed body, sync/async response.
- Sequence and join with after/done — how the flow pins order a graph without carrying data.
- Add human validation — when the pause is a decision someone owes.