What an AI Agent's Autonomy Level Actually Gates
Autonomy levels are usually an analogy. Here are three real missions at levels 1, 2, and 3, the code that enforces the gate, and what the levels do not do.
An AI agent's autonomy level is a number that decides one thing: whether the agent's next external
action runs on its own or stops for a human. In Vocion, an open-source agent workforce platform,
the level belongs to the mission rather than the agent — autonomyPolicy.level, an integer from 1
to 5, defaulting to 1. Levels 1 (draft only) and 2 (ask before action) gate every task that
produces an external side effect; levels 3 and above let those tasks run unless the task itself
sets approvalRequired. Internal work — analysis, drafting, synthesis — is never gated at any
level. When a task is gated, the mission run pauses in the review queue with a status of
awaiting_review until a person decides.
This is for the engineer who has been handed an "autonomy levels" slide by a security or governance stakeholder and needs to know what the number actually changes at runtime. Most pages on this topic describe the ladder by analogy — usually the SAE self-driving levels — and map each rung to a kind of sign-off authority. None of them show a config file where the level is set or the function the level changes. This one does, using a public workspace with three missions set to three different levels.
Autonomy levels are usually a metaphor. Here is a workspace with three of them
The zero-person-company demo (vocion-demos/demos/zero-person-company/) is a small autonomous
company: six agents on one team, and three standing missions, each with a different
autonomyPolicy.level. It is not a diagram — it is twenty-four checked-in files a reader can
apply against their own database. The three mission files, verbatim:
missions/board-charter-review.yaml:
agent: board
autonomyPolicy:
level: 1
goal: >-
No mission's autonomyPolicy.level is higher than the risk of what it
actually reaches, and the accountable human always has a current
recommendation on what to change.
schedule: '0 14 * * 1'
missions/content-pipeline.yaml:
agent: ceo
autonomyPolicy:
level: 2
goal: >-
Every open brief has a draft or a named blocker, and every draft either
clears the publish-post approval gate or has an explicit reason it is
still waiting.
missions/distribution-outreach.yaml:
agent: distribution
autonomyPolicy:
level: 3
goal: >-
Every approved post has a ready-to-send draft for each relevant channel,
and nothing goes out without a human's own send action.
Same workspace, same core version, three different numbers. That is the whole demo in one sentence: it is checkable, not descriptive.
What the level is attached to
The level is a property of the mission, not the agent. distribution runs the level-3 mission
above, but it is the same agent slug that could run a level-1 mission tomorrow if someone wrote
one for it. The workspace schema makes this explicit — the field lives on the mission entity, not
the agent entity:
autonomyPolicy: z.object({ level: z.number().int().min(1).max(5).default(1) }).default({ level: 1 })
(packages/core/src/libs/workspace/schemas.ts, on the mission object, around line 433.) So the
question is never "what autonomy does this agent have" — it is "what autonomy does this mission
grant this agent for this work." An agent with no mission assigned has no autonomy level at all;
it is just a template.
The five rungs, and what each label means
AUTONOMY_LABELS in packages/core/src/services/missions/autonomy.ts names all five:
| Level | Label |
|---|---|
| 1 | Draft only |
| 2 | Ask before action |
| 3 | Act within rules |
| 4 | Manage a goal |
| 5 | Improve itself |
docs/entities/mission.md describes the same row this way: "How much the mission may do without
asking: 1 draft only, 2 ask before action, 3 act within rules, 4 manage a goal, 5 improve itself.
Levels 1-2 gate every external action; 3+ let them run unless the task flags approval." Read that
sentence again: it already tells you where the line is. Five labels, but the doc itself only
describes two behaviors.
What the level actually gates, in code
One function decides this, and its own docblock calls it out directly: "The single source of
truth for the autonomy gate." From packages/core/src/services/authz.ts:
export function requiresApprovalForMutation(
level: AutonomyLevel,
opts: { external: boolean; approvalRequired?: boolean },
): boolean {
if (opts.approvalRequired) {
return true;
}
if (!opts.external) {
return false;
}
return level <= 2;
}
That is the entire gate. An explicit approvalRequired on a task always wins. Non-external work
never gates, regardless of level. Otherwise the whole decision collapses to one comparison:
level <= 2.
services/missions/autonomy.ts wraps this for mission tasks — taskNeedsApproval(task, level)
just forwards task.type and task.approvalRequired into requiresApprovalForMutation — and its
header comment states the framing plainly: "Autonomy is a property of the mission + action, not
the agent." Here is the finding worth sitting with: five declared levels, one enforcement
boundary, between 2 and 3. Levels 3, 4, and 5 are not distinguished from each other anywhere in
this function. They carry different labels, and getting-started.md describes 4 as "broader
latitude over the goal" and 5 as "plus self-improvement," but the approval decision at those three
levels is identical: run it, unless the task itself asked to be held. This is not a gap we found
by digging through an edge case — docs/entities/mission.md says as much itself ("Levels 1-2 gate
every external action; 3+ let them run unless the task flags approval"), so this piece agrees with
the documentation rather than contradicting it. What changes between 3, 4, and 5 today is not the
gate; it is what the mission's own goal, success criteria, and the tools its agent is given
actually let it reach — which is exactly what the next two sections show in a real file.
What is never gated
Only tasks of type action count as external — EXTERNAL_TYPES = new Set(['action']) in
autonomy.ts. Analysis, drafting, creative work, synthesis, artifacts, and diagnostics are never
auto-gated, at level 1 or level 5. A level-1 mission's agent still reads sources, drafts a brief,
produces a full post — it just cannot send anything, publish anything, or write anything external
without a human clicking approve first. If a stakeholder's concern is "will locking this down to
level 1 stop the agent from thinking," the answer in this code is no: it stops the agent from
acting, not from working.
What happens when the gate fires
executeMissionRun in packages/core/src/services/missions/runtime.ts reads the run's level
(clamped, see below), then walks the task list. When it hits a gated task:
if (taskNeedsApproval(task, level)) {
task.status = 'awaiting_approval';
await patch(runId, {
status: 'awaiting_review',
pauseReason: `awaiting_approval:${task.id}`,
pausedAt: new Date(),
plan: { tasks },
});
return 'awaiting_review';
}
The task becomes awaiting_approval, the run becomes awaiting_review, pauseReason records
which task is blocking, and the function returns. Nothing downstream of that task executes until
a person acts on it. There is no timeout that auto-approves, and no separate "level 6" that skips
the queue.
The three real missions, read side by side
Put the three files next to each other and the pattern is not abstract. board-charter-review is
level 1 and literally never touches anything outside itself — its whole job, per its own goal
line, is checking that "no mission's autonomyPolicy.level is higher than the risk of what it
actually reaches." content-pipeline is level 2 — the CEO's daily queue, drafts and briefs moving,
but every send-shaped action still stops at review. distribution-outreach is level 3 — the one
mission in this workspace where an external action is allowed to run without a per-task ask.
Picking a level for a new mission, from this example, comes down to one question: does this
mission's work ever produce a task of type action, and if it does, are you comfortable with that
action running unsupervised the moment its own approvalRequired flag is unset? If the answer is
no, the mission belongs at 1 or 2, full stop — the label difference between 3, 4, and 5 will not
protect you.
Level 3 does not mean "no human"
This is the part the demo makes concrete instead of asserted. distribution-outreach runs at
level 3, so its action-typed tasks are not gated by the level check. But open the agent it
belongs to, agents/distribution.yaml:
description: >-
Prepares an approved post for the outside world — where it should go and
what to say about it. Never posts to a third-party platform itself.
harness:
provider: local
excludeTools:
- propose_action
distribution has no tool that reaches a third party — propose_action is explicitly excluded
from its harness. The mission's own goal says the same thing in prose: "nothing goes out without a
human's own send action." So level 3 in this workspace means "don't make every draft wait for a
human to ask for it," not "skip the human." The latitude is granted by the level; the reach is
constrained by the agent's tool list, a separate mechanism entirely. If you want a mission to move
faster without also handing it a way to actually publish or send, this is the pattern: raise the
level, and leave the sending tool off the agent.
Autonomy is not the same as trust thresholds
trust.yaml is a second, independent mechanism — a per-action confidence ceiling, not a per-level
one. This workspace's file has two rules:
rules:
- action: dataset.add_example
autoApproveAbove: 0.97
enabled: true
- action: gmail.send
autoApproveAbove: 0.99
enabled: false
dataset.add_example — filing a QA-approved correction into the training set — is low-risk and
reversible, so it is allowed to auto-execute once the system is confident enough, and it stays
audited even when it skips the review queue. gmail.send is listed with a threshold but
enabled: false; the comment in the file says it plainly: this rule "exists to be read, not to
fire." Trust rules answer "how confident does the system need to be before this specific action
can skip review," a finer-grained question than "what level is this mission." The two systems
compose rather than substitute for each other.
What the ladder does not do today
Say this plainly rather than let the five labels imply more than they deliver: levels 3, 4, and 5
are not distinguished from each other by requiresApprovalForMutation. The enforcement code has
one boundary, between 2 and 3. The differences the labels promise at 4 ("manage a goal") and 5
("improve itself") are, as of v2.47.4, expressed only in what a mission's goal and success
criteria ask an agent to do and in which tools that agent is given — never in the approval gate
itself. Level 5 should not be read as self-modifying or self-deploying; it is a label attached to
the same gate behavior as level 3.
Two further limits, both worth stating before anyone builds a compliance argument on top of this
number. First, the level does not restrict what data an agent can read — that is a separate
permissions and scope model, not this one. Second, an out-of-range value is coerced rather than
rejected at the point it is used: clampAutonomyLevel floors a missing or sub-1 value to 1 and
caps anything above 5 at 5, even though the authored workspace schema already validates the file
to the 1-5 range on workspace:apply. Neither of these is a defect to route around; they are
exactly what the code does, and a page that hid them would be less useful than no page.
How to try it
From a vocion-demos clone with the pinned vocion-core submodule installed (the demo's README
has the full command list; the short version):
npm run workspace:check -- demos/zero-person-company/workspace/zero-person-company
npm run workspace:apply -- demos/zero-person-company/workspace/zero-person-company \
--project zero-person-company
Then ./scripts/dev.sh from demos/zero-person-company (port 3005), sign up once through the
dashboard, and re-run workspace:apply so the accountable user resolves. Start a publish-post
run, answer the fact-check step, and watch it stop at review — open /dashboard/review and
nothing downstream runs until a human approves. That pause is requiresApprovalForMutation
returning true, live.
The demo's workspace is pinned to core v2.36.0; the enforcement code quoted above is read from
v2.47.4 on vocion-core main, several releases ahead. The gate rule has not changed between
those two tags.
Related
For the product-level framing of missions and where autonomy fits among their other parts, see missions, which has its own "autonomy ladder" section. And for how this fits into a wider agent workforce rather than a single mission, see what an open-source agent workforce platform needs to include beyond the gate itself.
Clone vocion-demos, apply the zero-person-company workspace, and trigger publish-post to
watch the level-2 gate hold a run for yourself.