Imagine an AI agent is reviewing a page before publication.
It decides the page is ready, calls publish_page, and the request succeeds. But the response times out.
The agent cannot tell whether publication happened, so it tries again.
Now imagine something else changed between those attempts: the page was edited, the user's permissions changed, or an approval was revoked.
The problem is no longer whether the model made a reasonable decision. The problem is that model intent has been allowed to become a real-world side effect without enough deterministic control around it.
That is why AI agents that can change external systems need an execution boundary.
The model should produce intent. Application code should control authority and side effects.
A useful architecture looks like this:
AI reasoning
↓
Structured action proposal
↓
Execution boundary
↓
External system
The execution boundary is not just a human-approval step. It is the layer responsible for validation, authorization, policy enforcement, workflow state, approvals, idempotency, retries, execution, auditing, and verification.
Why direct tool execution becomes risky
A simple agent prototype often looks like this:
User request
↓
LLM
↓
Tool call
↓
External system
The model decides that publish_page is appropriate, and the application immediately performs the action.
That is convenient during prototyping, but several different responsibilities have now been collapsed into one decision.
Before a page is actually published, the system may still need to ask:
- Is
publish_pagea supported operation? - Is the payload valid?
- Is this user allowed to publish this page?
- Is the page still in a publishable state?
- Does publication require approval?
- Is that approval still valid?
- Has this operation already run?
- Can it be retried safely?
- Did publication actually produce the intended result?
Those are not questions the model should resolve by itself.
They belong to deterministic application logic.
Put an execution boundary between intent and action
Instead of allowing the agent to invoke privileged side effects directly, let it propose an action.
For example:
{
"type": "publish_page",
"resourceId": "page_284",
"reason": "draft_ready_for_publication",
"operationId": "op_8f219"
}
This object represents what the agent wants to happen.
It does not prove that the action is allowed.
A safer flow is:
Agent proposal
↓
Schema validation
↓
Authorization
↓
Policy evaluation
↓
Approval if required
↓
Pre-execution validation
↓
Execution
↓
Verification
↓
Audit record
That middle layer is the execution boundary.
The model remains useful for reasoning. The application remains responsible for deciding whether the proposed action may affect the outside world.
Use structured proposals instead of free-form instructions
Free-form output is difficult to validate reliably.
A bounded action vocabulary gives the application something predictable to inspect:
type AgentAction =
| {
type: "publish_page";
resourceId: string;
operationId: string;
}
| {
type: "create_ticket";
projectId: string;
title: string;
operationId: string;
};
This example is illustrative rather than production-tested, but the design principle matters.
The agent can select from known actions. It cannot invent a privileged operation simply by describing one convincingly.
Application code can then own the control path:
async function processAgentAction(
action: AgentAction,
actor: Actor
) {
validateSchema(action);
await authorize(actor, action);
await enforcePolicy(action);
if (requiresApproval(action)) {
return queueForApproval(action, actor);
}
return executeIdempotently(action, actor);
}
The important separation is:
The model produces intent. The application grants authority.
That makes the boundary easier to test, audit, and reason about.
Authorization and policy belong outside the model
Suppose the agent proposes:
{
"type": "publish_page",
"resourceId": "page_284",
"operationId": "op_8f219"
}
The application still needs to decide whether publication is allowed.
That may depend on:
- the authenticated user;
- the user's role;
- ownership of
page_284; - the current environment;
- the page's workflow state;
- publishing policy;
- risk level;
- approval requirements.
An agent may understand how publishing works without having authority to publish.
That distinction is useful across agent roles.
A research agent might be allowed to search and summarize. An editing agent might be allowed to create a draft. A publishing agent might be allowed to request publication.
request publication does not have to mean publish immediately.
Capability and permission are separate concerns.
Make approval part of the workflow state
If an action needs human approval, approval should be represented in application state rather than only as an instruction in a prompt.
For a publishing workflow, the states might look like this:
PROPOSED
↓
VALIDATED
↓
AWAITING_APPROVAL
↓
APPROVED
↓
EXECUTING
↓
VERIFYING
↓
COMPLETED
Failure or exception states might include:
REJECTED
VALIDATION_FAILED
EXECUTION_FAILED
RETRY_PENDING
CANCELLED
This gives the application explicit answers to operational questions:
- Which actions are waiting for approval?
- Which exact proposal was approved?
- Who approved it?
- Can it still be cancelled?
- What should happen after execution fails?
- Which state transitions are legal?
It also prevents "approval required" from becoming a vague behavioral suggestion to the model.
Recheck important assumptions before execution
Approval itself should not automatically make an action safe forever.
Imagine page_284 is approved for publication at 10:00.
Before the executor runs, one of these things changes:
- the page is edited;
- the user loses publishing permission;
- the resource is deleted;
- policy changes;
- the approval is revoked;
- the approved version no longer matches the current version.
The executor should not assume that an earlier approval proves the current action is still valid.
For important side effects, recheck authorization, relevant state, and approval validity immediately before execution.
An approval should authorize a specific action under specific conditions, not create unlimited future authority.
Make retries safe with idempotency
Now return to the original publishing example.
The executor sends a request to publish page_284.
The request succeeds.
The response times out.
From the application's point of view, the result is ambiguous.
Retrying blindly may cause the same side effect twice.
That is where idempotency becomes part of the execution boundary.
The proposal may contain an operation identifier:
{
"type": "publish_page",
"resourceId": "page_284",
"operationId": "op_8f219"
}
Before execution, the application checks whether op_8f219 has already completed or is already in progress.
There is one important detail here: an identifier should not automatically become trustworthy just because the model supplied it.
Ideally, trusted orchestration or application code should create the operationId, or at minimum validate it before use.
The model should not control the transactional identity of privileged operations without checks.
Once the operation is tracked by trusted code, the execution layer can safely handle:
- duplicate requests;
- retries;
- timeouts;
- in-progress operations;
- completed operations;
- recovery or compensation logic.
The model does not need to remember transactional state. The system does.
Verify the outcome, not just the API call
Even successful execution is not necessarily the end of the workflow.
Suppose the publishing API returns 200 OK.
Does that prove the new page is live?
Not always.
The request may have been accepted while a downstream process later fails. A public page may still display an older cached version. A deployment may start successfully but fail during rollout.
For meaningful side effects, verification should be explicit.
A workflow record might contain:
- proposed action;
- validation result;
- authorization result;
- approval record;
- operation ID;
- execution response;
- verification result;
- final state.
That gives COMPLETED a stronger meaning.
It means the system observed the intended result, not merely that it sent a request.
Test the execution boundary without the LLM
Separating reasoning from execution also makes the system much easier to test.
The executor should be testable independently from the model.
You can submit:
- a valid authorized proposal;
- a malformed proposal;
- an unsupported action;
- an unauthorized action;
- an action missing required approval;
- an action whose approval became stale;
- a duplicate operation ID;
- an execution failure;
- a verification failure.
Then assert the expected result and state transition.
For example:
Unauthorized proposal
→ rejected before execution
Approved but stale proposal
→ returned for revalidation
Duplicate operation
→ not executed twice
Execution succeeds, verification fails
→ not marked COMPLETED
The reasoning layer can remain probabilistic.
The side-effect controls do not have to be.
Implementation checklist
Before allowing an AI agent to change a real system, check:
- Does the agent propose actions instead of receiving unrestricted execution access?
- Is every action represented by a narrow, validated schema?
- Is the action vocabulary explicitly bounded?
- Is authorization enforced outside the model?
- Are policy checks deterministic where possible?
- Are approval requirements represented in workflow state?
- Is the approved action tied to the correct resource and version?
- Are authorization and relevant state rechecked before important execution?
- Are operation IDs created or validated by trusted application code?
- Are retries protected by idempotency?
- Are failures observable and recoverable?
- Is the external result independently verified where necessary?
- Is enough audit data recorded to reconstruct what happened?
- Can the execution layer be tested without invoking the model?
The core idea is simple:
Let the model decide what it wants to do. Let deterministic application logic decide whether that action is valid, authorized, approved, safe to execute, and actually completed.
That execution boundary is what turns tool use into a controlled system rather than a direct path from model output to real-world side effects.
Where would you place that boundary in your current agent architecture?
AI disclosure: This article was created with AI assistance. The human author is responsible for validating the technical content before publication.
Top comments (0)