Step 2 of 7: First tool
By the end you will be able to:
- Write a tool the model can call: a description, an input schema and an
executefunction - Explain why the description decides when the tool is used
- See your tool called from chat on your demo URL
Use the Cloudflare account on your team card (also on the Workspace page). No account yet? Ask a host: you can read on, but you can't deploy until it is ready.
The curl and PowerShell examples use $DEMO_URL (macOS / Linux) or $env:DEMO_URL (PowerShell): your demo URL from your team card, set once per terminal in Step 1. Open a new terminal later? Set it again.
Why it matters
- A chat model can only talk. A tool is how it does something: look a thing up, call an API, change a record.
- A tool is what makes an agent useful. One tool that is genuinely useful in your industry is the difference between a demo people nod at and one they want to use.
- It is the smallest real change. One function, and your agent can act.
Here is where a tool lives in the starter. It sits in the Durable Object, next to ChatAgent, and the model reaches it through the agent. Click any box to remind yourself what it does.
Click a box or an arrow to see what it does.
The concepts
A tool is an ordinary function that you describe to the model. The starter builds tools with the AI SDK, the open-source TypeScript library it uses to talk to models, so every tool has the same three parts:
tool({
description: "...", // 1. what the model reads to decide when to call it
inputSchema: z.object({ ... }), // 2. the arguments it must supply (a Zod schema)
execute: async (args) => { ... }, // 3. your code: runs in your Worker, returns JSON
})
- The description is the model's only guide. The model never sees your code, only that sentence, so it decides when to call the tool from it. A vague description gives you a tool that is called at the wrong time, or never. Say what it does and when to use it.
- The input schema is a contract. The model fills in the arguments from the conversation, and inputs that do not match the schema are rejected before your code runs. That is why
executecan trust what it receives, and why each field gets a short.describe(). executeis your code, in your Worker. It can call an API, read a binding or change a record. What it returns goes back to the model, which writes its answer from it, so return a small, clear JSON object rather than a wall of text.- The model decides; your code acts. The model chooses whether and when to call a tool, but it never runs anything itself. That is why a tool is also the right place to add a limit or an approval click (Step 5).
- One list serves everything. The same
tools()list serves the chat page andPOST /api/chat, so one edit covers both. - The AI SDK keeps tools portable. The starter reaches Workers AI through it, so a tool you write now still works if you change the model later (the
MODELline insrc/server.ts).
Docs: Using AI models · AI SDK docs
Which tool? Start from the action, not the model: what is the one thing a person on your team does repeatedly that an agent could do or prepare? Want a starting point? The Agentic Patterns list the common shapes an agent takes.
Go deeper: Code Mode and where tools run
You do not need any of this to finish the step. It is what you reach for when your agent grows beyond a few tools. Tool design is three separate choices: how the model sees tools (direct calls or Code Mode), where tool code runs (a Worker, the browser or another agent) and where tools come from (your own code or an MCP server).
Code Mode: let the model write the glue
- What you just wrote is "direct tool calls". The model gets each tool definition, calls one, sees the result, then picks the next. It is easy to follow and the right choice for a small, known set of tools.
- Code Mode gives the model one tool: code. Instead of calling tools one by one, the model writes JavaScript against typed tool interfaces, and that code runs in a sandbox and calls your tools.
- Why that helps: fewer round trips, less clutter. With direct calls, every dependent step is another model turn, and every intermediate result lands in the model's context. In Code Mode the intermediate results stay inside the sandbox, and only the final value comes back.
- It also copes with big tool catalogues. The model can search for the tools it needs and ask for the detailed types of only those, and a program that worked can be saved and reused.
- Reach for it when a task needs several dependent calls, filtering, branching or repeatable logic, or when there are many tools. For one or two tools, stay with direct calls.
Docs: Code Mode · How Code Mode works
Choose where tools run
- Where a tool runs is a separate choice from how the model sees it. Any location can be used with direct calls or with Code Mode.
- In a Worker (what you are doing now). The tool calls an API, queries SQL, or uses bindings and secrets that must stay on the server.
getWeatherandcalculatein the starter work this way. - In the browser. The tool needs something only a browser has: location, clipboard, local storage. The starter has one:
getUserTimezonehas noexecute, so the chat page answers it. That is also whyPOST /api/chatcannot complete it: there is no browser. - In another agent. A chat-capable agent can run as a streaming tool for your agent, so you can split a big job between specialists.
- Where the tool comes from is a third choice. Tools can also come from an MCP server, which is how your agent could use someone else's tools, and what the
/mcpbox in the diagram offers to others. Step 6 secures it. - Side effects need a say-so wherever they run. Any tool that changes something outside the agent can require an approval, which is Step 5.
Docs: Tools: the concepts · Server-side tools · Client-side tools · Agents as tools
Build it
Prefer to let your coding agent do the typing? Copy this prompt into your agent, in your starter folder. The numbered parts below say what it does.
I am on Step 2 of Build with Cloudflare Stockholm, inside my starter folder. If you have the build-stockholm MCP server, call get_step with step 2.
Add one tool to tools() in src/server.ts, next to getWeather. Name it checkPage. It takes a full https URL (zod), fetches it with a HEAD request following redirects, and returns { url, status, cacheControl, cfCacheStatus } from the status, cache-control and cf-cache-status. Give it a clear description so the model knows when to use it.
Then run npm run dev, call POST http://localhost:5173/api/chat with {"message":"Is https://www.cloudflare.com/ up, and is it cached?"} and show me the reply. Stop the dev server, run npm run deploy, and make the same call against my demo URL.
Do not change anything else in wrangler.jsonc, and do not create or delete resources I did not ask for.
When it works, give me one good prompt to try in the chat page and say what I should see.Paste it into your coding agent: the terminal, the IDE panel or the desktop app all work.
1. Write the tool
Open src/server.ts and find tools(). Add yours next to getWeather (or replace it). This example is a real network call, the kind an edge or web team could use:
checkPage: tool({
description:
"Fetch a public web page and report its HTTP status and cache headers. " +
"Use when the user asks whether a page is up or cached.",
inputSchema: z.object({
url: z.string().url().describe("Full https URL to check"),
}),
execute: async ({ url }) => {
const res = await fetch(url, { method: "HEAD", redirect: "follow" });
return {
url,
status: res.status,
cacheControl: res.headers.get("cache-control"),
cfCacheStatus: res.headers.get("cf-cache-status"),
};
},
}),
Now replace it with your action. Keep the shape: a clear description, a small inputSchema, an execute that returns a small JSON object. Use synthetic data for this workshop. Your demo URL and chat may be publicly reachable; do not upload real customer, employee or proprietary data.
2. Try it locally, then deploy
npm run dev
Leave it running. In a second terminal, call the local /api/chat (it runs the same agent):
curl -s -X POST "http://localhost:5173/api/chat" \
-H 'content-type: application/json' \
-d '{"message":"Is https://www.cloudflare.com/ up, and is it cached?"}'
Is https://www.cloudflare.com/ up, and is it cached?
Open http://localhost:5173 in a browser and paste this into the message box instead of running the command above. It is the same agent either way.
Then stop npm run dev with Ctrl+C and deploy:
npm run deploy
Call the deployed agent on your demo URL the same way (it needs DEMO_URL, set in Step 1):
curl -s -X POST "$DEMO_URL/api/chat" \
-H 'content-type: application/json' \
-d '{"message":"Is https://www.cloudflare.com/ up, and is it cached?"}'
Is https://www.cloudflare.com/ up, and is it cached?
Open your demo URL in a browser and paste this into the message box instead of running the command above. It is the same agent either way.
Check it worked
- The reply uses what your tool returned (a status code, your data), not a guess.
- Ask something unrelated: the model answers without calling the tool.
npx wrangler tailshows the request when you call it on your demo URL.
Stuck?
- The model never calls the tool. The description is the only thing it sees. Say what the tool does and when to use it, and name it in your test prompt ("use checkPage on ...").
- TypeScript or Zod error on deploy. Check the commas between tools, and that
inputSchemais az.object({...}). - 500 from
/api/chat. Runnpx wrangler tail, call again and read the error. A thrown error insideexecuteshows up there. - The reply is empty after a tool ran. Return a small object, not a huge blob. Large outputs eat the model's context.
- A tool without
execute(likegetUserTimezone) ends the turn over/api/chat. That is by design: those tools need a browser. Test them in the chat page.
Go further
- Give the tool a second parameter and let the model choose (
market: z.enum(["se","dk","fi"])). - Connect an existing MCP server as tools: the starter already merges
this.mcp.getAITools()intotools(). - The deeper reference for each shape lives in the Agentic Patterns (optional reading).
- Ask your coding agent: "add a tool that ..., following the pattern of
getWeatherinsrc/server.ts."
Finished step 2?
Checking your team…
Back: Step 1: Launch your first agent. Next: Step 3: State and memory.
Need help? Raise a hand for a host, or ask the Mentor dock (it escalates to a host when unsure). Remote: remote help channel to be confirmed by the event owner.