tl;dr. tool() from langchain core wraps a
function with a name, description, and zod schema — the anatomy of a
skill, in langchain's wire
format. createAgent turns a model plus tools into a
complete working agent in five lines. one such agent is the unit
everything else in this course composes; get it solid before you hire
more of them.
tools, the langchain way
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// a tool is a function + name + description + schema.
// same anatomy as a skill — different wire format.
const lookupCase = tool(
async ({ caseId }) => {
const c = await db.cases.findById(caseId);
if (!c) return `no case found for id ${caseId}`;
return JSON.stringify({ id: c.id, status: c.status, amount: c.amount });
},
{
name: "lookup_case",
description:
"look up one debt-collection case by its id. " +
"use only when the user names a specific case.",
schema: z.object({
caseId: z.string().describe("the case id, e.g. 'case_8f2'"),
}),
},
); if you took the skills lesson, this is the same object with the labels moved around. everything argued there transfers wholesale: the description is a prompt with a conversion rate, so write when-and-when-not, not just what. the zod schema is your input validation — a hallucinated argument fails parsing before it touches your database. and the return value is a message to the model, so a failed lookup should return a legible sentence the model can act on, not a stack trace.
userId parameter the backend
trusts and you've built confused-deputy-as-a-service — the
security-review
lesson i only had to learn once.
the react agent, one call
import { createAgent } from "langchain";
// a full react loop — model picks tools, tools run, results
// feed back, repeat until the model answers — in one call.
const agent = createAgent({
model: "anthropic:claude-sonnet-5",
tools: [lookupCase, listOverdue],
systemPrompt:
"you are a support agent for a debt-collection product. " +
"answer from tool results only; if a lookup fails, say so.",
});
const out = await agent.invoke({
messages: [{ role: "user", content: "what's the status of case_8f2?" }],
});
// you'll still meet createReactAgent from @langchain/langgraph/prebuilt
// in older code and tutorials — same shape, deprecated since v1.
// createAgent is its successor, plus a middleware system. createAgent — langchain 1.x's front door, built on the
langgraph runtime — is the 80%-case in one call: the model reasons,
requests tool calls, a tool node executes them, results loop back, and
the run ends when the model produces a plain answer. the return value
is itself a compiled graph — you can invoke it, stream it,
or (this matters from
lesson four on)
mount it as a single node inside a bigger graph. it also carries a
middleware system — hook points before and after model and tool calls
for summarization, redaction, approval prompts — which covers a lot of
what used to require dropping down a level.
// createAgent is just this graph — two nodes, one loop:
//
// ┌─────────┐
// START ─► │ agent │ ◄──────────┐
// └────┬────┘ │
// did the model ask │
// for a tool call? │
// ┌───────┴───────┐ │
// ▼ yes ▼ no │
// ┌─────────┐ END │
// │ tools │ ───────────────────┘
// └─────────┘ ToolNode runs the calls,
// appends ToolMessages
//
// when you outgrow it, you rebuild exactly this shape
// with your own nodes — nothing magical to give up. when one call stops being enough
createAgent has one loop shape and one opinion: keep
calling tools until the model stops, with middleware decorating the
edges of that loop. you outgrow it the day the structure of
the cycle has to change — and then you write the two nodes yourself,
which is why the diagram above is worth internalizing:
- a gate inside the loop. validate the draft after every model turn, not just at the end — add a checker node between agent and tools.
- structured state. createAgent's state is messages plus little else. the moment the loop needs a revision counter or a typed verdict — lesson two's whole argument — you need your own annotation.
- real stopping rules. budget, wall-clock, no-progress detection. the one-call agent gives you a recursion limit; production wants a policy.
this is deliberate framework design, and it's the right kind:
createAgent is a starting point that decomposes into the
primitives you already know, not a black box you fight. middleware
handles the decorate-the-loop cases; the graph api handles the
change-the-loop cases. knowing which case you're in is most of the
skill.
when it breaks
- the twenty-tool agent. tool selection quality degrades as the menu grows — every tool is considered on every turn. past a dozen tools you don't need a bigger prompt, you need a team: narrow agents with narrow toolsets, and routing above them.
- tool results that flood the context. a tool returning 400 rows of json spends your context window on noise. summarize, paginate, or return ids the model can drill into. the model reads every byte you return, whether it needed to or not.
- testing only through the model. tools are plain functions — unit-test the handler and schema without an llm in the loop. save the model-in-the-loop budget for what only it can test: whether the description routes correctly. write those cases first.
one competent agent, done. next: the supervisor — what changes when there's more than one, and why the answer is a router with no other job.