Control Plane
Projects, run modes, 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 and the spend cap are operator-owned, and every refusal below is named.
Two independent brakes, answering two 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 spend cap | Is there allowance left this period? | a higher cap, or the next billing period |
Both 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. (A third, time-based "approval brake" existed until 2026-09-13; it
paused sandbox projects on a hidden clock and is gone. A paused project is a
project whose operator chose paused.)
{ "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 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.