Flows
A flow is a saved, multi-step workflow. Each step runs one Breeze tool (for example, running a script on a device), and steps can wait, set values, or repeat over a list of items. A flow runs under its own automation identity rather than a person’s login, so enabling a flow is a delegation decision.
Enabling a flow and what it delegates
Section titled “Enabling a flow and what it delegates”A flow is a draft until someone enables it. Enabling publishes an immutable version of the flow and makes you its delegator: the flow acts with authority you are lending it.
To enable a flow you must:
- Hold the permission for every step in the flow. If any step needs a permission you lack, enabling is refused.
- Have unrestricted access in the flow’s scope. A user limited to specific sites cannot enable a flow, because a flow acts across the whole organization. For a partner-wide flow you need access to all organizations of the partner, with no site restriction.
A flow that is enabled cannot be edited. Edit it while it is a draft, paused, or disabled, then enable it again to publish a new version.
The delegator must stay valid. If the delegator is deactivated, deleted, removed from the flow’s scope, or loses access below the requirements above, the flow pauses. This also applies to runs that are already waiting for an approval. An administrator can re-enable the flow under a new delegator.
Pre-approving a step
Section titled “Pre-approving a step”By default, any step that Breeze classifies as sensitive (Tier 3) stops the run and waits in the Approvals inbox. At enable time you can pre-approve certain steps so scheduled and event-driven runs do not wait for a person.
A step can be pre-approved only when it is a Run script step and all of these hold:
- The script is chosen directly (not looked up from a variable) and its parameters are fixed values.
- It targets exactly one device.
- The script’s approval scope is supervised (not four-eyes).
- The script uses no tenant variables.
- The step is not inside a for-each loop.
The pre-approval is bound to the script’s content. If the script is edited after you enable the flow, the next run detects the change, the flow pauses with the reason binding_drift, and you must review and enable it again.
Runs started by an AI never use pre-approvals. When someone starts a flow from Breeze AI with run_flow, or a run is started with an API key, every sensitive step waits for a human approval. A manual run started from the web interface honors pre-approvals only when the person starting it could run every step themselves (and, for a partner-wide flow, has access to all of the partner’s organizations).
Who can start a run. Starting a run uses the flow’s delegation, which is organization-wide, so Breeze refuses to start a run for anyone limited to specific sites. This includes a person whose membership is site-limited, a token with a site ceiling, and an API key whose creator is site-limited. The start is rejected with delegation_scope_restricted; it does not start without pre-approvals. A run started with an API key is also clamped to the key’s scopes: the key must be allowed to do everything the flow’s steps do.
Enabling, disabling, starting and cancelling a flow all require a session that has completed multi-factor authentication. Without it the request is refused with MFA_REQUIRED.
Approvals
Section titled “Approvals”Sensitive steps that are not pre-approved park the run in awaiting approval and appear in the Approvals inbox. The card shows the flow name and the step, for example “Flow Disk cleanup” and “Step cleanup: run_script”.
- Supervised steps can be approved by an eligible approver, including the flow’s delegator.
- Four-eyes steps exclude the delegator and, for manual runs, the person who started the run. If the only eligible approver is one of those people, the run fails with
no_eligible_approversinstead of approving itself. - Approvals expire after
FLOW_APPROVAL_TTL_HOURS(default 24 hours).
Triggers
Section titled “Triggers”| Trigger | Behavior |
|---|---|
| Manual | Start from the flow page, or from Breeze AI with run_flow. |
| Schedule | Runs at each scheduled time. If runs were missed (for example during an outage), only the latest occurrence runs; there is no backfill. |
| Event | Runs when a matching platform event occurs. |
| Webhook | Runs when an external system sends a signed POST to the flow’s webhook URL. See Webhook trigger. |
Event trigger prerequisites
Section titled “Event trigger prerequisites”Event flows need durable event delivery. Your deployment must set:
EVENT_DISPATCH_MODE=enforceEVENT_DISPATCH_QUEUE_SUBSCRIBERSincludingflow-event-trigger
Without both, enabling an event flow is refused, and event flows that are already enabled are paused when the deployment stops delivering events durably. Event delivery is at-least-once after publication, but an event that fails to be queued at the moment it is published is dropped before any flow sees it.
Flows that are triggered by other flows are limited to a chain depth of 3 to prevent loops.
Webhook trigger
Section titled “Webhook trigger”A webhook flow starts when an outside system sends an HTTP request to the flow’s URL. The URL is shown on the flow’s Webhook tab:
POST /api/v1/webhooks/flows/<flowId>The body must be a JSON object of at most 256 KB (262,144 bytes). The content type must be application/json. Other content types get 415. A larger body gets 413.
Authentication
Section titled “Authentication”You choose one of two modes in the trigger.
| Mode | How the sender proves it knows the secret |
|---|---|
hmac (default) |
Sends X-Breeze-Timestamp (unix seconds or milliseconds) and X-Breeze-Signature: sha256=<hex>. The signature is HMAC-SHA256 over <timestamp>.<raw body>. The timestamp must be within 300 seconds of the server’s clock. |
token |
Sends the secret itself in X-Breeze-Webhook-Token. |
Query-string tokens (?token=) are not supported, because query strings end up in logs.
SECRET="bwh_replace_with_your_secret"BODY='{"customer":"acme","event":"disk_low"}'TS=$(date +%s)SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST "https://api.example.com/api/v1/webhooks/flows/00000000-0000-0000-0000-000000000000" \ -H "Content-Type: application/json" \ -H "X-Breeze-Timestamp: $TS" \ -H "X-Breeze-Signature: sha256=$SIG" \ -H "X-Breeze-Delivery-Id: ticket-1042-attempt-1" \ --data "$BODY"Retries and replays
Section titled “Retries and replays”Send an optional X-Breeze-Delivery-Id (letters, digits and . _ : -, 1 to 128 characters) so that retries of the same delivery run once.
- In
hmacmode, a replayed signature runs once. The repeat gets200with{ "status": "duplicate", "runId": "..." }. - In
tokenmode, only the delivery id dedupes.
The secret
Section titled “The secret”Create or rotate the secret on the flow’s Webhook tab. This needs automations.write and a session with multi-factor authentication. For a partner-wide flow you also need access to all organizations of the partner.
- The secret is shown once. It starts with
bwh_. - Rotating cuts the old secret off immediately. There is no grace period.
- A webhook flow cannot be enabled without a secret.
- If you enable a new version whose draft changed since the secret was created, the secret is cleared and the interface tells you so. Create a new secret and give it to the sender.
Partner-wide webhook flows
Section titled “Partner-wide webhook flows”A partner-wide webhook flow must say which organization each delivery is for. Add an org resolver to the trigger:
{ "orgResolver": { "path": "body.customer", "system": "psa" } }path points at a field in the JSON body. With system, the value is matched against organization external IDs of that external system. Alternatively use { "path": "body.org", "kind": "breeze_org_id" } for a Breeze organization ID. Only live organizations of the flow’s partner can match. If the value matches nothing, Breeze records an org_unresolved delivery, starts no run and answers 422.
Approvals
Section titled “Approvals”Webhook runs never use pre-approvals. Every step that requires approval waits for a person.
Delivery log
Section titled “Delivery log”The Webhook tab lists deliveries with one of these statuses: accepted, org_unresolved, refused, invalid_body.
Requests that fail authentication are never logged as deliveries. The tab shows the reason for the last refused request instead. Users limited to some sites or devices see deliveries without the delivery id and the resolver value.
Non-accepted deliveries are deleted after FLOW_WEBHOOK_DELIVERY_RETENTION_DAYS. The per-flow rate limit is FLOW_WEBHOOK_RATE_PER_MINUTE. See Settings for administrators.
Built-in steps
Section titled “Built-in steps”Besides running a Breeze tool, a step can be one of these built-in kinds. The last three are what a Job becomes when it moves to Flows.
http_request
Section titled “http_request”Calls a URL without credentials. Authenticated calls to a vendor go through Tool Sources (MCP or OpenAPI), not through this step.
- The method and the URL origin (scheme, host and port) must be written out literally. The path, query and body can use templates.
- HTTPS only. Plain
httpis allowed only for private targets, and only on self-hosted deployments withTOOL_SOURCES_ALLOW_PRIVATE_EGRESS=true. - Breeze’s own API origin is refused.
- Redirects are never followed. A
3xxanswer is anhttp_statusfailure. - Timeout up to 30 seconds. Response up to 1 MB (at most 256 KB is stored). Request body up to 256 KB. Up to 100 calls per run.
- These headers cannot be set:
host,content-length,transfer-encoding,connection,upgrade,te,trailer,cookie,authorization,proxy-*,x-forwarded-*,forwardedandx-breeze-*. - Literal headers or query values that look like credentials are refused when you save.
GETandHEADare Tier 2.POST,PUT,PATCHandDELETEare Tier 3 and wait for approval on every run.- Enabling a flow with an
http_requeststep needsexternal_tools.write. - Failures are
egress_blockedandhttp_status.
notify
Section titled “notify”Sends a message to a notification channel: { channelId, title, message, severity? }.
- Tier 2. Needs
alerts.write. - Supported channel types: email, Slack, Teams, SMS, Pushover, webhook and PagerDuty. A PagerDuty notice uses the dedup key
flow:<runId>:<stepId>:<item>; the channel holds the credentials, never the flow. - Up to 20 per run.
- The channel must be visible to the flow’s owner. A partner-wide flow can use only partner-wide channels.
Asks Breeze AI a question and gets structured data back: { prompt, tools?, output, maxTurns?, maxOutputTokens? }.
toolsmay list only Tier 1 (read) tools, up to 20.outputis a JSON Schema of at most 16 KB. The answer must match it.maxTurnsis 1 to 8 (default 4).maxOutputTokensis 256 to 4096 (default 1024).- Usage is billed to the run organization’s AI budget, and each run has an AI token limit (see Monthly limits).
- Failures are
ai_budget_exhausted,ai_output_invalidandai_token_cap_reached. - An organization flow with an
aistep cannot be enabled when the organization has no AI budget to spend. - Calls to a partner’s own catalog endpoint are recorded in the LLM egress audit.
resolve_targets
Section titled “resolve_targets”Finds devices in the run’s organization: { target | conditions | deviceIds | fromStep } (exactly one).
- Tier 1. Needs
devices.read. targetis a device target (all, listed devices, groups or a filter);conditionsis a Job’s condition list;deviceIdsis a list such as['{{trigger.device.id}}'];fromStepis thesucceeded,dispatchedorfaileddevice set of an earlier step.- Resolves once per run. More than 500 devices fails the step with
targets_over_cap; it never truncates.
create_alert
Section titled “create_alert”Raises an alert on one device of the run’s organization: { deviceId, severity, title?, message }.
- Tier 2. Needs
alerts.write. Uses the same alert writer Jobs use. Failure:alert_create_failed.
run_script_on_targets
Section titled “run_script_on_targets”Runs one library script on a device set: { scriptId, parameters?, runAs?, whenOffline, target | conditions | deviceIds | fromStep }.
- Tier 3, so each run waits for approval unless the step is pre-approved (below). Needs the
run_scriptpermission. whenOffline: queue(default) queues the script for an offline device; a queued dispatch counts as success, as it did for Jobs.skipleaves offline devices out.- The output carries
dispatchedDeviceIds,succeededDeviceIdsandfailedDeviceIds, so a later step can act only on the devices that succeeded. runAs: elevatedis refused, and so is a script that runs elevated by default.- More than 500 devices fails with
targets_over_cap. Other failures:targets_unresolvable,script_dispatch_failed. - Pre-approval with a device cap. When you enable the flow you can pre-approve this step with a cap of 1 to 500 devices. Schedule, event and UI runs then dispatch without waiting; webhook and tool runs always wait. The approval is bound to the script content, parameters, run context, offline policy and target definition. Any change, or more devices than the cap, pauses the flow with
binding_driftbefore anything runs. The cap applies per run in each organization, so for an all-orgs flow the total can be the cap times the number of organizations. A step under a for-each can’t be pre-approved. - When a run waits for approval, the approval names the exact devices, and only those run when it is approved.
For-each concurrency
Section titled “For-each concurrency”A for-each normally processes items one at a time. Set forEach.concurrency above 1 to run items in parallel. This works only for steps of Tier 2 or lower that are not wait steps.
- Effective parallelism is the smaller of
concurrencyandFLOW_FOREACH_MAX_PARALLEL. - Outputs stay in item order.
- With
onFailure: stop, Breeze stops launching new items after a failure.
Run statuses
Section titled “Run statuses”| Status | Meaning |
|---|---|
| Queued | Waiting for a free slot. |
| Running | Executing steps. |
| Waiting | Parked on a wait step; it resumes by itself. |
| Awaiting approval | Parked on a sensitive step until someone decides. |
| Completed / Failed / Cancelled | Finished. |
| Expired | An approval the run was waiting on expired before anyone decided. |
A step can also end unknown outcome: a step that changes things timed out or lost its executor, so Breeze cannot tell whether it took effect. The run fails; check the target before re-running.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Steps per flow | 50 |
| For-each items | 500, one at a time unless the step sets forEach.concurrency |
| Active time per run | 1 hour (time spent waiting or awaiting approval does not count) |
| Active runs per owner | 1,000 |
| Running runs per owner | 20 by default (FLOW_MAX_RUNNING_PER_OWNER) |
| Running runs per flow per organization | 1 |
| Flow-triggers-flow chain depth | 3 |
| Run detail retention | Inputs, outputs and payloads are cleared after 90 days by default (FLOW_RUN_RETENTION_DAYS); the run record and step history remain |
Monthly limits
Section titled “Monthly limits”Each owner, flow and AI step has a cap. Defaults and platform ceilings:
| Limit | Default | Ceiling |
|---|---|---|
| Runs per flow per month | 10,000 | 100,000 |
| Runs per owner per month | 100,000 | 1,000,000 |
| AI tokens per run | 100,000 | 1,000,000 |
The owner is the organization (all its flows) or, for partner-wide flows, the partner. Months are UTC calendar months. A flow’s document can set limits.monthlyRuns lower still.
Set limits on Settings > Flow limits (/settings/flow-limits):
- A partner default needs
automations.write, multi-factor authentication and access to all organizations of the partner. - An organization override can only lower the partner value. The server refuses a raise.
When a cap is reached, new runs are refused with flow_monthly_cap_reached or owner_monthly_cap_reached. Webhook and manual runs get HTTP 429. Schedule and event triggers drop the refused run and do not retry. The flow is not paused. Its page shows a banner and the flow list shows a “Runs refused” badge. One notice per flow, month and reason goes to the person the flow runs for, and to the flow’s notice channel if it has one.
The same limits are available over the API:
GET /api/v1/flows/limits(optionally?orgId=) returns the effective values.PUT /api/v1/flows/limitswith{ "orgId"?, "limits": { "monthlyRunsPerOwner"?, "monthlyRunsPerFlow"?, "aiTokensPerRun"? } }sets them. Anullvalue clears a limit so it inherits again.
Using flows from Breeze AI
Section titled “Using flows from Breeze AI”Four AI tools work with flows:
run_flowstarts a flow. It always requires a human approval tap, and it is not available to MCP clients.list_flow_runsandget_flow_runread run history and the step ledger.cancel_flow_runcancels an active run. People using Breeze AI can call it directly; autonomous AI agents cannot use it.
There is no AI tool for enabling a flow. Enabling is the delegation decision and stays a person’s action in the web interface.
Flows as tools
Section titled “Flows as tools”An enabled flow with a manual trigger is also a tool named flow__<slug>. It appears in Breeze AI chat, in Breeze’s MCP server and to AI agents. Its tier is the flow’s live tier: the higher of the recorded tier and the tier recomputed now.
- Chat: Tier 1 runs directly, Tier 2 asks for confirmation, Tier 3 goes through approval.
- MCP: Tier 3 flows are never listed or callable. Tier 2 flows need an
ai:writekey. - Agents: only flows named in the agent’s tool allowlist, only Tier 1 flows, and only in agent runs that are not limited to a device or site.
A session can use at most 20 flow tools. A flow is never a step of another flow. To chain flows, use the flow.run.completed event.
Settings for administrators
Section titled “Settings for administrators”| Setting | Default | Purpose |
|---|---|---|
FLOWS_ENABLED |
on | Turns the Flows feature on (default). false turns it off: flow routes answer 404 and Jobs stay fully writable. |
FLOW_APPROVAL_TTL_HOURS |
24 | How long a parked step waits for approval. |
FLOW_MAX_RUNNING_PER_OWNER |
20 | Concurrent running runs per owner. |
FLOW_RUN_RETENTION_DAYS |
90 | Age at which run bodies are cleared. |
FLOW_WEBHOOK_RATE_PER_MINUTE |
120 | Inbound webhook deliveries accepted per flow per minute (1-6000); over it the request gets a 429. |
FLOW_WEBHOOK_DELIVERY_RETENTION_DAYS |
30 | Days a webhook delivery that was not accepted (refused, org unresolved, invalid body) is kept (1-365); accepted deliveries live as long as their run. |
FLOW_FOREACH_MAX_PARALLEL |
5 | Most for-each items one flow step runs at once (1-10); a step’s own forEach.concurrency is capped by this. |
EVENT_DISPATCH_MODE / EVENT_DISPATCH_QUEUE_SUBSCRIBERS |
see above | Required for event triggers. |