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 belongs to the project and the object being acted on, so the dashboard and the API get the same answer to the same request. Run modes and the spend cap are set by the operator: an agent running a task cannot change its own project's run mode. Every refusal below is named.
Two separate controls answer two different questions:
| Control | 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 |
Read the run mode from the project:
{ "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 |
A rung permits an axis only when the agent declares that rung and marks the
axis true. A mode the agent does not declare — a typo, an old value, a rung
that was removed — permits nothing.
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. If IonWarp cannot read your billing
state, the run starts. 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.