D
Dev SOPKnowledge Base
Search
← All topics

Claude API: Tool Use, Agentic Loops, and Structured Outputs

Claude API tool use patterns in TypeScript — tool runner, manual agentic loops, structured outputs with output_config, streaming, and production best practices.

claudeanthropictool-useagentsllmtypescript
Agent trigger phrases: Claude API tools · Claude tool use · Anthropic tool use · agentic loop · Claude structured output · Claude streaming · betaZodTool

Overview

Claude API tool use for TypeScript applications — from the simplest tool runner to full agentic loops. Uses claude-sonnet-4-6 as the capable default; swap to claude-opus-4-6 for complex reasoning tasks.

Model Defaults

| Task | Model | Why | |------|-------|-----| | Production default | claude-sonnet-4-6 | Cost-efficient, highly capable | | Complex reasoning | claude-opus-4-6 | Maximum reasoning depth | | Simple tasks | claude-haiku-4-5 | Cheapest, fast |

Use exact model ID strings — never append date suffixes.

Basic API Call

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

const response = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 2048,
  system: "You are a helpful assistant. Be concise.",
  messages: [
    { role: "user", content: "Summarize the key points of async/await in JavaScript." }
  ],
});

const text = response.content[0].type === "text" ? response.content[0].text : "";

Tool Use — Tool Runner (Recommended)

The tool runner handles the agentic loop automatically:

import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";

const client = new Anthropic();

const tools: Anthropic.Tool[] = [
  {
    name: "get_weather",
    description: "Get current weather for a city",
    input_schema: {
      type: "object" as const,
      properties: {
        city: { type: "string", description: "City name" },
        units: { type: "string", enum: ["celsius", "fahrenheit"] },
      },
      required: ["city"],
    },
  },
];

async function getWeather(city: string, units = "celsius") {
  // Real implementation would call a weather API
  return { temperature: 22, condition: "Sunny", city, units };
}

async function askClaude(userMessage: string) {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage }
  ];

  while (true) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 4096,
      tools,
      messages,
    });

    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find(b => b.type === "text");
      return textBlock?.text ?? "";
    }

    if (response.stop_reason === "tool_use") {
      messages.push({ role: "assistant", content: response.content });

      const toolResults: Anthropic.ToolResultBlockParam[] = [];
      for (const block of response.content) {
        if (block.type !== "tool_use") continue;
        const input = block.input as { city: string; units?: string };
        const result = await getWeather(input.city, input.units);
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: JSON.stringify(result),
        });
      }
      messages.push({ role: "user", content: toolResults });
    }
  }
}

Structured Outputs

// Prefer output_config over deprecated output_format
const response = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  output_config: {
    format: {
      type: "json_schema",
      json_schema: {
        name: "article_analysis",
        schema: {
          type: "object",
          properties: {
            title: { type: "string" },
            sentiment: { type: "string", enum: ["positive", "negative", "neutral"] },
            keyPoints: { type: "array", items: { type: "string" } },
            score: { type: "number", minimum: 0, maximum: 10 },
          },
          required: ["title", "sentiment", "keyPoints", "score"],
        },
      },
    },
  },
  messages: [{ role: "user", content: `Analyze: ${articleText}` }],
});

Streaming (Use for Long Outputs)

// Stream to avoid HTTP timeout on long responses
const stream = client.messages.stream({
  model: "claude-sonnet-4-6",
  max_tokens: 8192,
  messages: [{ role: "user", content: prompt }],
});

stream.on("text", (text) => process.stdout.write(text));
const finalMessage = await stream.finalMessage();
// finalMessage.usage has token counts

Adaptive Thinking (Opus 4.6)

const response = await client.messages.create({
  model: "claude-opus-4-6",
  max_tokens: 16000,
  thinking: { type: "adaptive" },   // Claude decides when and how much to think
  messages: [{ role: "user", content: complexProblem }],
});

Never use budget_tokens with Opus 4.6 or Sonnet 4.6 — it is deprecated. Use adaptive instead.

Prompt Caching (Cost Reduction)

const response = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 2048,
  system: [
    {
      type: "text",
      text: longSystemPrompt,           // This gets cached
      cache_control: { type: "ephemeral" },
    },
  ],
  messages: [{ role: "user", content: "Question about the document." }],
});
// Subsequent calls reuse cached system prompt — 90% cost reduction on input tokens

Common Pitfalls

| Mistake | Fix | |---------|-----| | Date-suffixed model IDs | Use exact IDs: claude-sonnet-4-6 | | budget_tokens on Opus/Sonnet 4.6 | Use thinking: {type: "adaptive"} | | Deprecated output_format | Use output_config: {format: {...}} | | Prefill on Opus 4.6 | Removed — use output_config instead | | Blocking fetch for large outputs | Use streaming with .stream() | | String-matching error messages | Use typed exceptions: Anthropic.RateLimitError |