Advanced · Lesson 1 of 6 · 20 min
AI agents and harnesses
What makes a system an agent, how the tool-calling loop actually runs, why the harness around the model decides whether it works, and when you should not build an agent at all.
Workflows, agents and the line between them
Anthropic draws a useful line. Workflows are systems where models and tools are orchestrated through predefined code paths. Agents are systems where the model directs its own process and tool usage, deciding what to do next based on what it has seen so far.
A lead-intake flow is a workflow: a form fires, one model call classifies the lead, your code writes the tag to GoHighLevel and assigns an owner. You can draw the flowchart in advance. A support agent that investigates a disputed refund is different: it may check the order, then the shipping carrier, then the refund policy, then the customer's history, in an order nobody can fix ahead of time.
Both are useful. The mistake is building the second when the problem is the first. Every decision you hand to the model is a decision you can no longer guarantee, test exhaustively or explain to a client in one sentence.
The loop: tool_use in, tool_result out
Underneath every agent framework is the same loop. You send the model a task and a list of tools, each with a name, a description and a JSON schema for its input. If the model wants a tool, the response stops with stop_reason set to tool_use and contains one or more tool_use blocks. Your code runs each tool and sends the output back as tool_result blocks in the next user message. You repeat until the model answers without asking for a tool.
Three details separate a demo from a production loop. First, when a tool fails, return the error as a tool_result with is_error set to true instead of throwing; the model can often retry with corrected input or explain the failure to the user. Second, cap the number of turns, because a confused model will happily call the same tool forever. Third, treat any stop reason other than tool_use as the end of the run, and log which one it was: max_tokens means the answer was cut off, and refusal means the model declined.
The sample below is a complete loop against the Claude Messages API. The model id comes from the site's model registry, so it stays current.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY from the environment
const MAX_TURNS = 8;
const tools: Anthropic.Tool[] = [
{
name: "get_order",
description: "Look up one order by ID. Returns status, total and ship date. Read-only.",
input_schema: {
type: "object",
properties: { order_id: { type: "string", description: "Order ID, e.g. ORD-1042" } },
required: ["order_id"],
},
},
];
// fetchOrder is your own API client; it throws when the order does not exist.
async function runTool(name: string, input: unknown): Promise<string> {
if (name === "get_order") return JSON.stringify(await fetchOrder(input));
throw new Error("Unknown tool: " + name);
}
export async function runAgent(task: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [{ role: "user", content: task }];
for (let turn = 0; turn < MAX_TURNS; turn++) {
const response = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 2048,
system: "You are a support agent. Check facts with tools before answering.",
tools,
messages,
});
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason !== "tool_use") {
return response.content.flatMap((block) => (block.type === "text" ? [block.text] : [])).join(" ");
}
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type !== "tool_use") continue;
try {
results.push({ type: "tool_result", tool_use_id: block.id, content: await runTool(block.name, block.input) });
} catch (error) {
// Hand the failure back so the model can recover, instead of crashing the run.
results.push({ type: "tool_result", tool_use_id: block.id, content: String(error), is_error: true });
}
}
messages.push({ role: "user", content: results });
}
throw new Error("Agent stopped after " + MAX_TURNS + " turns without finishing");
}The harness is most of the product
The harness is everything around the model: the loop, tool execution, permissions, context management, retries, logging and stopping rules. The same model inside a careless harness and a careful one behaves like two different products. Claude Code, the Claude Agent SDK, Anthropic's Managed Agents beta and n8n's AI Agent node are all harnesses; they differ in how much of this they decide for you.
Tool design is the part teams underinvest in. Anthropic calls it the agent-computer interface and reports that, while building its SWE-bench agent, it spent more time optimizing tools than the overall prompt. Write tool descriptions the way you would brief a new hire, and change arguments so mistakes are hard to make: require an absolute ID rather than a fuzzy name, return a short summary rather than a 50 KB JSON dump, and say in the description whether the tool is read-only.
Typical harness failures look like model failures but are not: a tool that returns an entire contact list floods the context and the model loses the task; two tools with near-identical names get confused; a missing turn cap turns one bad input into a four-figure API bill overnight.
When not to use an agent
Anthropic's advice is to find the simplest solution possible and only add complexity when it is needed, which can mean not building an agentic system at all. Agents trade latency and cost for better performance on open-ended tasks. In Anthropic's own data, agents use about four times the tokens of a chat interaction, and their autonomy brings the risk of compounding errors.
Use a fixed workflow when you can draw the steps in advance, when volume is high and margins are thin, or when every run must be auditable step by step. Use an agent when the path genuinely depends on what is discovered along the way and a human would otherwise do the investigating. Even then, keep irreversible actions such as refunds, deletions and outbound messages behind an approval step until you have evidence the agent gets them right.
A common hybrid works well in business automation: a deterministic workflow in n8n, Make or GoHighLevel handles the trigger, validation and writes, and calls an agent only for the one step that needs judgment, such as researching an account or drafting a reply for review.
Key points
- ◆Workflows follow predefined code paths; agents choose their own next step. Default to the workflow.
- ◆The agent loop is: model requests a tool, your code runs it, you return a tool_result, repeat until it stops asking.
- ◆Return tool failures with is_error so the model can recover, and always cap turns.
- ◆The harness (tools, permissions, context, stopping rules) matters as much as the model; design tools so misuse is hard.
- ◆Agents cost more and add latency; reserve them for paths you cannot specify in advance.
Terms in this lesson
Try it · exercise
Build the loop from this lesson with one fake tool and watch it handle a success, a tool failure and a task that needs no tool.
- Copy the loop and replace fetchOrder with an in-memory map holding two orders; throw an Error for any other ID.
- Log each turn: the stop_reason, every tool name and input requested, and whether each tool_result had is_error set.
- Run three tasks: ask about an order that exists, ask about one that does not, and ask a general question about your returns policy.
- Set MAX_TURNS to 1 and rerun the first task to see the hard stop fire.
You're done when: Your log shows tool_use followed by a tool_result and a final answer for the existing order; an is_error result and an answer saying the order was not found for the missing one; zero tool calls for the general question; and the turn-limit error when MAX_TURNS is 1.
