Date: 2026-08-03 Status: Design - approved Owner: Ferenc Szontagh
Let a schedule trigger declare what happens when its scheduled time arrives while a previous run of the same workflow is still in flight: skip the tick, defer it until the run finishes, or allow a bounded number of concurrent runs.
WorkflowScheduler::checkAndExecute already skips a workflow whose running
flag is set, but that flag is meaningless in practice:
webserver_service.cpp dispatches scheduled runs with
request.set_wait_for_completion(false), so the gRPC call returns as soon as
the runner accepts the job.checkAndExecute clears running immediately after the callback returns.So running covers the dispatch, a few milliseconds, not the execution. Two runs
of the same workflow can overlap today with no protection.
A second defect: next_run is computed from a now captured before execution.
When a run outlasts its interval, next_run is already in the past and the next
tick fires immediately.
This matters concretely: run duration is dominated by hosted vision latency, measured between 12s and 117s per image, so a slow patch can push a run past a 10-minute interval.
Added to nodes/core/schedule-trigger.js:
| Field | Type | Default | Meaning |
|---|---|---|---|
overlapPolicy |
skip | queue | allow |
skip |
Behaviour when a run is already active |
maxConcurrent |
integer >= 1 | 1 | Concurrent run ceiling, only meaningful for allow |
maxRunMinutes |
integer >= 0 | 0 | Treat a run as abandoned after this long. 0 derives it as 5x the interval. |
Semantics:
activeRuns < maxConcurrent.ScheduledWorkflow gains overlap_policy, max_concurrent, max_run_minutes,
an active_runs map of execution id to deadline, and a pending_start flag.
New public methods:
notifyExecutionStarted(workflow_id, execution_id) - called after a successful
dispatch, records the run and its deadline.notifyExecutionFinished(workflow_id, execution_id) - called on
completion, failure or cancellation, releases the slot and, when a
pending_start is set, makes the workflow immediately eligible.checkAndExecute gains, before dispatch:
skip or queue workflow forever.active_runs count.next_run is recomputed from the time after dispatch.
webserver_service.cpp reads the three new fields when registering a workflow
and calls notifyExecutionStarted once the runner returns an execution id.ExecutionController gains a WorkflowScheduler& and calls
notifyExecutionFinished for execution.completed, execution.failed and
execution.cancelled. It already receives these events with both
executionId and workflowId.skip drops ticks, queue fires exactly one deferred run, and allow with
maxConcurrent 2 permits two.checkAndExecute dispatches serially, but
dispatch is now non-blocking, so one workflow no longer stalls others.