Skip to content

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:

http
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 ​

TriggerStarted byAffected
manualA person via the UI Run button or POST /runsRefused
run_nowA person pressing Run now on a scheduleAllowed
scheduleThe scheduler, on the schedule's cadenceAllowed
webhookAn inbound call to a webhook endpointAllowed
flowA parent flow's flow.run stepAllowed

"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:

text
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:

http
POST /runs?force=true

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

Released as open source under the AGPL-3.0-or-later license. Development is sponsored by Rexonix s.r.o.. Contact — [email protected].