How to Build an AI Agent
A working agent is a loop, a set of tools, and a test suite. The loop takes an afternoon. The test suite is what decides whether the thing is still running in six months.
Updated 27 July 2026
What you are actually building
Strip away the framework names and an agent is one while loop. You send the model a goal and a list of tools. It replies either with an answer or with a request to call a tool. You run the tool, append the result to the conversation, and send the whole thing back. Repeat until the model stops asking for tools or you hit a ceiling you set.
That is the entire pattern. Every agent framework in existence is this loop plus opinions about state, retries, and observability. Understanding the loop first means you can judge those opinions instead of inheriting them.
// agent.ts
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const MAX_TURNS = 8;
export async function run(goal: string) {
const messages: Anthropic.MessageParam[] = [{ role: 'user', content: goal }];
for (let turn = 0; turn < MAX_TURNS; turn++) {
const res = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 2048,
system: SYSTEM_PROMPT,
tools: [lookupOrder, issueRefund],
messages,
});
// Always append the assistant turn verbatim. Editing it breaks the
// tool_use / tool_result pairing on the next request.
messages.push({ role: 'assistant', content: res.content });
if (res.stop_reason !== 'tool_use') {
return textOf(res); // the model answered instead of calling a tool
}
const results = [];
for (const block of res.content) {
if (block.type !== 'tool_use') continue;
results.push({
type: 'tool_result',
tool_use_id: block.id,
...(await callTool(block.name, block.input)),
});
}
messages.push({ role: 'user', content: results });
}
throw new Error('Turn ceiling reached without a final answer');
}Three details in that code matter more than they look. The assistant turn is appended verbatim, because the API pairs each tool_use block with a tool_result that references its id. Tool results are sent back in a user turn, which surprises people the first time. And MAX_TURNS exists because a confused agent will happily call the same tool forty times. Set the ceiling low and raise it when a real trace justifies it.
An agent is a loop with a budget. Most production incidents are a missing budget.
Define a narrow job
Write the job in one sentence, with a subject, a verb, and a boundary. “Triage inbound support email, tag it, and draft a reply for a human to send.” That is a job. “Handle support” is a wish.
The failure mode here is the everything-agent. It gets twenty tools, a system prompt of nine paragraphs, and a task description that changes based on what the user typed. It works in the demo, because the demo has one path. In production the tool descriptions start competing with each other, the model picks the wrong one on inputs nobody anticipated, and you cannot debug it because there is no expected behaviour to compare against.
Narrow scope buys you three things. You can enumerate the tools, so you can reason about permissions. You can write down what correct looks like, so you can build an eval set. And you can put a hard turn ceiling on the loop, because you know roughly how many steps the job takes.
Two narrow agents that hand off to each other beat one wide agent nearly every time. They are separately testable, separately deployable, and when one drifts you know which one.
Choose your build path
There are three honest paths, and the right one depends on how unusual your control flow is, not on how technical your team is.
| Path | You write | Pick it when | What it costs you |
|---|---|---|---|
| No-code platform | Configuration: tools, prompts, routing, gates | The job is a recognisable shape and you want evals, traces, and deploys included | Unusual control flow is awkward inside a canvas |
| Framework (LangGraph, CrewAI) | Graph or crew definitions in Python or TypeScript | You need custom control flow, branching, or long-running state machines | Evals, observability, failover, and governance are yours to build and staff |
| Raw provider API | The loop above, and everything around it | The agent is small, or you are learning, or you need zero dependencies | You will rebuild retries, tracing, and cost accounting by hand |
The common mistake runs in one direction. Teams reach for a framework because agents feel like an engineering problem, then spend the next quarter building an eval harness and a trace viewer rather than improving the agent. If your control flow is a trigger, a few tool calls, and an approval gate, that quarter bought you nothing.
The opposite mistake is real too. Once your agent needs to fork, wait on external events for days, or coordinate several sub-agents with shared state, a visual builder will fight you. Move.
Swfte Studio is the no-code path here, with the production layer built in rather than bolted on. AI agent builder covers what to look for in that category generally.
Connect tools and data
Tools are where agents earn their keep and where they do their damage. A tool definition is a name, a description aimed squarely at the model, and a JSON Schema for the arguments. The description is prompt engineering, not documentation. It should say when to call the tool, when not to, and what the tool returns.
// tools.ts
// A tool is three things: a name, a description written for the model,
// and a JSON Schema the model must fill in.
export const lookupOrder = {
name: 'lookup_order',
description:
'Fetch one order by ID. Use this whenever the customer references an ' +
'order number. Returns status, line items, and shipping events. ' +
'Does not modify anything.',
input_schema: {
type: 'object',
properties: {
order_id: { type: 'string', description: 'Order ID, e.g. ORD-10423' },
},
required: ['order_id'],
},
} as const;
export const issueRefund = {
name: 'issue_refund',
description:
'Refund an order, in full or in part. IRREVERSIBLE. Only call after ' +
'lookup_order confirms the order is refundable and the amount is ' +
'at or below the order total.',
input_schema: {
type: 'object',
properties: {
order_id: { type: 'string' },
amount_cents: { type: 'integer', minimum: 1 },
reason: { type: 'string', enum: ['damaged', 'late', 'duplicate', 'other'] },
},
required: ['order_id', 'amount_cents', 'reason'],
},
} as const;Notice the split between a read tool and a write tool, and the shouted warning on the irreversible one. Models respect that more than you would expect, and it costs nothing. It is still not a permission system. Enforce the real limit in callTool, where an amount above the order total is rejected regardless of what the model asked for.
Data arrives three ways. Triggers push work in, and webhooks are the most common trigger because they fire at the moment the event happens. Retrieval pulls context in at run time, which becomes agentic RAG once the agent decides for itself what to search for. And MCP lets you attach an existing catalogue of tools through one protocol instead of hand-writing an adapter per system.
Keep the tool list short. Somewhere past a dozen tools, selection accuracy starts to slide, and the fix is usually splitting the agent rather than rewriting descriptions.
Pick models per step
One model for the whole agent is a default, not a decision. Agents are multi-step, and the steps have wildly different difficulty. Deciding which of three tools to call is easy. Extracting six fields from an invoice is easy. Working out why a customer with two overlapping orders is angry is not.
Route accordingly: small fast models for classification, extraction, and formatting, frontier models for planning and the one step that carries the judgement. The cost difference between model tiers is roughly an order of magnitude, and a multi-step agent multiplies that difference by its turn count.
| Step type | Model class | Why |
|---|---|---|
| Intent classification, routing | Small | Short output, verifiable against labels, runs on every request |
| Field extraction from documents | Small to mid | Schema-constrained, catchable by a validator |
| Planning, tool selection under ambiguity | Frontier | Errors here cascade through every later step |
| Final customer-facing draft | Mid to frontier | Tone and accuracy are what the human reviewer judges |
| Summarising a run for compaction | Small | High volume, low stakes, easy to check |
Doing this against provider SDKs directly means an if-statement per step and a rewrite when a model is deprecated. A gateway makes it configuration. Swfte Connect fronts 50+ providers behind one API with per-step routing, automatic failover, and cost telemetry per call, so switching a step is a config change and a provider outage degrades one step instead of killing the run. Estimate the arithmetic first with the token cost calculator.
Test with evals before you trust it
This is the step people skip, and it is the difference between an agent and a demo. An agent has no exit code. It fails by returning something plausible and wrong, and the run goes green.
A golden set is thirty to fifty real inputs pulled from actual traffic, each labelled with the tools that should have been called, the tools that must never be called, and a known-good answer. Invented test cases are worse than none, because they encode your assumptions about the input distribution rather than the distribution itself.
// evals/run.ts: this file is the reason the agent is allowed in production.
import cases from './golden.json'; // 30-50 real inputs, hand-labelled
for (const c of cases) {
const trace = await runAgent(c.input);
record({
id: c.id,
toolsCalled: trace.toolCalls.map((t) => t.name),
toolMatch: sameSet(trace.toolCalls.map((t) => t.name), c.expectedTools),
forbiddenCalled: trace.toolCalls.some((t) => c.forbiddenTools.includes(t.name)),
answerScore: await judge(c.expectedAnswer, trace.finalAnswer), // 0-1
turns: trace.turns,
costUsd: trace.costUsd,
});
}
// Ship gate: forbiddenCalled must be 0 across the whole set,
// toolMatch above your agreed floor, mean answerScore not below last release.
assertNoRegression(loadBaseline(), currentResults());Score the trajectory, not just the answer. An agent that reached the right conclusion after calling the refund tool twice is broken, and an answer-only score will pass it. The forbidden-tool check is the one assertion that should be absolute: zero violations, no exceptions, or the release does not ship.
Run the set before every prompt edit, every tool change, and every model swap. Then run it on a schedule anyway, because providers update and deprecate models on their own timetable and your agent can regress without a single commit from you. Studio ships golden sets, regression runs, guardrail checks, and versioned releases as part of the builder, which mostly means this stops being a side project nobody owns.
Deploy, observe, iterate
Agents are stateful in a way that catches teams out. The conversation is working memory: expensive, volatile, and capped by the context window. Anything that must survive a compaction or a restart belongs in durable storage next to it.
type AgentRun = {
runId: string; // one per triggered run, on every log line
goal: string;
messages: Anthropic.MessageParam[]; // working memory, expensive, volatile
facts: Record<string, string>; // durable memory, survives compaction
toolCalls: { name: string; ok: boolean; ms: number }[];
costUsd: number;
};
// Long runs outgrow the window. Keep the head and the recent tail verbatim,
// summarise the middle, and never let a summary replace a confirmed fact.
function compact(run: AgentRun): AgentRun {
if (estimateTokens(run.messages) < 120_000) return run;
const head = run.messages.slice(0, 2);
const tail = run.messages.slice(-6);
const digest = summarise(run.messages.slice(2, -6));
return { ...run, messages: [...head, { role: 'user', content: digest }, ...tail] };
}Errors deserve the same care. The instinct is to throw when a tool fails. Do not. Hand the failure back to the model as a tool result flagged as an error, and it will usually recover: fix the argument, try the other tool, or explain that it cannot proceed. Reserve exceptions for the loop itself.
// Failures are inputs to the loop, not exceptions that escape it.
async function callTool(name: string, input: unknown) {
const impl = TOOLS[name];
if (!impl) {
return { content: 'No such tool: ' + name, is_error: true };
}
const parsed = SCHEMAS[name].safeParse(input);
if (!parsed.success) {
// Validate server side too. The schema is a strong hint, not a contract.
return { content: 'Invalid arguments: ' + parsed.error.message, is_error: true };
}
try {
return { content: JSON.stringify(await withRetry(() => impl(parsed.data))) };
} catch (err) {
return { content: 'Tool failed: ' + describe(err), is_error: true };
}
}
// Retry transport faults. Never retry a write that may have landed.
async function withRetry<T>(fn: () => Promise<T>, tries = 3): Promise<T> {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (err) {
if (i >= tries - 1 || !isTransient(err)) throw err;
await sleep(250 * 2 ** i + Math.random() * 100);
}
}
}Once it is live, one trace per run with the full prompt, response, model version, tool arguments, and token count per step. Aggregate logs will not do. When an agent does something strange you need to replay the exact turn that produced it, and “step 4 completed” tells you nothing. Alert on turn count and cost per run, not just errors, because a degrading agent gets chattier before it gets wrong.
Keep the approval gate on irreversible actions until your eval data earns its removal. Version prompts and tool definitions like code, with a rollback that takes seconds. Then iterate: read failed traces weekly, add each new failure mode to the golden set, tighten a description, rerun. That loop is the actual work.
Worked example: inbound email triage
A support inbox is the standard first agent for good reasons. Volume is high, the input is text nobody could parse with rules, and a human already reads the output.
- 01
Write the job down
Read one inbound email, classify it into your existing tag set, pull the order ID if present, look up the order, and draft a reply. It does not send. It does not refund. Two sentences, and every noun in them maps to something you can test. - 02
Wire the trigger and the deterministic steps
Inbox webhook to normaliser: strip quoted history, drop auto-replies, deduplicate on message id. Every field you can parse without a model is a field the model cannot get wrong and you are not paying for. - 03
Add tools, read-only first
lookup_order and search_help_centre. No writes on day one. Give the agent a turn ceiling of six and watch what it does with a hundred real emails before adding anything. - 04
Build the golden set from those hundred
Label forty of them: correct tag, correct order ID, tools that should have been called, an acceptable draft. Include the ugly ones. Forwarded threads, three orders in one email, no order at all. - 05
Route the steps
Classification and extraction on a small model. Drafting on a mid or frontier model, since a human is judging the tone. Rerun the eval set after each change and keep the score. - 06
Add guardrails, then the write path
The draft must not contain a link to a domain you do not own, must not promise a refund amount, must name the order it references. Only once those hold across the set does draft_reply become send_reply, and only for the tag with the lowest risk. - 07
Ship behind a gate and read the traces
A human presses send. Every week, pull the runs where the human rewrote the draft heavily and add them to the golden set. That is your roadmap, and it is more reliable than any of your guesses about what to improve.
Common questions
- Can I build an AI agent without coding?
- Yes. No-code platforms like Swfte Studio let you assemble the goal, tools, integrations, and model choices in a visual builder and deploy without writing code. Frameworks like LangGraph or CrewAI are the code-first equivalent when you need full control.
- How long does it take to build an AI agent?
- A useful single-purpose agent is typically a same-day build on a no-code platform once you know the job and have access to the tools it needs. Most of the real time goes into testing and tightening its behavior, not initial assembly.
- What model should my agent use?
- Usually more than one. Route routine steps such as classification, extraction, and formatting to fast, cheap models, and reserve frontier models for planning and hard reasoning. A multi-model gateway lets you change this per step without rewriting the agent.
- How do I stop my agent from making mistakes?
- Constrain it: narrow job, explicit tool permissions, guardrail checks on outputs, human approval on irreversible actions, and an eval set of real cases you rerun before every change. Agents fail quietly without regression testing.
- What's the difference between building an agent and building a chatbot?
- A chatbot only needs a model and a prompt. An agent additionally needs tools it can call, a loop for multi-step work, and much stronger testing, because it acts on systems rather than just replying.
Related reading
- AI agent builderThe no-code path in detail
- Best AI agentsWhat is already built, by category
- Agentic AIThe concepts underneath
- MCP gatewayTool access as a managed layer
- Agentic RAGWhen the agent controls retrieval
- What is a webhookThe most common agent trigger
- Swfte StudioBuild, test, and deploy agents
- Swfte ConnectOne API across 50+ providers
- TemplatesWorking agents you can fork