Intermediate · Lesson 4 of 6 · 18 min

Structured output and tool calling

Get JSON that matches a schema, let a model call your functions, and validate both before anything touches a real system.

Why free text breaks automations

An automation step needs fields: a category to map to a pipeline stage, an email to look up, a yes or no to branch on. Prose that varies from run to run breaks the parsing step, usually on the rare input you never tested.

Structured outputs fix the shape. On the Claude API you pass a JSON schema in output_config.format, and the response text is JSON that matches it. Anthropic's docs describe two complementary features: JSON outputs control what Claude says, and strict tool use guarantees that tool inputs match their schema.

javascript
const response = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    authorization: "Bearer " + process.env.ANTHROPIC_API_KEY,
    "anthropic-version": "2026-10-21",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-haiku-5-5",
    max_tokens: 500,
    system: "You qualify inbound leads for a home services company.",
    messages: [{ role: "user", content: leadMessage }],
    output_config: {
      format: {
        type: "json_schema",
        schema: {
          type: "object",
          properties: {
            service: { type: "string", enum: ["plumbing", "electrical", "hvac", "other"] },
            urgency: { type: "string", enum: ["today", "this_week", "flexible", "unknown"] },
            summary: { type: "string" },
            confidence: { type: "string", enum: ["high", "medium", "low"] },
          },
          required: ["service", "urgency", "summary", "confidence"],
          additionalProperties: false,
        },
      },
    },
  }),
});

const message = await response.json();
const lead = JSON.parse(message.content.find((block) => block.type === "text").text);

Designing the schema

Use enums for anything a later step branches on, so the model cannot invent a category your CRM does not have. Include an explicit other or unknown value so the model has an honest option. Mark every field required and set additionalProperties to false so nothing unexpected appears.

Add a confidence field and route low confidence to a person. Keep the schema flat where you can, and give fields clear names. Some JSON Schema constraints are not supported; Anthropic's SDK provides a helper that moves them into field descriptions. Two incompatibilities to know: citations cannot be combined with output_config.format (the API returns a 400), and message prefill does not work with JSON outputs.

Tool calling

Tool calling lets the model ask your code to do something. You describe each tool with a name, a description and an input schema. When the model wants a tool, the response has stop_reason tool_use and a tool_use block with an id, the tool name and the input. Your code runs the operation and sends back a tool_result that references that id, and the model continues.

Setting strict to true on a tool guarantees its inputs match the schema. Tools you define run in your application, while server tools such as web search run on Anthropic's infrastructure. The model never touches your CRM directly; your code decides what each tool call is allowed to do.

jsonc
// 1. A tool you offer in the request's tools array
{
  "name": "lookup_contact",
  "description": "Find a CRM contact by email. Call this before drafting any reply.",
  "strict": true,
  "input_schema": {
    "type": "object",
    "properties": { "email": { "type": "string" } },
    "required": ["email"],
    "additionalProperties": false
  }
}

// 2. In the response (stop_reason: "tool_use"), a content block like:
{ "type": "tool_use", "id": "toolu_01AbC", "name": "lookup_contact", "input": { "email": "jane@example.com" } }

// 3. Your next user message returns the result
{
  "role": "user",
  "content": [
    { "type": "tool_result", "tool_use_id": "toolu_01AbC", "content": "{\"found\": true, \"stage\": \"Booked\"}" }
  ]
}

Validate anyway

Schema-valid is not the same as correct. A perfectly formed JSON object can still put a plumbing lead in electrical, or contain an email address the model misread. Validate at the boundary with a schema library such as zod, then apply business rules: does the stage exist, does the email look real, is the confidence high enough to act without review.

Log the raw model output next to the validated result so you can debug later, without logging personal data you do not need. And never let model output drive a destructive or customer-facing action, such as deleting a contact or sending an SMS, without a check in code or a person in between.

Key points

  • ◆Pass a JSON schema in output_config.format to get schema-matching JSON from the Claude API.
  • ◆Use enums, required fields, additionalProperties false and an explicit unknown option.
  • ◆Tool calls come back as tool_use blocks; your code runs them and returns tool_result.
  • ◆strict: true guarantees tool inputs match the schema; it does not make them correct.
  • ◆Validate with business rules and route low-confidence or risky actions to a person.

Terms in this lesson

Try it · exercise

Write a lead-qualification schema and a validator, and prove the validator catches bad output.

  1. Copy the schema from this lesson and adapt the service enum to a business you know.
  2. Write five JSON objects by hand or with a chat app: three valid, one missing the urgency field, and one with a service value outside the enum.
  3. Write a small validator with zod (or any JSON Schema validator) that checks each object and also routes confidence low to a review list.
  4. If you have Claude API access, send three real lead messages with the request from this lesson and validate the responses too.

You're done when: Your validator accepts the three valid objects, rejects the two broken ones with an error naming the bad field, and puts any object with confidence low on the review list.

Quiz me on this lesson