iTranslated by AI
3 Boundaries to Define Before Deploying AI Agents to Production
Introduction
When incorporating LLMs or AI agents into business systems, the primary discussion often revolves around model accuracy. You likely fine-tune prompts to ensure that high-precision responses are obtained consistently.
While accuracy is certainly important, what is truly frightening about production deployment is not just the possibility of AI making mistakes.
It is even more dangerous when automation progresses while it remains ambiguous where suggestions end and actual real-world changes begin.
For example, consider the following cases:
- AI summarizes inquiries
- AI generates reply drafts
- AI sends replies
- AI changes customer status
- AI changes contracts, billing, permissions, inventory, or user states
These are often lumped together under "using AI," but in terms of system design, they represent completely different areas of responsibility.
An AI that only summarizes should not be treated within the same boundary as an AI that modifies external states.
In this article, I will introduce three boundaries that you should separate before putting AI agents into production:
support-onlyreview-onlyeffect-bearing
These are not complex theories but practical classifications for implementing LLM applications safely.
The Three Boundaries
First, we divide AI involvement into the following three stages:
| Boundary | What it does | Modifies external state? | Examples |
|---|---|---|---|
support-only |
Assistance, summarization, classification, suggestion generation | No | Summaries, tag suggestions, reply drafts |
review-only |
Proposals assuming human verification | No (not directly) | Pending replies, change requests |
effect-bearing |
Actually modifies state | Yes | Sending emails, DB updates, granting permissions |
What matters is not the intelligence of the AI, but to which effect boundary its output is connected.
Even with the same LLM output, if it is only displayed on the screen, it is support-only.
If a human verifies it before sending, it is review-only.
If it triggers an API to perform an actual action, it is effect-bearing.
Fuzziness regarding this distinction leads to production incidents.
support-only: Only Providing Support
support-only is a domain where AI does not modify any external states.
Examples of this usage include:
- Summarizing inquiry text
- Extracting discussion points from long minutes
- Generating classification candidates for GitHub issues
- Organizing the atmosphere of Slack threads
- Creating drafts for replies
At this stage, the AI is merely creating "material for decision-making."
It does not change the state of users or the system.
type Boundary = "support-only" | "review-only" | "effect-bearing";
type SupportOnlyResult = {
boundary: "support-only";
summary: string;
suggestions: string[];
confidence: "low" | "medium" | "high";
};
The benefit of support-only is that the damage is relatively small if it fails.
Of course, inaccurate summaries or classifications are problematic, but because it has not yet changed any external state, humans can review and stop it.
In this boundary, you mainly care about the following:
- Is it truly reading the input?
- Is uncertainty displayed in the output?
- Might the user mistake AI output for fact?
- Does it automatically proceed to the next stage?
The point is not to connect support-only output directly to an effect. In many cases, early AI features are implicitly placed in this stage.
review-only: Requiring Human Review
review-only is a domain where the AI creates proposals, but execution requires human verification.
Examples of this usage include:
- AI creating email reply drafts
- AI providing revision suggestions for contract reviews
- AI creating requests for user permission changes
- AI proposing steps for incident response
- AI providing candidates for refund decisions
At this stage, the AI can go as far as making a "proposal." However, it cannot execute it.
type ReviewOnlyRequest = {
boundary: "review-only";
proposed_action: {
type: string;
payload: unknown;
};
reason: string;
required_reviewer_role: string;
};
The key to review-only is to make human review a mandatory system condition, not just a UI formality.
For example, the following implementation is dangerous:
// Bad example
if (aiResponse.looksGood) {
await sendEmail(aiResponse.email);
}
This looks like a review, but in reality, the AI is doing the sending.
If you make it review-only, you should at least ensure it cannot be executed unless there is a review record, as shown below:
type ReviewRecord = {
review_id: string;
request_id: string;
reviewer_id: string;
reviewer_role: string;
decision: "approved" | "rejected";
reviewed_at: string;
};
function canExecuteReviewedAction(
review: ReviewRecord,
requestId: string,
requiredReviewerRole: string
): boolean {
return (
review.request_id === requestId &&
review.decision === "approved" &&
review.reviewer_role === requiredReviewerRole
);
}
What is important here is not treating the review record in isolation.
"Someone approved something" is not enough. If it is not linked to which request is being approved, the review record could be misused for a different proposal.
The essence of review-only is not just stopping the AI's output, but converting the AI's output into a request that can be verified before execution.
The caveat here is not to execute review-only proposals directly. Approved proposals should be converted into separate effect-bearing execution requests before passing through an execution gate.
At least in the field, there seem to be more operations where humans implicitly check support-only output than cases where these boundaries are explicitly designed.
effect-bearing: Changing External State
effect-bearing is the domain where external states are actually modified.
Examples include the following processes:
- Sending emails
- Posting to Slack
- Closing GitHub issues
- Changing DB statuses
- Granting permissions to users
- Executing processes affecting billing, refunds, contracts, or inventory
From this point forward, it is not enough for the AI to just generate text.
Reality is changed.
Therefore, effect-bearing requires at least the following information:
- Who approved the execution?
- What is being changed?
- What input/evidence is it based on?
- What was the state before the change?
- Can it be reverted if it fails?
- If it cannot be reverted, what compensation measures exist?
- When and under which version of the rules was it executed?
The minimal schema looks like this:
type EffectType =
| "send_email"
| "post_slack_message"
| "update_ticket_status"
| "grant_access"
| "issue_refund";
type Reversibility = "reversible" | "compensatable" | "irreversible";
type EffectRequest = {
request_id: string;
boundary: "effect-bearing";
effect_type: EffectType;
idempotency_key: string;
requested_by: string;
payload: unknown;
evidence_refs: string[];
review_id: string;
reversibility: Reversibility;
};
effect-bearing does not mean you shouldn't let AI handle it.
However, processes entering this domain should be treated as auditable execution requests rather than regular function calls.
Common Pitfall: Thinking It's support-only When It's Actually effect-bearing
A common dangerous pattern in AI implementation is when something that starts as support-only evolves into effect-bearing without anyone noticing.
For example, it might start like this:
Summarize the inquiry and suggest a reply
This is support-only.
Then, because it's convenient, it becomes:
Draft a reply and have a staff member review and send it
This is review-only.
Further, for efficiency, it becomes:
Automatically reply under certain conditions
This is effect-bearing.
If you advance the implementation without changing the design boundaries here, accidents will happen.
This is because the prompts, logs, UI, permissions, and tests created under the assumption of support-only cannot handle the responsibilities of effect-bearing.
What makes adding AI functionality dangerous is not that the model suddenly becomes smarter.
It is that the boundary has changed, but the design has not.
Enforcing Boundaries with Code
Writing boundaries in documentation is not enough.
You need to implement them in a way that can be checked at runtime.
Below is a simple TypeScript example.
type Boundary = "support-only" | "review-only" | "effect-bearing";
type ActionRequest = {
id: string;
boundary: Boundary;
action_type: string;
payload: unknown;
evidence_refs: string[];
review_id?: string;
required_reviewer_role?: string;
idempotency_key?: string;
requested_by: string;
};
type ReviewRecord = {
review_id: string;
request_id: string;
reviewer_id: string;
reviewer_role: string;
decision: "approved" | "rejected";
reviewed_at: string;
};
type ExecutionDecision = { ok: true } | { ok: false; reason: string };
function canExecute(
request: ActionRequest,
review?: ReviewRecord,
): ExecutionDecision {
if (request.boundary === "support-only") {
return {
ok: false,
reason: "support-only actions must not execute external effects",
};
}
if (request.boundary === "review-only") {
return {
ok: false,
reason:
"review-only actions require conversion to effect-bearing after approval",
};
}
if (request.boundary === "effect-bearing") {
if (!request.evidence_refs.length) {
return {
ok: false,
reason: "effect-bearing actions require evidence_refs",
};
}
if (!request.review_id) {
return {
ok: false,
reason: "effect-bearing actions require review_id",
};
}
if (!review || review.review_id !== request.review_id) {
return {
ok: false,
reason: "matching review record not found",
};
}
if (review.request_id !== request.id) {
return {
ok: false,
reason: "review record does not belong to this request",
};
}
if (review.decision !== "approved") {
return {
ok: false,
reason: "review is not approved",
};
}
if (
request.required_reviewer_role &&
review.reviewer_role !== request.required_reviewer_role
) {
return {
ok: false,
reason: "reviewer role does not satisfy required_reviewer_role",
};
}
if (!request.idempotency_key) {
return {
ok: false,
reason: "effect-bearing actions require idempotency_key",
};
}
return { ok: true };
}
return {
ok: false,
reason: "unknown boundary",
};
}
In this example, support-only or review-only actions cannot be executed directly.
To execute as effect-bearing, at least the following are required:
evidence_refsreview_id- A matching
ReviewRecord - An
approveddecision - Fulfillment of the required reviewer role
idempotency_key
Even just this can prevent a significant portion of accidents.
Leave an Effect Record
Once an effect-bearing action is executed, record the result.
This is not just a log, but a record that allows you to trace back "why this change occurred."
type EffectRecord = {
effect_id: string;
request_id: string;
idempotency_key: string;
effect_type: string;
executed_at: string;
executed_by: string;
input_refs: string[];
evidence_refs: string[];
review_id: string;
result: "succeeded" | "failed" | "unknown";
rollback_ref?: string;
compensation_ref?: string;
};
For example, if you sent an email, you record the following:
{
"effect_id": "eff_001",
"request_id": "req_123",
"idempotency_key": "idem_customer_support_req_123_send_email",
"effect_type": "send_email",
"executed_at": "2026-04-29T10:00:00+09:00",
"executed_by": "workflow_customer_support_v1",
"input_refs": ["ticket_456"],
"evidence_refs": ["summary_789", "policy_refund_v2"],
"review_id": "rev_999",
"result": "succeeded"
}
With this, you can verify later:
- What input did it start from?
- What was the rationale?
- Who approved it?
- Which process was executed?
- Did it succeed or fail?
Simply logging the AI output is not enough.
You need to record the point where the AI output connected to real-world effects.
Distinguish Between Rollbackable and Non-rollbackable Actions
Not all effects are created equal.
For example, a DB status update might be reversible. However, sending an external email cannot be fully undone.
Therefore, we introduce reversibility for effect-bearing.
type Reversibility = "reversible" | "compensatable" | "irreversible";
Here is what each means:
| Type | Meaning | Example |
|---|---|---|
reversible |
Nearly fully reversible | Label changes, revoking permission grants |
compensatable |
Not fully reversible, but compensatable | Corrective notification after an erroneous one |
irreversible |
Cannot be undone | Sending external emails, final legal/financial processing |
Even just looking at this before execution will change your design.
type GateLevel =
| "policy-gated"
| "human-review-required"
| "do-not-auto-execute";
function gateLevelFor(reversibility: Reversibility): GateLevel {
if (reversibility === "reversible") {
return "policy-gated";
}
if (reversibility === "compensatable") {
return "human-review-required";
}
return "do-not-auto-execute";
}
-
irreversibleeffects should, in principle, not be auto-executed. -
compensatableeffects should have compensation means prepared in advance. -
reversibleeffects should still have audit trails and rollback paths.
Even rules at this level make AI automation significantly safer.
The point here is not that "if it's reversible, it's fine to auto-execute everything."
Even if there is a possibility for auto-execution, it is limited to cases that pass a policy gate and retain audit trails and rollback paths.
Show Boundaries in the UI
Boundaries should be exposed not only in the backend but also in the UI.
For example, display labels like the following in the AI output area:
Mode: support-only
This output is a suggestion. It cannot execute external actions.
Mode: review-only
This proposed action requires human approval before execution.
Mode: effect-bearing
This action will change external state if approved and executed.
This display is understated but important.
Users often forget while looking at AI output whether it is a "suggestion" or an "execution."
Using the same boundaries on the screen, during execution, and in logs will reduce confusion.
Minimal Configuration Proposal
There is no need to build a large governance system from the start.
You can start with at least these four components:
1. ActionRequest
2. ReviewRecord
3. EffectRecord
4. canExecute gate
For something a bit more thorough, a configuration like this:
LLM Output
↓
ActionRequest
↓
Boundary Gate
↓
ReviewRecord
↓
Execution Gate
↓
EffectRecord
The important thing here is not to connect the AI output directly to an API.
Always drop it into an ActionRequest first and determine the boundary.
Conclusion
Before putting an AI agent into production, the first thing you should categorize is not the type of model.
It is how far the AI is allowed to impact reality.
In this article, we divided it into three levels:
-
support-only- Summarization, classification, and candidate generation only
-
review-only- Proposals requiring human review
-
effect-bearing- Execution that changes external states
Just by separating these three, the design of AI automation becomes much clearer.
Especially important is not letting features built as support-only quietly become effect-bearing.
If AI output is connected to real-world changes, it requires at least:
- Evidence
- Approval
- Effect type
- Execution record
- Rollback or compensation
- Explicit boundaries
The safety of AI applications is not determined by the model's intelligence alone.
Where does support end, and where does execution begin?
Expressing that boundary explicitly in code is the first step toward production deployment.
Supplementary Note: On the Worldview of Rollback
The classification in this article is based on the worldview from my previous post: "The Worlds of Distributed Systems" (in Japanese).
In particular, when thinking about effect-bearing processes, one must consider separately: "Can it be failed safely in a closed world?", "Can it be reversed via compensation or retries?", or "Does it remain as history and only allow for corrections, refunds, or explanations?"
In this article, I have adapted that discussion for LLM / AI agents, rephrasing it as the boundaries of support-only / review-only / effect-bearing.
Discussion