Manual Run Control
A flow can refuse runs a person starts by hand while still running normally from a schedule, a webhook, or another flow. Use it for the two kinds of flow that are not entry points:
- Subflows. A flow whose only job is to be called by a parent's Run Flow step misbehaves when started alone, because its parameters come from the caller.
- Automation-only flows. A flow that should only run on its own cadence, where a hand-started run outside a maintenance window touches real devices.
Manual is the fourth trigger, and until now it was the only one with no off switch: a schedule pauses, a webhook has enabled, and a parent's Run Flow step is removed by editing the parent. The alternative was taking run:trigger away from the operator role, which stops manual runs of every flow.
Turning it off
Open the flow, click the Manual card on its Start node, and press Turn off. The card greys out and reads Off, exactly as a paused schedule does.
It takes effect immediately — there is no commit. The switch is a property of the flow, not of its definition, which also means it cannot be walked around by hand-starting an older committed version.
If no schedule, webhook or parent flow targets the flow, turning Manual off leaves nothing able to start it. That is a legitimate state — you may be about to add a schedule — so the editor asks for confirmation rather than refusing.
Over the API:
PATCH /flows/{flow_id}/manual-runs
{"manual_runs_enabled": false}It needs flow:manage, the same action as any other edit to the flow, and it is recorded in the audit log like one.
Which triggers are affected
| Trigger | Started by | Affected |
|---|---|---|
manual | A person via the UI Run button or POST /runs | Refused |
run_now | A person pressing Run now on a schedule | Allowed |
schedule | The scheduler, on the schedule's cadence | Allowed |
webhook | An inbound call to a webhook endpoint | Allowed |
flow | A parent flow's flow.run step | Allowed |
"Run now" still works. It is a person pressing a button, but it is the schedule's trigger: the schedule vouches for the run. Blocking it would leave no one-click way to try a flow that runs fine on its own cadence.
Nested runs still work, and must: refusing them would break the very case this feature exists for. They are created through the worker-facing /internal API, which is guarded by the internal token rather than by RBAC — anything holding that token can already execute arbitrary flows.
What a caller sees
A refused run returns 409 Conflict:
Flow 'Upgrade one device' does not allow runs started by hand. It can still be
started by a schedule, a webhook, or another flow's Run Flow step. Turn manual
runs back on from the Manual card on the flow's Start node.409 rather than 403 is deliberate: the caller may well hold run:trigger, and the payload is fine — it is the flow's configuration that conflicts with starting it this way. Keeping 403 for RBACMiddleware lets the UI tell "you lack the permission" apart from "this flow is not hand-startable". It is not 429 either: schedules read that as "at capacity, skip this occurrence".
In the UI the Run button is disabled wherever it appears — the flow card, the flow detail header — with a tooltip saying why, and the new-run form blocks with the same message the API returns.
Admin override
An admin can start one run anyway:
POST /runs?force=trueA caller without the admin role gets 403 for passing force at all, whether or not the flow would have refused the run. A forced run is recorded with manual_run_override: true alongside its trigger_source in the audit log, so the override leaves a trail.
There is deliberately no button for this. A visible "run anyway" turns the guard rail into a speed bump; the escape hatch exists so a subflow stays debuggable without reopening it for everyone.
Scope
It applies to every version. Unlike run limits, the switch is not part of definition_json, so a manual run that pins an older committed version is refused too.
Duplicates inherit it. A copy of a subflow is still a subflow, so Duplicate carries the switch over (unlike the pin, which it does not).
Shared-organization flows carry it to every tenant. A flow published by the shared org arrives with its switch as the shared org set it, which is the point: the shared org owns templates, and a template can say it is a library flow.
Configuration bundles carry it. A flow exported through configuration exchange writes manual_runs_enabled: false only when it is off, so every other flow's file is byte-identical to what it was before this feature. A deployment can therefore arrive with its library flows already closed.
A git pull will not reopen it. Pushing writes the switch to the repository, and importing a flow that does not exist here yet honours what the bundle says. But a pull into a flow already linked here leaves the switch alone, the same way it leaves tags and the pin alone: a pull brings the definition, not this organization's decision about who may start it.