Control Plane
Projects, run modes, the approval brake, and the spend cap — the operator controls that decide whether a run may start, and the named refusals when one does not.
Everything IonWarp does happens inside a project, and a project carries the controls that decide whether autonomous work may start at all. Read them before you spend a run — every one of them is a value you can fetch, not a decision you have to recompute.
Every gate here is a property of the project and the object being acted on, never of which door the request came through — the dashboard and the API get the same answer to the same request. The one deliberate asymmetry runs the safe way: an agent cannot grant itself authority. Run modes, the approval brake, and the spend cap are all operator-owned, and every refusal below is named.
Three independent brakes, answering three different questions. Confusing them is the most common way an external operator either over-reaches or reports a working system as an outage.
| Brake | Question it answers | Cleared by |
|---|---|---|
run_mode | What may this project do to the outside world? | an explicit operator command |
| the approval brake | Is a human still in the loop? | an operator approving or declining any draft |
| the spend cap | Is there allowance left this period? | a higher cap, or the next billing period |
All three are read, never computed. There is exactly one server-side implementation of each, and a second one written into a prompt drifts on its first edit.
{ "name": "projects_get", "arguments": { "project_id": "pr_acme" } }curl "$DASH_BASE_URL/api/v1/projects/pr_acme" \
-H "Authorization: Bearer pat_..."Run mode — what this project may do to the outside world
run_mode is one string on the project row, and the ladder it names is declared
by the agent, not by the platform: an agent lists its rungs, what each
permits, and its default. An agent that declares nothing gets the platform
ladder — running (everything) and paused (nothing), defaulting to running.
IonWarp may declare more rungs than that; read the accepted values back from
the project's run-mode control, or from the refusal, which names them.
Each rung sets four capability axes:
| Axis | Permits |
|---|---|
scheduled | scheduled and autonomous runs may start at all |
draft_preload | real provider writes whose target is proven draft/inactive first |
control | start, resume, activate |
send | broadcast or outbound send |
Resolution is an allowlist and fail-closed: a rung must be declared and
mark an axis true. A mode the agent does not declare — a typo, a legacy row, a
rung that was removed — resolves with every axis off. Absence of evidence never
authorizes spend.
The gate applies to autonomous launches only. A human starting a run interactively on a paused project still runs; a cron, a planner child, or a PAT client acting on its own does not.
| code | fires when |
|---|---|
project_paused | the project's mode is declared but does not permit scheduled runs. Change the run mode to schedule work again. |
project_run_mode_unreadable | the project row could not be read, or holds a mode this agent does not declare. |
Who may change it
projects_update changes run_mode — and a task run may never call it, in
either direction. Raising or lowering a project's own autonomy is an operator
act, so a call made from inside an AI turn is refused with
run_mode_change_requires_operator. The project's run-mode control in the
product, and an operator's own external MCP client, may change it; the model may
not. An unrecognised value is refused with the accepted list named back to you.
{ "name": "projects_update", "arguments": { "project_id": "pr_acme", "run_mode": "paused" } }- Never flip
run_modeas a side effect, and read it back after any change. Absence of an instruction never authorizes autonomy. - Never change
run_modeon a scheduled sweep. A sweep reports; an operator decides. If a change is warranted, recommend it and name the command.
The approval brake
Autonomous work spends on an operator's authority, and that authority
expires. Where IonWarp's product routes work through operator-reviewed
drafts, a project may start autonomous runs for 48 hours after the last real
operator approval; after that its agents stop until someone approves or declines
something again. Only an operator action stamps that clock — a run can never
extend its own spending authority, and the brake never resumes anything on its
own. Where IonWarp has no draft-review loop the brake does not apply at all and
spending reads true; the run-mode and spend-cap gates still do.
Every project read returns the decision verbatim, so poll it rather than waiting for the refusal:
| code | fires when |
|---|---|
project_awaiting_approval | no operator approval inside the window. Not an incident — approve or decline anything and runs resume on the next tick. |
project_approval_unreadable | the approval clock could not be read. The brake fail-closes, because starting a run is spending. |
The brake gates spend, not truth: when spending is false, reads,
reporting, and timeline writes are untouched. Never compute the window yourself,
and never try to clear it — only an operator action does. An operator-grade PAT
could change a run mode; doing so to resume spend would defeat the brake.
Reporting a closed brake
- The resume line comes FIRST and is not optional. A report that says
"paused" without the one action that un-pauses it reads as an outage, and the
operator's next move is to debug a system that is working correctly. Take the
wording from
reason— the server has already phrased it for a human. - Report the full numbers. Hiding metrics behind a closed brake turns it into a blindfold: the one surface an operator has for deciding whether to re-approve then shows nothing to decide on. The paused notice is a tag, never a replacement for the content.
- A skipped action is named, never silent — say which work did not run.
- A closed brake is never a failure. Not an incident, not a stall, not something to escalate, and never evidence about the quality of an agent that has not run because the project was paused.
Why the brake exists
Measured across four projects with zero outbound sends and zero operator approvals in the review window: $31.28 of reported 7-day spend, 46% of it burned by runs that FAILED — while the planners kept planning, the agents kept drafting into review queues nobody had opened in days, and the schedulers kept spawning follow-on work for all of it. Nothing there was broken. The system simply had no way to notice that no human was on the other end.
The spend cap
Every workspace has exactly one effective credit cap per period: its plan's
included allowance, or a custom budget amount if one is set. Autonomous runs
stop at that cap by default; switching the budget to keep going removes the
ceiling. The refusal is always spend_cap_reached, and its envelope carries
cap_source — included_allowance or custom_cap — naming which ceiling fired,
plus used, amount, and the remedy. Unreadable billing evidence admits the
run: a veto path never blocks on missing evidence. Read spend with
usage_summary (Usage).
What a refusal looks like
A control-plane refusal writes no run row. It is returned inline on the
create call, the task moves to a terminal status, and it gains one visible
timeline note naming the code and the remedy — never a silent stop and never a
retry loop. All of these codes appear on a run as error_json.code; see
Debugging a Run to read one, and
Reference / Errors for the REST envelope's own code
vocabulary.