scalable/ai/langgraph agent teams/lesson 03 cph / /
lesson 03 / 08 · 12 min · updated ·

tools and the react agent

tool() plus zod, createAgent's react loop, and when one call stops being enough. one agent, working, before any team exists.

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

a tool — name, description, schema, handler
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.

the host side stays host-side. notice what the schema does not include: the tenant id, the db client, the auth token. the model supplies semantic arguments; the environment arrives via closure (or a config object), never via the model. give the model a 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

model + tools + prompt = working agent
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.

no magic — createAgent is a two-node graph
// 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:

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

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.

scalable labs·cvr 30091604·github·linkedin·hello@scalable.dk