Module 3 · Lesson 5 of 6
Schemas the Model Can Fill, Tools the Model Can Call
9 min read · July 30, 2026
The last lesson ended on a deliberately unsettling note: constrained decoding guarantees compliance with whatever schema you supply, and compliance with a bad schema is fluent, well-typed uselessness. The mechanism has no opinion about whether your schema is fillable. That's a design problem, it's yours, and it's this lesson's first half.
The second half is the payoff module 2 promised twice: defining your first tool. These halves belong in one lesson because they are one skill. A tool definition is a schema with a verb attached, tool arguments arrive through the same constrained decoding you just learned, and every design rule from the first half applies verbatim to the second. If you can design a schema a model fills well, you can define a tool a model calls well.
Descriptions are documentation the model reads
Start with the fact that changes how you write schemas: every description field is consumed by the model at inference time. You've written OpenAPI specs where descriptions were documentation for humans, skippable under deadline. Here, the description is load-bearing. The model decides what a field means, what units it's in, and when to leave it null based substantially on what the description says.
So write them like the API docs you wish your dependencies had: units ("seconds, not milliseconds"), formats ("ISO 8601 date, no time component"), boundaries ("the order total after discounts, before tax"), and null semantics ("null when the email mentions no order number; never guess one"). An undescribed field is an undocumented parameter, and the model does what your API consumers do with undocumented parameters: guesses, confidently. Field names carry the same freight. amt invites improvisation; refund_amount_usd is documentation that can't drift from the field it documents. And keep the schema's vocabulary aligned with the contract's: if the system prompt says "dispute," a schema field called complaint_type forces the model to guess that they're synonyms.
Design rules for fillable schemas
The rules share one root: every field is a small generation task, and your schema decides how hard each task is.
- Enums over free strings. A free-string
categoryyields"billing","Billing","billing issue", and one day"facturation", which is the well-formed garbage problem in miniature. An enum is a CHECK constraint: the sampler literally cannot emit a value off the list. - Give the enum an escape hatch. A closed enum with no honest answer forces confabulation; under constrained decoding, some legal value will be sampled regardless. Add
"unknown"or"other"explicitly, and say in the description when to use it. You're not inviting laziness; you're making "I can't tell" expressible so it stops masquerading as"billing". - Express uncertainty as nullable, and mean it. Strict modes commonly require every field to be present (OpenAI's strict mode requires all fields, with optionality expressed as nullable types). Semantics belong in the description: "null when not stated in the ticket" is an instruction the model follows surprisingly well, and it's the difference between missing data and invented data.
- Flat over deep. Four levels of nesting multiply the ways generation can wander and make every downstream error message worse. Most extraction wants one level, maybe two. If the shape is genuinely branched, a small discriminated union (a
typeenum plus a flat set of per-type fields) beats cleveranyOfgymnastics, which strict-mode subsets often restrict anyway (both providers accept only a slice of JSON Schema). - Constrain arrays. Say what an item is, and bound the count in the description ("the three most severe issues, most severe first"). An unbounded array is an invitation to pad, and you pay for padding by the token.
A schema the model fumbles
category: free string, no descriptiondetails: object nested four levels deepamount: number (of what? currency? sign?)- Optional fields simply absent from
required - No legal way to say "can't tell": improvisation guaranteed
A schema the model fills
category: enum of six values plus"unknown", each explained- Flat fields with names that are documentation (
refund_amount_usd) - Every description states units, format, and null semantics
- Nullable-but-required per strict-mode rules
- The honest answer is always expressible
// One well-designed extraction schema, rules annotated.
const TRIAGE_SCHEMA = {
type: "object",
additionalProperties: false,
required: ["category", "sentiment", "order_id", "refund_amount_usd"],
properties: {
category: {
type: "string",
// CHECK constraint, with an escape hatch so "can't tell" is legal.
enum: ["billing", "shipping", "product_defect", "account", "unknown"],
description: "Primary issue. Use 'unknown' when none clearly applies; never guess.",
},
sentiment: { type: "string", enum: ["negative", "neutral", "positive"] },
order_id: {
type: ["string", "null"], // required-but-nullable: absent ≠ invented
description: "Order ID exactly as written in the ticket (format ORD-XXXXXX). Null when the ticket doesn't state one; never construct one.",
},
refund_amount_usd: {
type: ["number", "null"], // name carries units; description carries rules
description: "Refund amount the customer requests, in USD. Null unless they name a figure.",
},
},
} as const;A tool is a schema with a verb
Now the promised definition. A tool is three things: a name, a description that says what it does and when to use it, and an input_schema for its arguments. That's the entire anatomy, on both providers. You are describing a typed internal endpoint to an unreliable caller.
Demystify what happens next, because the vocabulary oversells it: the model never executes anything. Given tools, the model may respond with a structured request ("call lookup_order with {"order_id": "ORD-1234"}"), generated through the same constrained decoding as lesson 4's outputs. Your code decides whether to honor the request, runs its own function, and reports back. The model proposes; your process disposes. A tool call is structured output pointed at a function boundary, full stop.
The pieces map onto what you already know: the tool description does the job of an endpoint's docs (and, like a system prompt clause, earns its length by covering when not to call); the input schema is the request validator, obeying every rule above; and tool_choice is your routing policy: auto (model decides), forced to a specific tool (lesson 4's extraction trick), or none. Anthropic additionally lets you schema-guarantee arguments with strict: true on the definition.
The single round-trip
Here is the full lifecycle, once, buffered, one provider dialect, no loop:
- Request + tool defsyoutools array rides the prompt (and bills as input tokens)
- tool_use blockstructured outputname + args; stop_reason: tool_use
- Your function runsyouvalidated, authorized, in your process
- tool_result → answerround-tripsend the result back; model finishes in prose
// The single round-trip, Anthropic dialect, deliberately un-abstracted.
const first = await anthropic.messages.create({
model: pins.support, max_tokens: 1024,
tools: [{
name: "lookup_order",
description: "Fetch one order's status and totals by exact order ID. " +
"Use only when the customer references a specific order.",
input_schema: ORDER_LOOKUP_SCHEMA, // every rule from above applies
}],
messages,
});
if (first.stop_reason === "tool_use") { // the stop vocabulary from module 2
const call = first.content.find((b) => b.type === "tool_use")!;
const result = await orders.lookup(call.input); // YOUR code: validate, authorize, run
const second = await anthropic.messages.create({
model: pins.support, max_tokens: 1024, tools,
messages: [...messages,
{ role: "assistant", content: first.content },
{ role: "user", content: [{ type: "tool_result",
tool_use_id: call.id, content: JSON.stringify(result) }] },
],
});
// second.content: prose that finally answers, grounded in the lookup
}Note what the transcript shows: the tool call and its result become conversation turns, which is module 2's statelessness doing its usual work. You resend everything, including the tool exchange, and the tool definitions themselves ride every request as input tokens: schema size is now a line item too.
Both directions of the boundary, schematized
The symmetry is the lesson. Outputs conform to schemas you designed; tool arguments conform to schemas you designed; the same five rules made both fillable; both arrive via the same constrained decoding. Data crossing the model boundary in either direction now has a declared, versioned shape, and the stack map's agent-runtime box has its foundation poured a module early.
What neither direction has is trust. A schema-perfect extraction can name an order that doesn't exist; a schema-perfect tool call can ask to refund an amount no policy allows. Shape guarantees end where your business rules begin, and a tool call that triggers a write is untrusted input asking to mutate state, which should raise exactly the reflex the next lesson mechanizes: validate it like it came from a user. The capstone closes the module there.
Key takeaways
- Schema descriptions and field names are consumed by the model at inference time. Write them like load-bearing API docs: units, formats, boundaries, null semantics.
- Every field is a generation task; design for fillability: enums over free strings, an explicit
unknown/otherescape hatch, required-but-nullable for honest absence, flat over deep, discriminated unions over cleveranyOf, bounded arrays. - A closed enum with no honest answer forces confabulation; under constrained decoding some legal value gets sampled regardless. Make "can't tell" expressible.
- A tool is a schema with a verb: name, when-to-use description, input schema. The model never executes; it emits a structured request via the same constrained decoding, and your code validates, authorizes, and runs the real function.
- The single round-trip is: tools ride the request,
stop_reason: "tool_use"hands you name + args, you execute,tool_resultgoes back, prose comes out. Loops, parallel calls, streaming, and cross-provider unification wait for module 5. - Schemas ship in the versioned prompt tuple and bill as input tokens on every call. Shape is now guaranteed both directions; trust is not, and that's the capstone's job.