← All posts

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.

Vocion Teamvocion-v2.47.4

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:

LevelLabel
1Draft only
2Ask before action
3Act within rules
4Manage a goal
5Improve 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.

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.