Your first agent demo can feel magical. It reads a prompt, calls a tool, writes a useful answer, and makes you think the hard part is done.
Then you try to ship it.
Suddenly the real questions show up: Where does the run live? What happens when the model times out? Can two users start the same workflow twice? How do you prove the agent used the right source, stayed inside budget, and did not silently fail halfway through?
This runbook is for that gap between “it worked on my laptop” and “users can safely rely on it.” We will turn a local AI agent into a reliable API-backed workflow with queues, idempotency, health checks, budget limits, evals, audit logs, and release gates.
The goal is not to make agents fully autonomous. The goal is to make them boring enough to operate.
Why Agent Deployment Breaks After the Demo
Most tutorials show the happy path:
- Define a task.
- Pick a framework.
- Add tools.
- Run the agent.
- Print the answer.
That is useful, but production has different failure modes.
A deployed agent must survive:
- slow model responses
- flaky tools
- duplicate requests
- user cancellations
- partial progress
- bad retrieved context
- token spikes
- schema drift
- unsafe tool arguments
- provider outages
- background jobs that never finish
Recent AI tooling trends make this more urgent. Agentic systems are moving from chat boxes into real workflows. Builders are connecting models to GitHub, Discord, email, CRMs, databases, browser sessions, and paid APIs. New products are also leaning into review-first behavior: the agent proposes, explains, and waits before touching real user data.
That is the right direction. But it only works if the deployment layer is designed for reliability from day one.
Architecture: The Minimum Reliable Agent System
For a serious agent workflow, avoid treating the model call as the whole product. Use a small system of parts:
Client
|
v
API server -----> Run database
| |
v v
Job queue ------> Worker process
| |
v v
Tool gateway ---> Audit log / traces
|
v
Model provider / local model
Each component has one job:
- API server: accepts requests, validates auth, creates runs, returns status.
- Run database: stores state, input hash, owner, budget, progress, and result.
- Job queue: keeps long work out of request timeouts.
- Worker: executes the agent step by step.
- Tool gateway: enforces permissions, schemas, rate limits, and logging.
- Audit log: records what happened, why, and under whose authority.
You can build this with FastAPI, Express, Django, Rails, or any stack you already use. The pattern matters more than the framework.
Step 1: Define the Agent Contract Before the Prompt
Before writing prompts, define the contract.
An agent contract says:
- what input the agent accepts
- what output shape it must return
- which tools it may use
- what it must never do
- how long it may run
- how much it may spend
- when it must ask for approval
- what evidence it must attach
Example contract:
{
"agent": "support_issue_triage",
"input": {
"issue_id": "string",
"source": "github|discord|email"
},
"output": {
"summary": "string",
"likely_duplicate_ids": ["string"],
"suggested_next_step": "string",
"confidence": "low|medium|high",
"evidence_urls": ["string"]
},
"limits": {
"max_model_calls": 8,
"max_tool_calls": 12,
"max_runtime_seconds": 180,
"requires_approval_for": ["send_reply", "close_issue", "edit_record"]
}
}
This contract becomes your shared truth across prompts, tests, API docs, monitoring, and review.
Step 2: Make the API Asynchronous by Default
Do not run meaningful agent work inside a normal HTTP request. It will time out, retry badly, or block your server under load.
Use this pattern instead:
- Client sends a request.
- API validates it.
- API creates an
agent_runrow. - API enqueues a job.
- Client polls or subscribes to updates.
- Worker writes progress and final result.
A simple Python shape:
from fastapi import FastAPI, Depends
from pydantic import BaseModel
from uuid import uuid4
app = FastAPI()
class StartRunRequest(BaseModel):
issue_id: str
source: str
@app.post("/agent-runs")
def start_agent_run(req: StartRunRequest, user=Depends(current_user)):
run_id = str(uuid4())
run = {
"id": run_id,
"user_id": user.id,
"status": "queued",
"input": req.model_dump(),
"budget": {
"max_model_calls": 8,
"max_tool_calls": 12,
"max_runtime_seconds": 180
}
}
save_run(run)
enqueue_job("run_support_triage_agent", run_id=run_id)
return {
"run_id": run_id,
"status": "queued",
"status_url": f"/agent-runs/{run_id}"
}
This small design choice prevents a lot of pain. Users get a run ID. Your system gets a durable object to monitor, cancel, retry, inspect, and audit.
Step 3: Add Idempotency Before Users Double-Click
Users refresh pages. Browsers retry. Mobile networks fail. Webhooks redeliver. If the same request can create two live agent runs, you will eventually create duplicate work or duplicate writes.
Add an idempotency key.
import hashlib
import json
def input_hash(user_id: str, payload: dict) -> str:
raw = json.dumps(payload, sort_keys=True)
return hashlib.sha256(f"{user_id}:{raw}".encode()).hexdigest()
@app.post("/agent-runs")
def start_agent_run(req: StartRunRequest, user=Depends(current_user)):
key = input_hash(user.id, req.model_dump())
existing = find_recent_run_by_key(user.id, key)
if existing and existing["status"] in ["queued", "running", "completed"]:
return {
"run_id": existing["id"],
"status": existing["status"],
"deduped": True
}
run = create_run(user_id=user.id, input=req.model_dump(), idempotency_key=key)
enqueue_job("run_support_triage_agent", run_id=run["id"])
return {"run_id": run["id"], "status": "queued"}
For write actions, idempotency is not optional. It is the difference between “the agent retried safely” and “the agent charged the customer twice.”
Step 4: Store Run State Like a Product Feature
An agent run should not be a blob of logs. Store structured state.
Minimum useful fields:
agent_runs
- id
- tenant_id
- user_id
- agent_name
- agent_version
- status: queued | running | waiting_for_approval | completed | failed | canceled
- input_json
- output_json
- error_code
- error_message
- idempotency_key
- model_calls_used
- tool_calls_used
- estimated_cost_cents
- started_at
- completed_at
- canceled_at
Then store steps separately:
agent_run_steps
- id
- run_id
- step_index
- step_type: model_call | tool_call | approval | validation | final
- name
- input_summary
- output_summary
- status
- latency_ms
- cost_cents
- created_at
This gives you enough detail to answer the questions users and engineers actually ask:
- Why did this run fail?
- Which step was slow?
- Did the agent call the right tool?
- Did it use customer A’s context in customer B’s run?
- Did a retry repeat a dangerous action?
Step 5: Put Every Tool Behind a Gateway
The fastest way to create agent risk is to let the model call tools directly without a runtime policy layer.
Instead, create a tool gateway. The worker asks the gateway to execute a tool. The gateway checks:
- Is this tool allowed for this agent?
- Is it allowed for this user and tenant?
- Are the arguments valid?
- Did the model supply fields that must come from trusted server state?
- Is the action read-only, write, paid, or destructive?
- Does this action require approval?
- Is the run still inside budget?
Example policy shape:
TOOL_POLICY = {
"search_issues": {"risk": "low", "approval": False},
"read_issue": {"risk": "low", "approval": False},
"draft_reply": {"risk": "medium", "approval": False},
"send_reply": {"risk": "high", "approval": True},
"close_issue": {"risk": "high", "approval": True}
}
def execute_tool(run, tool_name, args):
policy = TOOL_POLICY[tool_name]
assert_run_budget(run)
validate_tool_args(tool_name, args)
enforce_tenant_scope(run.tenant_id, args)
if policy["approval"]:
pause_for_approval(run.id, tool_name, args)
return {"status": "waiting_for_approval"}
result = call_tool(tool_name, args)
log_tool_call(run.id, tool_name, args, result, policy)
return result
Prompts can request safe behavior. Gateways enforce it.
Step 6: Build a Budget That Stops Runaway Work
A reliable agent has hard limits. Not vibes. Not “please be concise.” Real counters.
Track at least:
- model calls per run
- tool calls per run
- total tokens
- estimated cost
- retry count
- wall-clock runtime
- output size
- external API calls
When a budget is reached, fail gracefully:
{
"status": "failed",
"error_code": "BUDGET_EXCEEDED",
"message": "The agent stopped after 8 model calls. It saved partial findings and did not perform any write actions."
}
This is better than a worker spinning for 30 minutes while your bill climbs.
Step 7: Add Health Checks That Test the Whole Path
A basic /health endpoint only proves your server is awake. Agent systems need deeper checks.
Use three levels:
1. Liveness check
Is the process running?
GET /health/live -> 200 OK
2. Readiness check
Can the service reach its dependencies?
GET /health/ready
- database: ok
- queue: ok
- model_provider: ok
- tool_gateway: ok
3. Synthetic agent check
Can a tiny safe agent run complete?
Run a scheduled test that:
- creates a fake run
- uses a mock or low-cost model path
- calls a safe read-only tool
- validates structured output
- records latency and cost
This catches problems a normal health check misses, such as broken credentials, schema mismatches, or a changed model response format.
Step 8: Block Releases With Workflow Evals
Unit tests are not enough. You need evals that test the workflow.
Create fixtures for real failure cases:
| Eval case | What it catches |
|---|---|
| Duplicate issue with different wording | weak retrieval and matching |
| Tool timeout | missing retry/fallback behavior |
| Malicious content in a document | prompt injection exposure |
| Missing source | hallucinated evidence |
| Budget limit reached | graceful stop behavior |
| High-risk action requested | approval gate enforcement |
A simple eval result should include:
{
"case_id": "duplicate_issue_low_keyword_overlap",
"passed": true,
"scores": {
"correct_duplicate_found": 1,
"evidence_attached": 1,
"no_write_action": 1,
"within_budget": 1
}
}
Do not deploy a new prompt, model, tool, or retrieval setting unless core evals pass.
Step 9: Separate Draft, Approval, and Execution
Many production agents should not directly perform the final action. They should draft it.
Use this pattern:
Agent investigates -> Agent drafts action -> Human or policy approves -> System executes
For example:
- draft a support reply, but do not send it
- suggest closing an issue, but do not close it
- prepare a database update, but do not run it
- recommend a refund, but do not issue it
This does not make the product weaker. It makes it usable in higher-stakes workflows.
Approval records should store:
- proposed action
- arguments
- evidence
- risk level
- reviewer
- approval or rejection
- timestamp
- final executed action ID
That record becomes gold when a customer asks, “Why did the agent do this?”
Step 10: Design Failure Messages Users Can Act On
A failed agent run should not end with Something went wrong.
Useful failure output includes:
- what failed
- whether anything was changed
- what evidence was saved
- whether retry is safe
- what the user can do next
Example:
{
"status": "failed",
"error_code": "TOOL_TIMEOUT",
"message": "The agent could not read the issue tracker before the timeout. No replies were sent and no issues were modified.",
"retry_safe": true,
"partial_result": {
"sources_checked": ["discord"],
"sources_missing": ["github"]
}
}
This builds trust. Users do not need perfection. They need clear boundaries.
Step 11: Version Prompts, Tools, and Models Together
If you cannot tell which prompt and model produced an output, debugging becomes guesswork.
Version these together:
agent_version: support_triage_v4
prompt_version: triage_prompt_2026_07_28
model: selected_by_router
tool_policy_version: support_tools_v3
retrieval_config_version: issue_search_v5
When a run fails, you can compare it against previous versions. When a new model improves one case but breaks another, you can roll back cleanly.
Deployment Checklist
Before you expose an agent workflow to real users, confirm this list:
- [ ] The agent has a written contract.
- [ ] Requests create durable run records.
- [ ] Long work runs in a worker, not the request thread.
- [ ] Duplicate requests are idempotent.
- [ ] Each run has model, tool, runtime, and cost budgets.
- [ ] Tools go through a policy gateway.
- [ ] Tenant and user scope are enforced server-side.
- [ ] Risky actions pause for approval.
- [ ] Progress is streamed from durable state.
- [ ] Failures are user-readable and retry-aware.
- [ ] Workflow evals block unsafe releases.
- [ ] Prompts, tools, models, and retrieval configs are versioned.
- [ ] Audit logs can explain what happened.
- [ ] Synthetic checks test the full agent path.
If this feels like more work than the demo, it is. But it is much less work than cleaning up a runaway workflow after users trust it.
Real-World Use Cases
This runbook fits support triage, customer research, internal operations, and developer workflow automation. In each case, the safe pattern is the same: investigate, draft, attach evidence, enforce scope, and ask for approval before changing customer data or external systems.
Final Takeaway
A local agent demo proves the model can do the task once. A deployed agent system proves your product can handle the task repeatedly, safely, and with evidence.
The difference is not one magic framework. It is the runbook around the agent: durable state, queues, budgets, tool policy, approval gates, evals, health checks, and audit logs.
Ship the agent when you can answer this question without opening a terminal:
What happened in this run, what did it cost, what did it touch, and is it safe to retry?
If your system can answer that, you are much closer to a reliable AI product.
FAQ
What is an AI agent deployment runbook?
An AI agent deployment runbook is a practical operating plan for moving an agent from a local prototype to a reliable production workflow. It covers API design, queues, run state, tool permissions, budgets, monitoring, evals, approvals, and failure handling.
Should an AI agent API be synchronous or asynchronous?
Most agent APIs should be asynchronous. The API should create a run, enqueue work, and return a run ID. The client can poll, subscribe to events, or receive webhooks. This avoids request timeouts and makes retries safer.
What is the biggest mistake when deploying AI agents?
The biggest mistake is treating the model call as the product. Production agents need durable state, scoped tools, budgets, evals, logs, and clear failure behavior. Without that layer, small issues become expensive incidents.
How do you stop AI agents from taking unsafe actions?
Put every tool behind a policy gateway. Classify tools by risk, validate arguments, enforce tenant scope, limit budgets, and require approval for high-risk actions such as sending messages, changing records, deleting data, or spending money.
How do you monitor a production AI agent?
Track run status, step latency, model calls, tool calls, cost, retries, failures, approval waits, output validation errors, and user feedback. Add synthetic checks that run a tiny safe workflow on a schedule to catch broken dependencies.
How often should agent evals run?
Run fast workflow evals on every prompt, model, retrieval, or tool-policy change. Run a larger suite before major releases. Keep adding cases from real incidents and user corrections.
Top comments (0)