Repository Public
1---2title: "Tutorial: Build a tool-using agent"3url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent4description: A guided walkthrough from a single tool call to a production-ready agentic loop.5---67This tutorial builds a calendar-management agent in five concentric rings. Each ring is a complete, runnable program that adds exactly one concept to the ring before it. By the end you will have written the agentic loop by hand and then replaced it with the Tool Runner SDK abstraction.89The example tool is `create_calendar_event`. Its schema uses nested objects, arrays, and optional fields, so you will see how Claude handles realistic input shapes rather than a single flat string.1011<Note>12 Every ring runs standalone. Copy any ring into a fresh file and it will run without the code from earlier rings.13</Note>1415## Ring 1: Single tool, single turn1617The smallest possible tool-using program: one tool, one user message, one tool call, one result. The code is heavily commented so you can map each line to the [tool use lifecycle](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works).1819The request sends a `tools` array alongside the user message. When Claude determines that a tool call is needed, the response comes back with `stop_reason: "tool_use"` and a `tool_use` content block containing the tool name, a unique `id`, and the structured `input`. Your code runs the tool, then sends the result back in a `tool_result` block whose `tool_use_id` matches the `id` from the call.2021<CodeGroup>22 ```bash cURL23 #!/bin/bash24 # Ring 1: Single tool, single turn.2526 # Define one tool as a JSON fragment. The input_schema is a JSON Schema27 # object describing the arguments Claude should pass when it calls this28 # tool. This schema includes nested objects (recurrence), arrays29 # (attendees), and optional fields, which is closer to real-world tools30 # than a flat string argument.31 TOOLS='[32 {33 "name": "create_calendar_event",34 "description": "Create a calendar event with attendees and optional recurrence.",35 "input_schema": {36 "type": "object",37 "properties": {38 "title": {"type": "string"},39 "start": {"type": "string", "format": "date-time"},40 "end": {"type": "string", "format": "date-time"},41 "attendees": {42 "type": "array",43 "items": {"type": "string", "format": "email"}44 },45 "recurrence": {46 "type": "object",47 "properties": {48 "frequency": {"enum": ["daily", "weekly", "monthly"]},49 "count": {"type": "integer", "minimum": 1}50 }51 }52 },53 "required": ["title", "start", "end"]54 }55 }56 ]'5758 USER_MSG="Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am."5960 # Send the user's request along with the tool definition. Claude decides61 # whether to call the tool based on the request and the tool description.62 RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \63 -H "x-api-key: $ANTHROPIC_API_KEY" \64 -H "anthropic-version: 2023-06-01" \65 -H "content-type: application/json" \66 -d "$(jq -n \67 --argjson tools "$TOOLS" \68 --arg msg "$USER_MSG" \69 '{70 model: "claude-opus-5",71 max_tokens: 1024,72 tools: $tools,73 tool_choice: {type: "auto", disable_parallel_tool_use: true},74 messages: [{role: "user", content: $msg}]75 }')")7677 # When Claude calls a tool, the response has stop_reason "tool_use"78 # and the content array contains a tool_use block alongside any text.79 echo "stop_reason: $(echo "$RESPONSE" | jq -r '.stop_reason')"8081 # Find the tool_use block. A response may contain text blocks before the82 # tool_use block, so filter by type rather than assuming position.83 TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")')84 TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id')85 echo "Tool: $(echo "$TOOL_USE" | jq -r '.name')"86 echo "Input: $(echo "$TOOL_USE" | jq -c '.input')"8788 # Execute the tool. In a real system this would call your calendar API.89 # Here the result is hardcoded to keep the example self-contained.90 RESULT='{"event_id": "evt_123", "status": "created"}'9192 # Send the result back. The tool_result block goes in a user message and93 # its tool_use_id must match the id from the tool_use block above. The94 # assistant's previous response is included so Claude has the full history.95 ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content')96 FOLLOWUP=$(curl -s https://api.anthropic.com/v1/messages \97 -H "x-api-key: $ANTHROPIC_API_KEY" \98 -H "anthropic-version: 2023-06-01" \99 -H "content-type: application/json" \100 -d "$(jq -n \101 --argjson tools "$TOOLS" \102 --arg msg "$USER_MSG" \103 --argjson assistant "$ASSISTANT_CONTENT" \104 --arg tool_use_id "$TOOL_USE_ID" \105 --arg result "$RESULT" \106 '{107 model: "claude-opus-5",108 max_tokens: 1024,109 tools: $tools,110 tool_choice: {type: "auto", disable_parallel_tool_use: true},111 messages: [112 {role: "user", content: $msg},113 {role: "assistant", content: $assistant},114 {role: "user", content: [115 {type: "tool_result", tool_use_id: $tool_use_id, content: $result}116 ]}117 ]118 }')")119120 # With the tool result in hand, Claude produces a final natural-language121 # answer and stop_reason becomes "end_turn".122 echo "stop_reason: $(echo "$FOLLOWUP" | jq -r '.stop_reason')"123 echo "$FOLLOWUP" | jq -r '.content[] | select(.type == "text") | .text'124 ```125126 ```bash CLI127 #!/usr/bin/env bash128 # Ring 1: Single tool, single turn.129 # Uses jq for cross-turn message-array state — building an agentic loop in shell130 # requires JSON manipulation beyond ant's single-call --transform scope.131 set -euo pipefail132133 USER_MSG="Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am."134 MESSAGES=$(jq -n --arg msg "$USER_MSG" '[{role: "user", content: $msg}]')135136 # Define one tool. The input_schema is a JSON Schema object describing137 # the arguments Claude should pass when it calls this tool. This schema138 # includes nested objects (recurrence), arrays (attendees), and optional139 # fields, which is closer to real-world tools than a flat string argument.140 call_api() {141 # ant reads the request body as YAML on stdin: no auth headers, no142 # hand-built JSON envelope. The static keys (model, tools, tool_choice)143 # live in a quoted heredoc; the growing messages array is appended as144 # JSON, which YAML accepts as flow syntax.145 {146 cat <<'YAML'147 model: claude-opus-5148 max_tokens: 1024149 tool_choice: {type: auto, disable_parallel_tool_use: true}150 tools:151 - name: create_calendar_event152 description: Create a calendar event with attendees and optional recurrence.153 input_schema:154 type: object155 properties:156 title: {type: string}157 start: {type: string, format: date-time}158 end: {type: string, format: date-time}159 attendees:160 type: array161 items: {type: string, format: email}162 recurrence:163 type: object164 properties:165 frequency: {enum: [daily, weekly, monthly]}166 count: {type: integer, minimum: 1}167 required: [title, start, end]168 YAML169 printf 'messages: %s\n' "$MESSAGES"170 } | ant messages create --format json171 }172173 # Send the user's request along with the tool definition. Claude decides174 # whether to call the tool based on the request and the tool description.175 RESPONSE=$(call_api)176177 # When Claude calls a tool, the response has stop_reason "tool_use"178 # and the content array contains a tool_use block alongside any text.179 echo "stop_reason: $(jq -r '.stop_reason' <<<"$RESPONSE")"180181 # Find the tool_use block. A response may contain text blocks before the182 # tool_use block, so filter by type rather than assuming position.183 TOOL_USE=$(jq '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")184 TOOL_USE_ID=$(jq -r '.id' <<<"$TOOL_USE")185 echo "Tool: $(jq -r '.name' <<<"$TOOL_USE")"186 echo "Input: $(jq -c '.input' <<<"$TOOL_USE")"187188 # Execute the tool. In a real system this would call your calendar API.189 # Here the result is hardcoded to keep the example self-contained.190 RESULT='{"event_id": "evt_123", "status": "created"}'191192 # Send the result back. The tool_result block goes in a user message and193 # its tool_use_id must match the id from the tool_use block above. The194 # assistant's previous response is included so Claude has the full history.195 MESSAGES=$(jq \196 --argjson assistant "$(jq '.content' <<<"$RESPONSE")" \197 --arg tool_use_id "$TOOL_USE_ID" \198 --arg result "$RESULT" \199 '. + [200 {role: "assistant", content: $assistant},201 {role: "user", content: [202 {type: "tool_result", tool_use_id: $tool_use_id, content: $result}203 ]}204 ]' <<<"$MESSAGES")205206 FOLLOWUP=$(call_api)207208 # With the tool result in hand, Claude produces a final natural-language209 # answer and stop_reason becomes "end_turn".210 echo "stop_reason: $(jq -r '.stop_reason' <<<"$FOLLOWUP")"211 jq -r '.content[] | select(.type == "text") | .text' <<<"$FOLLOWUP"212 ```213214 ```python Python215 # Ring 1: Single tool, single turn.216217 import json218219 import anthropic220221 # Create a client. It reads ANTHROPIC_API_KEY from the environment.222 client = anthropic.Anthropic()223224 # Define one tool. The input_schema is a JSON Schema object describing225 # the arguments Claude should pass when it calls this tool. This schema226 # includes nested objects (recurrence), arrays (attendees), and optional227 # fields, which is closer to real-world tools than a flat string argument.228 tools = [229 {230 "name": "create_calendar_event",231 "description": "Create a calendar event with attendees and optional recurrence.",232 "input_schema": {233 "type": "object",234 "properties": {235 "title": {"type": "string"},236 "start": {"type": "string", "format": "date-time"},237 "end": {"type": "string", "format": "date-time"},238 "attendees": {239 "type": "array",240 "items": {"type": "string", "format": "email"},241 },242 "recurrence": {243 "type": "object",244 "properties": {245 "frequency": {"enum": ["daily", "weekly", "monthly"]},246 "count": {"type": "integer", "minimum": 1},247 },248 },249 },250 "required": ["title", "start", "end"],251 },252 }253 ]254255 # Send the user's request along with the tool definition. Claude decides256 # whether to call the tool based on the request and the tool description.257 response = client.messages.create(258 model="claude-opus-5",259 max_tokens=1024,260 tools=tools,261 tool_choice={"type": "auto", "disable_parallel_tool_use": True},262 messages=[263 {264 "role": "user",265 "content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",266 }267 ],268 )269270 # When Claude calls a tool, the response has stop_reason "tool_use"271 # and the content array contains a tool_use block alongside any text.272 print(f"stop_reason: {response.stop_reason}")273274 # Find the tool_use block. A response may contain text blocks before the275 # tool_use block, so scan the content array rather than assuming position.276 tool_use = next(block for block in response.content if block.type == "tool_use")277 print(f"Tool: {tool_use.name}")278 print(f"Input: {tool_use.input}")279280 # Execute the tool. In a real system this would call your calendar API.281 # Here the result is hardcoded to keep the example self-contained.282 result = {"event_id": "evt_123", "status": "created"}283284 # Send the result back. The tool_result block goes in a user message and285 # its tool_use_id must match the id from the tool_use block above. The286 # assistant's previous response is included so Claude has the full history.287 followup = client.messages.create(288 model="claude-opus-5",289 max_tokens=1024,290 tools=tools,291 tool_choice={"type": "auto", "disable_parallel_tool_use": True},292 messages=[293 {294 "role": "user",295 "content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",296 },297 {"role": "assistant", "content": response.content},298 {299 "role": "user",300 "content": [301 {302 "type": "tool_result",303 "tool_use_id": tool_use.id,304 "content": json.dumps(result),305 }306 ],307 },308 ],309 )310311 # With the tool result in hand, Claude produces a final natural-language312 # answer and stop_reason becomes "end_turn".313 print(f"stop_reason: {followup.stop_reason}")314 final_text = next(block for block in followup.content if block.type == "text")315 print(final_text.text)316 ```317318 ```typescript TypeScript319 // Ring 1: Single tool, single turn.320321 import Anthropic from "@anthropic-ai/sdk";322323 // Create a client. It reads ANTHROPIC_API_KEY from the environment.324 const client = new Anthropic();325326 // Define one tool. The input_schema is a JSON Schema object describing327 // the arguments Claude should pass when it calls this tool. This schema328 // includes nested objects (recurrence), arrays (attendees), and optional329 // fields, which is closer to real-world tools than a flat string argument.330 const tools: Anthropic.Tool[] = [331 {332 name: "create_calendar_event",333 description:334 "Create a calendar event with attendees and optional recurrence.",335 input_schema: {336 type: "object",337 properties: {338 title: { type: "string" },339 start: { type: "string", format: "date-time" },340 end: { type: "string", format: "date-time" },341 attendees: {342 type: "array",343 items: { type: "string", format: "email" },344 },345 recurrence: {346 type: "object",347 properties: {348 frequency: { enum: ["daily", "weekly", "monthly"] },349 count: { type: "integer", minimum: 1 },350 },351 },352 },353 required: ["title", "start", "end"],354 },355 },356 ];357358 // Send the user's request along with the tool definition. Claude decides359 // whether to call the tool based on the request and the tool description.360 const response = await client.messages.create({361 model: "claude-opus-5",362 max_tokens: 1024,363 tools,364 tool_choice: { type: "auto", disable_parallel_tool_use: true },365 messages: [366 {367 role: "user",368 content:369 "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",370 },371 ],372 });373374 // When Claude calls a tool, the response has stop_reason "tool_use"375 // and the content array contains a tool_use block alongside any text.376 console.log(`stop_reason: ${response.stop_reason}`);377378 // Find the tool_use block. A response may contain text blocks before the379 // tool_use block, so scan the content array rather than assuming position.380 const toolUse = response.content.find(381 (block): block is Anthropic.ToolUseBlock => block.type === "tool_use",382 )!;383 console.log(`Tool: ${toolUse.name}`);384 console.log(`Input: ${JSON.stringify(toolUse.input)}`);385386 // Execute the tool. In a real system this would call your calendar API.387 // Here the result is hardcoded to keep the example self-contained.388 const result = { event_id: "evt_123", status: "created" };389390 // Send the result back. The tool_result block goes in a user message and391 // its tool_use_id must match the id from the tool_use block above. The392 // assistant's previous response is included so Claude has the full history.393 const followup = await client.messages.create({394 model: "claude-opus-5",395 max_tokens: 1024,396 tools,397 tool_choice: { type: "auto", disable_parallel_tool_use: true },398 messages: [399 {400 role: "user",401 content:402 "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",403 },404 { role: "assistant", content: response.content },405 {406 role: "user",407 content: [408 {409 type: "tool_result",410 tool_use_id: toolUse.id,411 content: JSON.stringify(result),412 },413 ],414 },415 ],416 });417418 // With the tool result in hand, Claude produces a final natural-language419 // answer and stop_reason becomes "end_turn".420 console.log(`stop_reason: ${followup.stop_reason}`);421 for (const block of followup.content) {422 if (block.type === "text") {423 console.log(block.text);424 }425 }426 ```427428 ```csharp C#429 // Ring 1: Single tool, single turn.430431 using System;432 using System.Collections.Generic;433 using System.Linq;434 using System.Text.Json;435 using System.Threading.Tasks;436 using Anthropic;437 using Anthropic.Models.Messages;438439 // Create a client. It reads ANTHROPIC_API_KEY from the environment.440 AnthropicClient client = new();441442 // Define one tool. The input schema is a JSON Schema object describing443 // the arguments Claude should pass when it calls this tool. This schema444 // includes nested objects (recurrence), arrays (attendees), and optional445 // fields, which is closer to real-world tools than a flat string argument.446 List<ToolUnion> tools =447 [448 new ToolUnion(new Tool()449 {450 Name = "create_calendar_event",451 Description = "Create a calendar event with attendees and optional recurrence.",452 InputSchema = new InputSchema()453 {454 Properties = new Dictionary<string, JsonElement>455 {456 ["title"] = JsonSerializer.SerializeToElement(new { type = "string" }),457 ["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),458 ["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),459 ["attendees"] = JsonSerializer.SerializeToElement(new460 {461 type = "array",462 items = new { type = "string", format = "email" },463 }),464 ["recurrence"] = JsonSerializer.SerializeToElement(new465 {466 type = "object",467 properties = new468 {469 frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },470 count = new { type = "integer", minimum = 1 },471 },472 }),473 },474 Required = ["title", "start", "end"],475 },476 }),477 ];478479 // Ask for at most one tool call per turn so the single-turn flow below480 // stays predictable.481 var toolChoice = new ToolChoice(new ToolChoiceAuto { DisableParallelToolUse = true });482483 const string userPrompt =484 "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.";485486 // Send the user's request along with the tool definition. Claude decides487 // whether to call the tool based on the request and the tool description.488 var response = await client.Messages.Create(new MessageCreateParams489 {490 Model = Model.ClaudeOpus5,491 MaxTokens = 1024,492 Tools = tools,493 ToolChoice = toolChoice,494 Messages = [new() { Role = Role.User, Content = userPrompt }],495 });496497 // When Claude calls a tool, the response has stop_reason "tool_use"498 // and the content array contains a tool_use block alongside any text.499 Console.WriteLine($"stop_reason: {response.StopReason?.Raw()}");500501 // Find the tool_use block. A response may contain text blocks before the502 // tool_use block, so scan the content array rather than assuming position.503 ToolUseBlock? toolUse = null;504 foreach (var block in response.Content)505 {506 if (block.TryPickToolUse(out var picked))507 {508 toolUse = picked;509 break;510 }511 }512 Console.WriteLine($"Tool: {toolUse!.Name}");513 Console.WriteLine($"Input: {JsonSerializer.Serialize(toolUse.Input)}");514515 // Execute the tool. In a real system this would call your calendar API.516 // Here the result is hardcoded to keep the example self-contained.517 var result = """{"event_id": "evt_123", "status": "created"}""";518519 // Send the result back. The tool_result block goes in a user message and520 // its tool_use_id must match the id from the tool_use block above. The521 // assistant's previous response is included so Claude has the full history.522 List<ContentBlockParam> toolResults =523 [524 new ContentBlockParam(new ToolResultBlockParam()525 {526 ToolUseID = toolUse.ID,527 Content = result,528 }),529 ];530531 var followup = await client.Messages.Create(new MessageCreateParams532 {533 Model = Model.ClaudeOpus5,534 MaxTokens = 1024,535 Tools = tools,536 ToolChoice = toolChoice,537 Messages =538 [539 new() { Role = Role.User, Content = userPrompt },540 new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() },541 new() { Role = Role.User, Content = new MessageParamContent(toolResults) },542 ],543 });544545 // With the tool result in hand, Claude produces a final natural-language546 // answer and stop_reason becomes "end_turn".547 Console.WriteLine($"stop_reason: {followup.StopReason?.Raw()}");548 foreach (var block in followup.Content)549 {550 if (block.TryPickText(out var text))551 {552 Console.WriteLine(text.Text);553 }554 }555 ```556557 ```go Go558 // Ring 1: Single tool, single turn.559560 package main561562 import (563 "context"564 "fmt"565 "log"566567 "github.com/anthropics/anthropic-sdk-go"568 )569570 func main() {571 // Create a client. It reads ANTHROPIC_API_KEY from the environment.572 client := anthropic.NewClient()573 ctx := context.Background()574575 // Define one tool. The input schema is a JSON Schema object describing576 // the arguments Claude should pass when it calls this tool. This schema577 // includes nested objects (recurrence), arrays (attendees), and optional578 // fields, which is closer to real-world tools than a flat string argument.579 tools := []anthropic.ToolUnionParam{580 {OfTool: &anthropic.ToolParam{581 Name: "create_calendar_event",582 Description: anthropic.String("Create a calendar event with attendees and optional recurrence."),583 InputSchema: anthropic.ToolInputSchemaParam{584 Properties: map[string]any{585 "title": map[string]any{"type": "string"},586 "start": map[string]any{"type": "string", "format": "date-time"},587 "end": map[string]any{"type": "string", "format": "date-time"},588 "attendees": map[string]any{589 "type": "array",590 "items": map[string]any{"type": "string", "format": "email"},591 },592 "recurrence": map[string]any{593 "type": "object",594 "properties": map[string]any{595 "frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}},596 "count": map[string]any{"type": "integer", "minimum": 1},597 },598 },599 },600 Required: []string{"title", "start", "end"},601 },602 }},603 }604605 // Ask for at most one tool call per turn so the single-turn flow below606 // stays predictable.607 toolChoice := anthropic.ToolChoiceUnionParam{608 OfAuto: &anthropic.ToolChoiceAutoParam{DisableParallelToolUse: anthropic.Bool(true)},609 }610611 userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(612 "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",613 ))614615 // Send the user's request along with the tool definition. Claude decides616 // whether to call the tool based on the request and the tool description.617 response, err := client.Messages.New(ctx, anthropic.MessageNewParams{618 Model: anthropic.ModelClaudeOpus5,619 MaxTokens: 1024,620 Tools: tools,621 ToolChoice: toolChoice,622 Messages: []anthropic.MessageParam{userMessage},623 })624 if err != nil {625 log.Fatal(err)626 }627628 // When Claude calls a tool, the response has stop_reason "tool_use"629 // and the content array contains a tool_use block alongside any text.630 fmt.Printf("stop_reason: %s\n", response.StopReason)631632 // Find the tool_use block. A response may contain text blocks before the633 // tool_use block, so scan the content array rather than assuming position.634 var toolUse anthropic.ContentBlockUnion635 for _, block := range response.Content {636 if block.Type == "tool_use" {637 toolUse = block638 break639 }640 }641 fmt.Printf("Tool: %s\n", toolUse.Name)642 fmt.Printf("Input: %s\n", string(toolUse.Input))643644 // Execute the tool. In a real system this would call your calendar API.645 // Here the result is hardcoded to keep the example self-contained.646 result := `{"event_id": "evt_123", "status": "created"}`647648 // Send the result back. The tool_result block goes in a user message and649 // its tool_use_id must match the id from the tool_use block above. The650 // assistant's previous response is included so Claude has the full history.651 var assistantContent []anthropic.ContentBlockParamUnion652 for _, block := range response.Content {653 assistantContent = append(assistantContent, block.ToParam())654 }655656 followup, err := client.Messages.New(ctx, anthropic.MessageNewParams{657 Model: anthropic.ModelClaudeOpus5,658 MaxTokens: 1024,659 Tools: tools,660 ToolChoice: toolChoice,661 Messages: []anthropic.MessageParam{662 userMessage,663 anthropic.NewAssistantMessage(assistantContent...),664 anthropic.NewUserMessage(anthropic.NewToolResultBlock(toolUse.ID, result, false)),665 },666 })667 if err != nil {668 log.Fatal(err)669 }670671 // With the tool result in hand, Claude produces a final natural-language672 // answer and stop_reason becomes "end_turn".673 fmt.Printf("stop_reason: %s\n", followup.StopReason)674 for _, block := range followup.Content {675 if block.Type == "text" {676 fmt.Println(block.Text)677 }678 }679 }680 ```681682 ```java Java683 // Ring 1: Single tool, single turn.684685 import com.anthropic.client.AnthropicClient;686 import com.anthropic.client.okhttp.AnthropicOkHttpClient;687 import com.anthropic.core.JsonValue;688 import com.anthropic.models.messages.ContentBlockParam;689 import com.anthropic.models.messages.Message;690 import com.anthropic.models.messages.MessageCreateParams;691 import com.anthropic.models.messages.Model;692 import com.anthropic.models.messages.Tool;693 import com.anthropic.models.messages.Tool.InputSchema;694 import com.anthropic.models.messages.ToolChoiceAuto;695 import com.anthropic.models.messages.ToolResultBlockParam;696 import com.anthropic.models.messages.ToolUseBlock;697 import java.util.List;698 import java.util.Map;699700 void main() {701 // Create a client. It reads ANTHROPIC_API_KEY from the environment.702 AnthropicClient client = AnthropicOkHttpClient.fromEnv();703704 // Define one tool. The input schema is a JSON Schema object describing705 // the arguments Claude should pass when it calls this tool. This schema706 // includes nested objects (recurrence), arrays (attendees), and optional707 // fields, which is closer to real-world tools than a flat string argument.708 Tool calendarTool = Tool.builder()709 .name("create_calendar_event")710 .description("Create a calendar event with attendees and optional recurrence.")711 .inputSchema(InputSchema.builder()712 .properties(JsonValue.from(Map.of(713 "title", Map.of("type", "string"),714 "start", Map.of("type", "string", "format", "date-time"),715 "end", Map.of("type", "string", "format", "date-time"),716 "attendees", Map.of(717 "type", "array",718 "items", Map.of("type", "string", "format", "email")719 ),720 "recurrence", Map.of(721 "type", "object",722 "properties", Map.of(723 "frequency", Map.of("enum", List.of("daily", "weekly", "monthly")),724 "count", Map.of("type", "integer", "minimum", 1)725 )726 )727 )))728 .required(List.of("title", "start", "end"))729 .build())730 .build();731732 // Ask for at most one tool call per turn so the single-turn flow below733 // stays predictable.734 ToolChoiceAuto toolChoice = ToolChoiceAuto.builder()735 .disableParallelToolUse(true)736 .build();737738 String userPrompt =739 "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.";740741 // Send the user's request along with the tool definition. Claude decides742 // whether to call the tool based on the request and the tool description.743 Message response = client.messages().create(MessageCreateParams.builder()744 .model(Model.CLAUDE_OPUS_5)745 .maxTokens(1024L)746 .addTool(calendarTool)747 .toolChoice(toolChoice)748 .addUserMessage(userPrompt)749 .build());750751 // When Claude calls a tool, the response has stop_reason "tool_use"752 // and the content array contains a tool_use block alongside any text.753 IO.println("stop_reason: " + response.stopReason().orElse(null));754755 // Find the tool_use block. A response may contain text blocks before the756 // tool_use block, so scan the content array rather than assuming position.757 ToolUseBlock toolUse = response.content().stream()758 .flatMap(block -> block.toolUse().stream())759 .findFirst()760 .orElseThrow();761 IO.println("Tool: " + toolUse.name());762 IO.println("Input: " + toolUse._input());763764 // Execute the tool. In a real system this would call your calendar API.765 // Here the result is hardcoded to keep the example self-contained.766 String result = "{\"event_id\": \"evt_123\", \"status\": \"created\"}";767768 // Send the result back. The tool_result block goes in a user message and769 // its tool_use_id must match the id from the tool_use block above. The770 // assistant's previous response is included so Claude has the full history.771 Message followup = client.messages().create(MessageCreateParams.builder()772 .model(Model.CLAUDE_OPUS_5)773 .maxTokens(1024L)774 .addTool(calendarTool)775 .toolChoice(toolChoice)776 .addUserMessage(userPrompt)777 .addMessage(response)778 .addUserMessageOfBlockParams(List.of(ContentBlockParam.ofToolResult(779 ToolResultBlockParam.builder()780 .toolUseId(toolUse.id())781 .content(result)782 .build())))783 .build());784785 // With the tool result in hand, Claude produces a final natural-language786 // answer and stop_reason becomes "end_turn".787 IO.println("stop_reason: " + followup.stopReason().orElse(null));788 followup.content().stream()789 .flatMap(block -> block.text().stream())790 .forEach(textBlock -> IO.println(textBlock.text()));791 }792 ```793794 ```php PHP795 <?php796797 // Ring 1: Single tool, single turn.798799 use Anthropic\Client;800 use Anthropic\Messages\ToolChoiceAuto;801802 // Create a client. It reads ANTHROPIC_API_KEY from the environment.803 $client = new Client();804805 // Define one tool. The input_schema is a JSON Schema object describing806 // the arguments Claude should pass when it calls this tool. This schema807 // includes nested objects (recurrence), arrays (attendees), and optional808 // fields, which is closer to real-world tools than a flat string argument.809 $tools = [810 [811 'name' => 'create_calendar_event',812 'description' => 'Create a calendar event with attendees and optional recurrence.',813 'input_schema' => [814 'type' => 'object',815 'properties' => [816 'title' => ['type' => 'string'],817 'start' => ['type' => 'string', 'format' => 'date-time'],818 'end' => ['type' => 'string', 'format' => 'date-time'],819 'attendees' => [820 'type' => 'array',821 'items' => ['type' => 'string', 'format' => 'email'],822 ],823 'recurrence' => [824 'type' => 'object',825 'properties' => [826 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']],827 'count' => ['type' => 'integer', 'minimum' => 1],828 ],829 ],830 ],831 'required' => ['title', 'start', 'end'],832 ],833 ],834 ];835836 $userMessage = [837 'role' => 'user',838 'content' => 'Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.',839 ];840841 // Ask for at most one tool call per turn so the single-turn flow below842 // stays predictable.843 $toolChoice = ToolChoiceAuto::with(disableParallelToolUse: true);844845 // Send the user's request along with the tool definition. Claude decides846 // whether to call the tool based on the request and the tool description.847 $response = $client->messages->create(848 model: 'claude-opus-5',849 maxTokens: 1024,850 tools: $tools,851 toolChoice: $toolChoice,852 messages: [$userMessage],853 );854855 // When Claude calls a tool, the response has stop_reason "tool_use"856 // and the content array contains a tool_use block alongside any text.857 printf("stop_reason: %s\n", $response->stopReason);858859 // Find the tool_use block. A response may contain text blocks before the860 // tool_use block, so scan the content array rather than assuming position.861 $toolUse = null;862 foreach ($response->content as $block) {863 if ($block->type === 'tool_use') {864 $toolUse = $block;865 break;866 }867 }868 printf("Tool: %s\n", $toolUse->name);869 printf("Input: %s\n", json_encode($toolUse->input));870871 // Execute the tool. In a real system this would call your calendar API.872 // Here the result is hardcoded to keep the example self-contained.873 $result = ['event_id' => 'evt_123', 'status' => 'created'];874875 // Send the result back. The tool_result block goes in a user message and876 // its tool_use_id must match the id from the tool_use block above. The877 // assistant's previous response is included so Claude has the full history.878 $followup = $client->messages->create(879 model: 'claude-opus-5',880 maxTokens: 1024,881 tools: $tools,882 toolChoice: $toolChoice,883 messages: [884 $userMessage,885 ['role' => 'assistant', 'content' => $response->content],886 [887 'role' => 'user',888 'content' => [889 [890 'type' => 'tool_result',891 'tool_use_id' => $toolUse->id,892 'content' => json_encode($result),893 ],894 ],895 ],896 ],897 );898899 // With the tool result in hand, Claude produces a final natural-language900 // answer and stop_reason becomes "end_turn".901 printf("stop_reason: %s\n", $followup->stopReason);902 foreach ($followup->content as $block) {903 if ($block->type === 'text') {904 echo $block->text, "\n";905 }906 }907 ```908909 ```ruby Ruby910 # Ring 1: Single tool, single turn.911912 require "anthropic"913914 # Create a client. It reads ANTHROPIC_API_KEY from the environment.915 client = Anthropic::Client.new916917 # Define one tool. The input_schema is a JSON Schema object describing918 # the arguments Claude should pass when it calls this tool. This schema919 # includes nested objects (recurrence), arrays (attendees), and optional920 # fields, which is closer to real-world tools than a flat string argument.921 tools = [922 {923 name: "create_calendar_event",924 description: "Create a calendar event with attendees and optional recurrence.",925 input_schema: {926 type: "object",927 properties: {928 title: {type: "string"},929 start: {type: "string", format: "date-time"},930 end: {type: "string", format: "date-time"},931 attendees: {932 type: "array",933 items: {type: "string", format: "email"}934 },935 recurrence: {936 type: "object",937 properties: {938 frequency: {enum: ["daily", "weekly", "monthly"]},939 count: {type: "integer", minimum: 1}940 }941 }942 },943 required: ["title", "start", "end"]944 }945 }946 ]947948 user_message = {949 role: "user",950 content: "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am."951 }952953 # Ask for at most one tool call per turn so the single-turn flow below954 # stays predictable.955 tool_choice = {type: "auto", disable_parallel_tool_use: true}956957 # Send the user's request along with the tool definition. Claude decides958 # whether to call the tool based on the request and the tool description.959 response = client.messages.create(960 model: "claude-opus-5",961 max_tokens: 1024,962 tools: tools,963 tool_choice: tool_choice,964 messages: [user_message]965 )966967 # When Claude calls a tool, the response has stop_reason "tool_use"968 # and the content array contains a tool_use block alongside any text.969 puts "stop_reason: #{response.stop_reason}"970971 # Find the tool_use block. A response may contain text blocks before the972 # tool_use block, so scan the content array rather than assuming position.973 tool_use = response.content.find { |block| block.type == :tool_use }974 puts "Tool: #{tool_use.name}"975 puts "Input: #{tool_use.input}"976977 # Execute the tool. In a real system this would call your calendar API.978 # Here the result is hardcoded to keep the example self-contained.979 result = {event_id: "evt_123", status: "created"}980981 # Send the result back. The tool_result block goes in a user message and982 # its tool_use_id must match the id from the tool_use block above. The983 # assistant's previous response is included so Claude has the full history.984 followup = client.messages.create(985 model: "claude-opus-5",986 max_tokens: 1024,987 tools: tools,988 tool_choice: tool_choice,989 messages: [990 user_message,991 {role: "assistant", content: response.content},992 {993 role: "user",994 content: [995 {996 type: "tool_result",997 tool_use_id: tool_use.id,998 content: JSON.generate(result)999 }1000 ]1001 }1002 ]1003 )10041005 # With the tool result in hand, Claude produces a final natural-language1006 # answer and stop_reason becomes "end_turn".1007 puts "stop_reason: #{followup.stop_reason}"1008 followup.content.each do |block|1009 puts block.text if block.type == :text1010 end1011 ```1012</CodeGroup>10131014**What to expect**10151016```text Output wrap1017stop_reason: tool_use1018Tool: create_calendar_event1019Input: {'title': 'Sync', 'start': '2026-03-30T10:00:00', 'end': '2026-03-30T10:30:00', 'attendees': ['alice@example.com', 'bob@example.com']}1020stop_reason: end_turn1021I've scheduled your 30-minute sync with Alice and Bob for Monday, March 30 at 10am.1022```10231024The first `stop_reason` is `tool_use` because Claude is waiting for the calendar result. After you send the result, the second `stop_reason` is `end_turn` and the content is natural language for the user.10251026## Ring 2: The agentic loop10271028Ring 1 assumed Claude would call the tool exactly once. Real tasks often need several calls: Claude might create an event, read the confirmation, then create another. The fix is a `while` loop that keeps running tools and feeding results back until `stop_reason` is no longer `"tool_use"`.10291030The other change is conversation history. Instead of rebuilding the `messages` array from scratch on each request, keep a running list and append to it. Every turn sees the complete prior context.10311032<CodeGroup>1033 ```bash cURL1034 #!/bin/bash1035 # Ring 2: The agentic loop.10361037 TOOLS='[1038 {1039 "name": "create_calendar_event",1040 "description": "Create a calendar event with attendees and optional recurrence.",1041 "input_schema": {1042 "type": "object",1043 "properties": {1044 "title": {"type": "string"},1045 "start": {"type": "string", "format": "date-time"},1046 "end": {"type": "string", "format": "date-time"},1047 "attendees": {"type": "array", "items": {"type": "string", "format": "email"}},1048 "recurrence": {1049 "type": "object",1050 "properties": {1051 "frequency": {"enum": ["daily", "weekly", "monthly"]},1052 "count": {"type": "integer", "minimum": 1}1053 }1054 }1055 },1056 "required": ["title", "start", "end"]1057 }1058 }1059 ]'10601061 run_tool() {1062 local name="$1"1063 local input="$2"1064 if [ "$name" = "create_calendar_event" ]; then1065 local title=$(echo "$input" | jq -r '.title')1066 jq -n --arg title "$title" '{event_id: "evt_123", status: "created", title: $title}'1067 else1068 echo "{\"error\": \"Unknown tool: $name\"}"1069 fi1070 }10711072 # Keep the full conversation history in a JSON array so each turn sees prior context.1073 MESSAGES='[{"role": "user", "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com."}]'10741075 call_api() {1076 curl -s https://api.anthropic.com/v1/messages \1077 -H "x-api-key: $ANTHROPIC_API_KEY" \1078 -H "anthropic-version: 2023-06-01" \1079 -H "content-type: application/json" \1080 -d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \1081 '{model: "claude-opus-5", max_tokens: 1024, tools: $tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: $messages}')"1082 }10831084 RESPONSE=$(call_api)10851086 # Loop until Claude stops asking for tools. Each iteration runs the requested1087 # tool, appends the result to history, and asks Claude to continue.1088 while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do1089 TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")')1090 TOOL_NAME=$(echo "$TOOL_USE" | jq -r '.name')1091 TOOL_INPUT=$(echo "$TOOL_USE" | jq -c '.input')1092 TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id')1093 RESULT=$(run_tool "$TOOL_NAME" "$TOOL_INPUT")10941095 ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content')1096 MESSAGES=$(echo "$MESSAGES" | jq \1097 --argjson assistant "$ASSISTANT_CONTENT" \1098 --arg tool_use_id "$TOOL_USE_ID" \1099 --arg result "$RESULT" \1100 '. + [1101 {role: "assistant", content: $assistant},1102 {role: "user", content: [{type: "tool_result", tool_use_id: $tool_use_id, content: $result}]}1103 ]')11041105 RESPONSE=$(call_api)1106 done11071108 echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'1109 ```11101111 ```bash CLI1112 #!/usr/bin/env bash1113 # Ring 2: The agentic loop.1114 # Uses jq for cross-turn message-array state — building an agentic loop in shell1115 # requires JSON manipulation beyond ant's single-call --transform scope.1116 set -euo pipefail11171118 run_tool() {1119 local name="$1" input="$2"1120 if [ "$name" = "create_calendar_event" ]; then1121 jq -n --arg title "$(jq -r '.title' <<<"$input")" \1122 '{event_id: "evt_123", status: "created", title: $title}'1123 else1124 printf '{"error": "Unknown tool: %s"}' "$name"1125 fi1126 }11271128 # Keep the full conversation history in a JSON array so each turn sees1129 # prior context.1130 MESSAGES='[{"role": "user", "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com."}]'11311132 call_api() {1133 # ant reads the request body as YAML on stdin: no auth headers, no1134 # hand-built JSON envelope. The static keys (model, tools, tool_choice)1135 # live in a quoted heredoc; the growing messages array is appended as1136 # JSON, which YAML accepts as flow syntax.1137 {1138 cat <<'YAML'1139 model: claude-opus-51140 max_tokens: 10241141 tool_choice: {type: auto, disable_parallel_tool_use: true}1142 tools:1143 - name: create_calendar_event1144 description: Create a calendar event with attendees and optional recurrence.1145 input_schema:1146 type: object1147 properties:1148 title: {type: string}1149 start: {type: string, format: date-time}1150 end: {type: string, format: date-time}1151 attendees:1152 type: array1153 items: {type: string, format: email}1154 recurrence:1155 type: object1156 properties:1157 frequency: {enum: [daily, weekly, monthly]}1158 count: {type: integer, minimum: 1}1159 required: [title, start, end]1160 YAML1161 printf 'messages: %s\n' "$MESSAGES"1162 } | ant messages create --format json1163 }11641165 RESPONSE=$(call_api)11661167 # Loop until Claude stops asking for tools. Each iteration runs the1168 # requested tool, appends the result to history, and asks Claude to1169 # continue.1170 while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do1171 TOOL_USE=$(jq '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")1172 TOOL_NAME=$(jq -r '.name' <<<"$TOOL_USE")1173 TOOL_INPUT=$(jq -c '.input' <<<"$TOOL_USE")1174 TOOL_USE_ID=$(jq -r '.id' <<<"$TOOL_USE")1175 RESULT=$(run_tool "$TOOL_NAME" "$TOOL_INPUT")11761177 MESSAGES=$(jq \1178 --argjson assistant "$(jq '.content' <<<"$RESPONSE")" \1179 --arg tool_use_id "$TOOL_USE_ID" \1180 --arg result "$RESULT" \1181 '. + [1182 {role: "assistant", content: $assistant},1183 {role: "user", content: [1184 {type: "tool_result", tool_use_id: $tool_use_id, content: $result}1185 ]}1186 ]' <<<"$MESSAGES")11871188 RESPONSE=$(call_api)1189 done11901191 jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE"1192 ```11931194 ```python Python1195 # Ring 2: The agentic loop.11961197 import json11981199 import anthropic12001201 client = anthropic.Anthropic()12021203 tools = [1204 {1205 "name": "create_calendar_event",1206 "description": "Create a calendar event with attendees and optional recurrence.",1207 "input_schema": {1208 "type": "object",1209 "properties": {1210 "title": {"type": "string"},1211 "start": {"type": "string", "format": "date-time"},1212 "end": {"type": "string", "format": "date-time"},1213 "attendees": {1214 "type": "array",1215 "items": {"type": "string", "format": "email"},1216 },1217 "recurrence": {1218 "type": "object",1219 "properties": {1220 "frequency": {"enum": ["daily", "weekly", "monthly"]},1221 "count": {"type": "integer", "minimum": 1},1222 },1223 },1224 },1225 "required": ["title", "start", "end"],1226 },1227 }1228 ]122912301231 def run_tool(name, tool_input):1232 if name == "create_calendar_event":1233 return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}1234 return {"error": f"Unknown tool: {name}"}123512361237 # Keep the full conversation history in a list so each turn sees prior context.1238 messages = [1239 {1240 "role": "user",1241 "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",1242 }1243 ]12441245 response = client.messages.create(1246 model="claude-opus-5",1247 max_tokens=1024,1248 tools=tools,1249 tool_choice={"type": "auto", "disable_parallel_tool_use": True},1250 messages=messages,1251 )12521253 # Loop until Claude stops asking for tools. Each iteration runs the requested1254 # tool, appends the result to history, and asks Claude to continue.1255 while response.stop_reason == "tool_use":1256 tool_use = next(block for block in response.content if block.type == "tool_use")1257 result = run_tool(tool_use.name, tool_use.input)12581259 messages.append({"role": "assistant", "content": response.content})1260 messages.append(1261 {1262 "role": "user",1263 "content": [1264 {1265 "type": "tool_result",1266 "tool_use_id": tool_use.id,1267 "content": json.dumps(result),1268 }1269 ],1270 }1271 )12721273 response = client.messages.create(1274 model="claude-opus-5",1275 max_tokens=1024,1276 tools=tools,1277 tool_choice={"type": "auto", "disable_parallel_tool_use": True},1278 messages=messages,1279 )12801281 final_text = next(block for block in response.content if block.type == "text")1282 print(final_text.text)1283 ```12841285 ```typescript TypeScript1286 // Ring 2: The agentic loop.12871288 import Anthropic from "@anthropic-ai/sdk";12891290 const client = new Anthropic();12911292 const tools: Anthropic.Tool[] = [1293 {1294 name: "create_calendar_event",1295 description:1296 "Create a calendar event with attendees and optional recurrence.",1297 input_schema: {1298 type: "object",1299 properties: {1300 title: { type: "string" },1301 start: { type: "string", format: "date-time" },1302 end: { type: "string", format: "date-time" },1303 attendees: {1304 type: "array",1305 items: { type: "string", format: "email" },1306 },1307 recurrence: {1308 type: "object",1309 properties: {1310 frequency: { enum: ["daily", "weekly", "monthly"] },1311 count: { type: "integer", minimum: 1 },1312 },1313 },1314 },1315 required: ["title", "start", "end"],1316 },1317 },1318 ];13191320 function runTool(name: string, input: Record<string, unknown>) {1321 if (name === "create_calendar_event") {1322 return { event_id: "evt_123", status: "created", title: input.title };1323 }1324 return { error: `Unknown tool: ${name}` };1325 }13261327 // Keep the full conversation history so each turn sees prior context.1328 const messages: Anthropic.MessageParam[] = [1329 {1330 role: "user",1331 content:1332 "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",1333 },1334 ];13351336 let response = await client.messages.create({1337 model: "claude-opus-5",1338 max_tokens: 1024,1339 tools,1340 tool_choice: { type: "auto", disable_parallel_tool_use: true },1341 messages,1342 });13431344 // Loop until Claude stops asking for tools. Each iteration runs the requested1345 // tool, appends the result to history, and asks Claude to continue.1346 while (response.stop_reason === "tool_use") {1347 const toolUse = response.content.find(1348 (block): block is Anthropic.ToolUseBlock => block.type === "tool_use",1349 )!;1350 const result = runTool(toolUse.name, toolUse.input as Record<string, unknown>);13511352 messages.push({ role: "assistant", content: response.content });1353 messages.push({1354 role: "user",1355 content: [1356 {1357 type: "tool_result",1358 tool_use_id: toolUse.id,1359 content: JSON.stringify(result),1360 },1361 ],1362 });13631364 response = await client.messages.create({1365 model: "claude-opus-5",1366 max_tokens: 1024,1367 tools,1368 tool_choice: { type: "auto", disable_parallel_tool_use: true },1369 messages,1370 });1371 }13721373 for (const block of response.content) {1374 if (block.type === "text") {1375 console.log(block.text);1376 }1377 }1378 ```13791380 ```csharp C#1381 // Ring 2: The agentic loop.13821383 using System;1384 using System.Collections.Generic;1385 using System.Linq;1386 using System.Text.Json;1387 using System.Threading.Tasks;1388 using Anthropic;1389 using Anthropic.Models.Messages;13901391 AnthropicClient client = new();13921393 List<ToolUnion> tools =1394 [1395 new ToolUnion(new Tool()1396 {1397 Name = "create_calendar_event",1398 Description = "Create a calendar event with attendees and optional recurrence.",1399 InputSchema = new InputSchema()1400 {1401 Properties = new Dictionary<string, JsonElement>1402 {1403 ["title"] = JsonSerializer.SerializeToElement(new { type = "string" }),1404 ["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),1405 ["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),1406 ["attendees"] = JsonSerializer.SerializeToElement(new1407 {1408 type = "array",1409 items = new { type = "string", format = "email" },1410 }),1411 ["recurrence"] = JsonSerializer.SerializeToElement(new1412 {1413 type = "object",1414 properties = new1415 {1416 frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },1417 count = new { type = "integer", minimum = 1 },1418 },1419 }),1420 },1421 Required = ["title", "start", "end"],1422 },1423 }),1424 ];14251426 // Run the requested tool and return its result as a string.1427 string RunTool(ToolUseBlock toolUse)1428 {1429 if (toolUse.Name == "create_calendar_event")1430 {1431 var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : "";1432 return JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title });1433 }1434 return JsonSerializer.Serialize(new { error = $"Unknown tool: {toolUse.Name}" });1435 }14361437 var toolChoice = new ToolChoice(new ToolChoiceAuto { DisableParallelToolUse = true });14381439 // Keep the full conversation history in a list so each turn sees prior context.1440 List<MessageParam> messages =1441 [1442 new()1443 {1444 Role = Role.User,1445 Content = "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",1446 },1447 ];14481449 var response = await client.Messages.Create(new MessageCreateParams1450 {1451 Model = Model.ClaudeOpus5,1452 MaxTokens = 1024,1453 Tools = tools,1454 ToolChoice = toolChoice,1455 Messages = messages,1456 });14571458 // Loop until Claude stops asking for tools. Each iteration runs the requested1459 // tool, appends the result to history, and asks Claude to continue.1460 while (response.StopReason == StopReason.ToolUse)1461 {1462 ToolUseBlock? toolUse = null;1463 foreach (var block in response.Content)1464 {1465 if (block.TryPickToolUse(out var picked))1466 {1467 toolUse = picked;1468 break;1469 }1470 }1471 var result = RunTool(toolUse!);14721473 messages.Add(new()1474 {1475 Role = Role.Assistant,1476 Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(),1477 });1478 messages.Add(new()1479 {1480 Role = Role.User,1481 Content = new MessageParamContent(1482 [1483 new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUse!.ID, Content = result }),1484 ]),1485 });14861487 response = await client.Messages.Create(new MessageCreateParams1488 {1489 Model = Model.ClaudeOpus5,1490 MaxTokens = 1024,1491 Tools = tools,1492 ToolChoice = toolChoice,1493 Messages = messages,1494 });1495 }14961497 foreach (var block in response.Content)1498 {1499 if (block.TryPickText(out var text))1500 {1501 Console.WriteLine(text.Text);1502 }1503 }1504 ```15051506 ```go Go1507 // Ring 2: The agentic loop.15081509 package main15101511 import (1512 "context"1513 "encoding/json"1514 "fmt"1515 "log"15161517 "github.com/anthropics/anthropic-sdk-go"1518 )15191520 func runTool(name string, input map[string]any) string {1521 if name == "create_calendar_event" {1522 title, _ := input["title"].(string)1523 return fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, title)1524 }1525 return fmt.Sprintf(`{"error": "Unknown tool: %s"}`, name)1526 }15271528 func main() {1529 client := anthropic.NewClient()1530 ctx := context.Background()15311532 tools := []anthropic.ToolUnionParam{1533 {OfTool: &anthropic.ToolParam{1534 Name: "create_calendar_event",1535 Description: anthropic.String("Create a calendar event with attendees and optional recurrence."),1536 InputSchema: anthropic.ToolInputSchemaParam{1537 Properties: map[string]any{1538 "title": map[string]any{"type": "string"},1539 "start": map[string]any{"type": "string", "format": "date-time"},1540 "end": map[string]any{"type": "string", "format": "date-time"},1541 "attendees": map[string]any{1542 "type": "array",1543 "items": map[string]any{"type": "string", "format": "email"},1544 },1545 "recurrence": map[string]any{1546 "type": "object",1547 "properties": map[string]any{1548 "frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}},1549 "count": map[string]any{"type": "integer", "minimum": 1},1550 },1551 },1552 },1553 Required: []string{"title", "start", "end"},1554 },1555 }},1556 }15571558 toolChoice := anthropic.ToolChoiceUnionParam{1559 OfAuto: &anthropic.ToolChoiceAutoParam{DisableParallelToolUse: anthropic.Bool(true)},1560 }15611562 // Keep the full conversation history in a slice so each turn sees prior context.1563 messages := []anthropic.MessageParam{1564 anthropic.NewUserMessage(anthropic.NewTextBlock(1565 "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",1566 )),1567 }15681569 response, err := client.Messages.New(ctx, anthropic.MessageNewParams{1570 Model: anthropic.ModelClaudeOpus5,1571 MaxTokens: 1024,1572 Tools: tools,1573 ToolChoice: toolChoice,1574 Messages: messages,1575 })1576 if err != nil {1577 log.Fatal(err)1578 }15791580 // Loop until Claude stops asking for tools. Each iteration runs the requested1581 // tool, appends the result to history, and asks Claude to continue.1582 for response.StopReason == "tool_use" {1583 var toolUse anthropic.ContentBlockUnion1584 for _, block := range response.Content {1585 if block.Type == "tool_use" {1586 toolUse = block1587 break1588 }1589 }15901591 var input map[string]any1592 if err := json.Unmarshal(toolUse.Input, &input); err != nil {1593 log.Fatal(err)1594 }1595 result := runTool(toolUse.Name, input)15961597 var assistantContent []anthropic.ContentBlockParamUnion1598 for _, block := range response.Content {1599 assistantContent = append(assistantContent, block.ToParam())1600 }1601 messages = append(messages, anthropic.NewAssistantMessage(assistantContent...))1602 messages = append(messages, anthropic.NewUserMessage(1603 anthropic.NewToolResultBlock(toolUse.ID, result, false),1604 ))16051606 response, err = client.Messages.New(ctx, anthropic.MessageNewParams{1607 Model: anthropic.ModelClaudeOpus5,1608 MaxTokens: 1024,1609 Tools: tools,1610 ToolChoice: toolChoice,1611 Messages: messages,1612 })1613 if err != nil {1614 log.Fatal(err)1615 }1616 }16171618 for _, block := range response.Content {1619 if block.Type == "text" {1620 fmt.Println(block.Text)1621 }1622 }1623 }1624 ```16251626 ```java Java1627 // Ring 2: The agentic loop.16281629 import com.anthropic.client.AnthropicClient;1630 import com.anthropic.client.okhttp.AnthropicOkHttpClient;1631 import com.anthropic.core.JsonValue;1632 import com.anthropic.models.messages.ContentBlockParam;1633 import com.anthropic.models.messages.Message;1634 import com.anthropic.models.messages.MessageCreateParams;1635 import com.anthropic.models.messages.MessageParam;1636 import com.anthropic.models.messages.Model;1637 import com.anthropic.models.messages.StopReason;1638 import com.anthropic.models.messages.Tool;1639 import com.anthropic.models.messages.Tool.InputSchema;1640 import com.anthropic.models.messages.ToolChoiceAuto;1641 import com.anthropic.models.messages.ToolResultBlockParam;1642 import com.anthropic.models.messages.ToolUseBlock;1643 import java.util.ArrayList;1644 import java.util.List;1645 import java.util.Map;16461647 String runTool(ToolUseBlock toolUse) {1648 // The raw tool input is a JSON object; read fields out of it as a map.1649 Map<String, JsonValue> input = (Map<String, JsonValue>) toolUse._input().asObject().get();1650 if (toolUse.name().equals("create_calendar_event")) {1651 String title = input.containsKey("title") ? input.get("title").asStringOrThrow() : "";1652 return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}";1653 }1654 return "{\"error\": \"Unknown tool: " + toolUse.name() + "\"}";1655 }16561657 void main() {1658 AnthropicClient client = AnthropicOkHttpClient.fromEnv();16591660 Tool calendarTool = Tool.builder()1661 .name("create_calendar_event")1662 .description("Create a calendar event with attendees and optional recurrence.")1663 .inputSchema(InputSchema.builder()1664 .properties(JsonValue.from(Map.of(1665 "title", Map.of("type", "string"),1666 "start", Map.of("type", "string", "format", "date-time"),1667 "end", Map.of("type", "string", "format", "date-time"),1668 "attendees", Map.of(1669 "type", "array",1670 "items", Map.of("type", "string", "format", "email")1671 ),1672 "recurrence", Map.of(1673 "type", "object",1674 "properties", Map.of(1675 "frequency", Map.of("enum", List.of("daily", "weekly", "monthly")),1676 "count", Map.of("type", "integer", "minimum", 1)1677 )1678 )1679 )))1680 .required(List.of("title", "start", "end"))1681 .build())1682 .build();16831684 ToolChoiceAuto toolChoice = ToolChoiceAuto.builder()1685 .disableParallelToolUse(true)1686 .build();16871688 // Keep the full conversation history in a list so each turn sees prior context.1689 List<MessageParam> messages = new ArrayList<>();1690 messages.add(MessageParam.builder()1691 .role(MessageParam.Role.USER)1692 .content("Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.")1693 .build());16941695 Message response = client.messages().create(MessageCreateParams.builder()1696 .model(Model.CLAUDE_OPUS_5)1697 .maxTokens(1024L)1698 .addTool(calendarTool)1699 .toolChoice(toolChoice)1700 .messages(messages)1701 .build());17021703 // Loop until Claude stops asking for tools. Each iteration runs the requested1704 // tool, appends the result to history, and asks Claude to continue.1705 while (response.stopReason().isPresent()1706 && response.stopReason().get().equals(StopReason.TOOL_USE)) {1707 ToolUseBlock toolUse = response.content().stream()1708 .flatMap(block -> block.toolUse().stream())1709 .findFirst()1710 .orElseThrow();1711 String result = runTool(toolUse);17121713 messages.add(response.toParam());1714 messages.add(MessageParam.builder()1715 .role(MessageParam.Role.USER)1716 .contentOfBlockParams(List.of(ContentBlockParam.ofToolResult(1717 ToolResultBlockParam.builder()1718 .toolUseId(toolUse.id())1719 .content(result)1720 .build())))1721 .build());17221723 response = client.messages().create(MessageCreateParams.builder()1724 .model(Model.CLAUDE_OPUS_5)1725 .maxTokens(1024L)1726 .addTool(calendarTool)1727 .toolChoice(toolChoice)1728 .messages(messages)1729 .build());1730 }17311732 response.content().stream()1733 .flatMap(block -> block.text().stream())1734 .forEach(textBlock -> IO.println(textBlock.text()));1735 }1736 ```17371738 ```php PHP1739 <?php17401741 // Ring 2: The agentic loop.17421743 use Anthropic\Client;1744 use Anthropic\Messages\ToolChoiceAuto;17451746 $client = new Client();17471748 $tools = [1749 [1750 'name' => 'create_calendar_event',1751 'description' => 'Create a calendar event with attendees and optional recurrence.',1752 'input_schema' => [1753 'type' => 'object',1754 'properties' => [1755 'title' => ['type' => 'string'],1756 'start' => ['type' => 'string', 'format' => 'date-time'],1757 'end' => ['type' => 'string', 'format' => 'date-time'],1758 'attendees' => [1759 'type' => 'array',1760 'items' => ['type' => 'string', 'format' => 'email'],1761 ],1762 'recurrence' => [1763 'type' => 'object',1764 'properties' => [1765 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']],1766 'count' => ['type' => 'integer', 'minimum' => 1],1767 ],1768 ],1769 ],1770 'required' => ['title', 'start', 'end'],1771 ],1772 ],1773 ];17741775 function runTool(string $name, array $input): string1776 {1777 if ($name === 'create_calendar_event') {1778 return json_encode([1779 'event_id' => 'evt_123',1780 'status' => 'created',1781 'title' => $input['title'],1782 ]);1783 }17841785 return json_encode(['error' => "Unknown tool: {$name}"]);1786 }17871788 $toolChoice = ToolChoiceAuto::with(disableParallelToolUse: true);17891790 // Keep the full conversation history in an array so each turn sees prior context.1791 $messages = [1792 [1793 'role' => 'user',1794 'content' => 'Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.',1795 ],1796 ];17971798 $response = $client->messages->create(1799 model: 'claude-opus-5',1800 maxTokens: 1024,1801 tools: $tools,1802 toolChoice: $toolChoice,1803 messages: $messages,1804 );18051806 // Loop until Claude stops asking for tools. Each iteration runs the requested1807 // tool, appends the result to history, and asks Claude to continue.1808 while ($response->stopReason === 'tool_use') {1809 $toolUse = null;1810 foreach ($response->content as $block) {1811 if ($block->type === 'tool_use') {1812 $toolUse = $block;1813 break;1814 }1815 }18161817 $result = runTool($toolUse->name, $toolUse->input);18181819 $messages[] = ['role' => 'assistant', 'content' => $response->content];1820 $messages[] = [1821 'role' => 'user',1822 'content' => [1823 [1824 'type' => 'tool_result',1825 'tool_use_id' => $toolUse->id,1826 'content' => $result,1827 ],1828 ],1829 ];18301831 $response = $client->messages->create(1832 model: 'claude-opus-5',1833 maxTokens: 1024,1834 tools: $tools,1835 toolChoice: $toolChoice,1836 messages: $messages,1837 );1838 }18391840 foreach ($response->content as $block) {1841 if ($block->type === 'text') {1842 echo $block->text, "\n";1843 }1844 }1845 ```18461847 ```ruby Ruby1848 # Ring 2: The agentic loop.18491850 require "anthropic"18511852 client = Anthropic::Client.new18531854 tools = [1855 {1856 name: "create_calendar_event",1857 description: "Create a calendar event with attendees and optional recurrence.",1858 input_schema: {1859 type: "object",1860 properties: {1861 title: {type: "string"},1862 start: {type: "string", format: "date-time"},1863 end: {type: "string", format: "date-time"},1864 attendees: {1865 type: "array",1866 items: {type: "string", format: "email"}1867 },1868 recurrence: {1869 type: "object",1870 properties: {1871 frequency: {enum: ["daily", "weekly", "monthly"]},1872 count: {type: "integer", minimum: 1}1873 }1874 }1875 },1876 required: ["title", "start", "end"]1877 }1878 }1879 ]18801881 def run_tool(name, input)1882 case name1883 when "create_calendar_event"1884 JSON.generate({event_id: "evt_123", status: "created", title: input[:title]})1885 else1886 JSON.generate({error: "Unknown tool: #{name}"})1887 end1888 end18891890 tool_choice = {type: "auto", disable_parallel_tool_use: true}18911892 # Keep the full conversation history in an array so each turn sees prior context.1893 messages = [1894 {1895 role: "user",1896 content: "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com."1897 }1898 ]18991900 response = client.messages.create(1901 model: "claude-opus-5",1902 max_tokens: 1024,1903 tools: tools,1904 tool_choice: tool_choice,1905 messages: messages1906 )19071908 # Loop until Claude stops asking for tools. Each iteration runs the requested1909 # tool, appends the result to history, and asks Claude to continue.1910 while response.stop_reason == :tool_use1911 tool_use = response.content.find { |block| block.type == :tool_use }1912 result = run_tool(tool_use.name, tool_use.input)19131914 messages << {role: "assistant", content: response.content}1915 messages << {1916 role: "user",1917 content: [1918 {1919 type: "tool_result",1920 tool_use_id: tool_use.id,1921 content: result1922 }1923 ]1924 }19251926 response = client.messages.create(1927 model: "claude-opus-5",1928 max_tokens: 1024,1929 tools: tools,1930 tool_choice: tool_choice,1931 messages: messages1932 )1933 end19341935 response.content.each do |block|1936 puts block.text if block.type == :text1937 end1938 ```1939</CodeGroup>19401941**What to expect**19421943```text Output wrap1944I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.1945```19461947The loop might run once or several times depending on how Claude breaks down the task. Your code no longer needs to know in advance.19481949## Ring 3: Multiple tools, parallel calls19501951Agents rarely have just one capability. Add a second tool, `list_calendar_events`, so Claude can check the existing schedule before creating something new.19521953When Claude has multiple independent tool calls to make, it might return several `tool_use` blocks in a single response. Your loop needs to process all of them and send back all results together in one user message. Iterate over every `tool_use` block in `response.content`, not just the first.19541955<CodeGroup>1956 ```bash cURL1957 #!/bin/bash1958 # Ring 3: Multiple tools, parallel calls.19591960 TOOLS='[1961 {1962 "name": "create_calendar_event",1963 "description": "Create a calendar event with attendees and optional recurrence.",1964 "input_schema": {1965 "type": "object",1966 "properties": {1967 "title": {"type": "string"},1968 "start": {"type": "string", "format": "date-time"},1969 "end": {"type": "string", "format": "date-time"},1970 "attendees": {"type": "array", "items": {"type": "string", "format": "email"}},1971 "recurrence": {1972 "type": "object",1973 "properties": {1974 "frequency": {"enum": ["daily", "weekly", "monthly"]},1975 "count": {"type": "integer", "minimum": 1}1976 }1977 }1978 },1979 "required": ["title", "start", "end"]1980 }1981 },1982 {1983 "name": "list_calendar_events",1984 "description": "List all calendar events on a given date.",1985 "input_schema": {1986 "type": "object",1987 "properties": {"date": {"type": "string", "format": "date"}},1988 "required": ["date"]1989 }1990 }1991 ]'19921993 run_tool() {1994 case "$1" in1995 create_calendar_event)1996 jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;;1997 list_calendar_events)1998 echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;1999 *)2000 echo "{\"error\": \"Unknown tool: $1\"}" ;;2001 esac2002 }20032004 MESSAGES='[{"role": "user", "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts."}]'20052006 call_api() {2007 curl -s https://api.anthropic.com/v1/messages \2008 -H "x-api-key: $ANTHROPIC_API_KEY" \2009 -H "anthropic-version: 2023-06-01" \2010 -H "content-type: application/json" \2011 -d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \2012 '{model: "claude-opus-5", max_tokens: 1024, tools: $tools, messages: $messages}')"2013 }20142015 RESPONSE=$(call_api)20162017 while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do2018 # A single response can contain multiple tool_use blocks. Process all of2019 # them and return all results together in one user message.2020 TOOL_RESULTS='[]'2021 while read -r block; do2022 NAME=$(echo "$block" | jq -r '.name')2023 INPUT=$(echo "$block" | jq -c '.input')2024 ID=$(echo "$block" | jq -r '.id')2025 RESULT=$(run_tool "$NAME" "$INPUT")2026 TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$RESULT" \2027 '. + [{type: "tool_result", tool_use_id: $id, content: $result}]')2028 done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")')20292030 MESSAGES=$(echo "$MESSAGES" | jq \2031 --argjson assistant "$(echo "$RESPONSE" | jq '.content')" \2032 --argjson results "$TOOL_RESULTS" \2033 '. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]')20342035 RESPONSE=$(call_api)2036 done20372038 echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'2039 ```20402041 ```bash CLI2042 #!/usr/bin/env bash2043 # Ring 3: Multiple tools, parallel calls.2044 # Uses jq for cross-turn message-array state — building an agentic loop in shell2045 # requires JSON manipulation beyond ant's single-call --transform scope.2046 set -euo pipefail20472048 run_tool() {2049 case "$1" in2050 create_calendar_event)2051 jq -n --arg title "$(jq -r '.title' <<<"$2")" \2052 '{event_id: "evt_123", status: "created", title: $title}' ;;2053 list_calendar_events)2054 echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;2055 *)2056 printf '{"error": "Unknown tool: %s"}' "$1" ;;2057 esac2058 }20592060 MESSAGES='[{"role": "user", "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts."}]'20612062 call_api() {2063 # ant reads the request body as YAML on stdin: no auth headers, no2064 # hand-built JSON envelope. The static keys (model, tools) live in a2065 # quoted heredoc; the growing messages array is appended as JSON,2066 # which YAML accepts as flow syntax.2067 {2068 cat <<'YAML'2069 model: claude-opus-52070 max_tokens: 10242071 tools:2072 - name: create_calendar_event2073 description: Create a calendar event with attendees and optional recurrence.2074 input_schema:2075 type: object2076 properties:2077 title: {type: string}2078 start: {type: string, format: date-time}2079 end: {type: string, format: date-time}2080 attendees:2081 type: array2082 items: {type: string, format: email}2083 recurrence:2084 type: object2085 properties:2086 frequency: {enum: [daily, weekly, monthly]}2087 count: {type: integer, minimum: 1}2088 required: [title, start, end]2089 - name: list_calendar_events2090 description: List all calendar events on a given date.2091 input_schema:2092 type: object2093 properties:2094 date: {type: string, format: date}2095 required: [date]2096 YAML2097 printf 'messages: %s\n' "$MESSAGES"2098 } | ant messages create --format json2099 }21002101 RESPONSE=$(call_api)21022103 while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do2104 # A single response can contain multiple tool_use blocks. Process all2105 # of them and return all results together in one user message.2106 TOOL_RESULTS='[]'2107 while read -r block; do2108 NAME=$(jq -r '.name' <<<"$block")2109 INPUT=$(jq -c '.input' <<<"$block")2110 ID=$(jq -r '.id' <<<"$block")2111 RESULT=$(run_tool "$NAME" "$INPUT")2112 TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$RESULT" \2113 '. + [{type: "tool_result", tool_use_id: $id, content: $result}]' \2114 <<<"$TOOL_RESULTS")2115 done < <(jq -c '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")21162117 MESSAGES=$(jq \2118 --argjson assistant "$(jq '.content' <<<"$RESPONSE")" \2119 --argjson results "$TOOL_RESULTS" \2120 '. + [2121 {role: "assistant", content: $assistant},2122 {role: "user", content: $results}2123 ]' <<<"$MESSAGES")21242125 RESPONSE=$(call_api)2126 done21272128 jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE"2129 ```21302131 ```python Python2132 # Ring 3: Multiple tools, parallel calls.21332134 import json21352136 import anthropic21372138 client = anthropic.Anthropic()21392140 tools = [2141 {2142 "name": "create_calendar_event",2143 "description": "Create a calendar event with attendees and optional recurrence.",2144 "input_schema": {2145 "type": "object",2146 "properties": {2147 "title": {"type": "string"},2148 "start": {"type": "string", "format": "date-time"},2149 "end": {"type": "string", "format": "date-time"},2150 "attendees": {2151 "type": "array",2152 "items": {"type": "string", "format": "email"},2153 },2154 "recurrence": {2155 "type": "object",2156 "properties": {2157 "frequency": {"enum": ["daily", "weekly", "monthly"]},2158 "count": {"type": "integer", "minimum": 1},2159 },2160 },2161 },2162 "required": ["title", "start", "end"],2163 },2164 },2165 {2166 "name": "list_calendar_events",2167 "description": "List all calendar events on a given date.",2168 "input_schema": {2169 "type": "object",2170 "properties": {2171 "date": {"type": "string", "format": "date"},2172 },2173 "required": ["date"],2174 },2175 },2176 ]217721782179 def run_tool(name, tool_input):2180 if name == "create_calendar_event":2181 return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}2182 if name == "list_calendar_events":2183 return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}2184 return {"error": f"Unknown tool: {name}"}218521862187 messages = [2188 {2189 "role": "user",2190 "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",2191 }2192 ]21932194 response = client.messages.create(2195 model="claude-opus-5",2196 max_tokens=1024,2197 tools=tools,2198 messages=messages,2199 )22002201 while response.stop_reason == "tool_use":2202 # A single response can contain multiple tool_use blocks. Process all of2203 # them and return all results together in one user message.2204 tool_results = []2205 for block in response.content:2206 if block.type == "tool_use":2207 result = run_tool(block.name, block.input)2208 tool_results.append(2209 {2210 "type": "tool_result",2211 "tool_use_id": block.id,2212 "content": json.dumps(result),2213 }2214 )22152216 messages.append({"role": "assistant", "content": response.content})2217 messages.append({"role": "user", "content": tool_results})22182219 response = client.messages.create(2220 model="claude-opus-5",2221 max_tokens=1024,2222 tools=tools,2223 messages=messages,2224 )22252226 final_text = next(block for block in response.content if block.type == "text")2227 print(final_text.text)2228 ```22292230 ```typescript TypeScript2231 // Ring 3: Multiple tools, parallel calls.22322233 import Anthropic from "@anthropic-ai/sdk";22342235 const client = new Anthropic();22362237 const tools: Anthropic.Tool[] = [2238 {2239 name: "create_calendar_event",2240 description:2241 "Create a calendar event with attendees and optional recurrence.",2242 input_schema: {2243 type: "object",2244 properties: {2245 title: { type: "string" },2246 start: { type: "string", format: "date-time" },2247 end: { type: "string", format: "date-time" },2248 attendees: {2249 type: "array",2250 items: { type: "string", format: "email" },2251 },2252 recurrence: {2253 type: "object",2254 properties: {2255 frequency: { enum: ["daily", "weekly", "monthly"] },2256 count: { type: "integer", minimum: 1 },2257 },2258 },2259 },2260 required: ["title", "start", "end"],2261 },2262 },2263 {2264 name: "list_calendar_events",2265 description: "List all calendar events on a given date.",2266 input_schema: {2267 type: "object",2268 properties: {2269 date: { type: "string", format: "date" },2270 },2271 required: ["date"],2272 },2273 },2274 ];22752276 function runTool(name: string, input: Record<string, unknown>) {2277 if (name === "create_calendar_event") {2278 return { event_id: "evt_123", status: "created", title: input.title };2279 }2280 if (name === "list_calendar_events") {2281 return {2282 events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }],2283 };2284 }2285 return { error: `Unknown tool: ${name}` };2286 }22872288 const messages: Anthropic.MessageParam[] = [2289 {2290 role: "user",2291 content:2292 "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",2293 },2294 ];22952296 let response = await client.messages.create({2297 model: "claude-opus-5",2298 max_tokens: 1024,2299 tools,2300 messages,2301 });23022303 while (response.stop_reason === "tool_use") {2304 // A single response can contain multiple tool_use blocks. Process all of2305 // them and return all results together in one user message.2306 const toolResults: Anthropic.ToolResultBlockParam[] = [];2307 for (const block of response.content) {2308 if (block.type === "tool_use") {2309 const result = runTool(block.name, block.input as Record<string, unknown>);2310 toolResults.push({2311 type: "tool_result",2312 tool_use_id: block.id,2313 content: JSON.stringify(result),2314 });2315 }2316 }23172318 messages.push({ role: "assistant", content: response.content });2319 messages.push({ role: "user", content: toolResults });23202321 response = await client.messages.create({2322 model: "claude-opus-5",2323 max_tokens: 1024,2324 tools,2325 messages,2326 });2327 }23282329 for (const block of response.content) {2330 if (block.type === "text") {2331 console.log(block.text);2332 }2333 }2334 ```23352336 ```csharp C#2337 // Ring 3: Multiple tools, parallel calls.23382339 using System;2340 using System.Collections.Generic;2341 using System.Linq;2342 using System.Text.Json;2343 using System.Threading.Tasks;2344 using Anthropic;2345 using Anthropic.Models.Messages;23462347 AnthropicClient client = new();23482349 List<ToolUnion> tools =2350 [2351 new ToolUnion(new Tool()2352 {2353 Name = "create_calendar_event",2354 Description = "Create a calendar event with attendees and optional recurrence.",2355 InputSchema = new InputSchema()2356 {2357 Properties = new Dictionary<string, JsonElement>2358 {2359 ["title"] = JsonSerializer.SerializeToElement(new { type = "string" }),2360 ["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),2361 ["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),2362 ["attendees"] = JsonSerializer.SerializeToElement(new2363 {2364 type = "array",2365 items = new { type = "string", format = "email" },2366 }),2367 ["recurrence"] = JsonSerializer.SerializeToElement(new2368 {2369 type = "object",2370 properties = new2371 {2372 frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },2373 count = new { type = "integer", minimum = 1 },2374 },2375 }),2376 },2377 Required = ["title", "start", "end"],2378 },2379 }),2380 new ToolUnion(new Tool()2381 {2382 Name = "list_calendar_events",2383 Description = "List all calendar events on a given date.",2384 InputSchema = new InputSchema()2385 {2386 Properties = new Dictionary<string, JsonElement>2387 {2388 ["date"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date" }),2389 },2390 Required = ["date"],2391 },2392 }),2393 ];23942395 string RunTool(ToolUseBlock toolUse)2396 {2397 if (toolUse.Name == "create_calendar_event")2398 {2399 var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : "";2400 return JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title });2401 }2402 if (toolUse.Name == "list_calendar_events")2403 {2404 return """{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}""";2405 }2406 return JsonSerializer.Serialize(new { error = $"Unknown tool: {toolUse.Name}" });2407 }24082409 List<MessageParam> messages =2410 [2411 new()2412 {2413 Role = Role.User,2414 Content = "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",2415 },2416 ];24172418 var response = await client.Messages.Create(new MessageCreateParams2419 {2420 Model = Model.ClaudeOpus5,2421 MaxTokens = 1024,2422 Tools = tools,2423 Messages = messages,2424 });24252426 while (response.StopReason == StopReason.ToolUse)2427 {2428 // A single response can contain multiple tool_use blocks. Process all of2429 // them and return all results together in one user message.2430 List<ContentBlockParam> toolResults = [];2431 foreach (var block in response.Content)2432 {2433 if (block.TryPickToolUse(out var toolUse))2434 {2435 toolResults.Add(new ContentBlockParam(new ToolResultBlockParam()2436 {2437 ToolUseID = toolUse.ID,2438 Content = RunTool(toolUse),2439 }));2440 }2441 }24422443 messages.Add(new()2444 {2445 Role = Role.Assistant,2446 Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(),2447 });2448 messages.Add(new() { Role = Role.User, Content = new MessageParamContent(toolResults) });24492450 response = await client.Messages.Create(new MessageCreateParams2451 {2452 Model = Model.ClaudeOpus5,2453 MaxTokens = 1024,2454 Tools = tools,2455 Messages = messages,2456 });2457 }24582459 foreach (var block in response.Content)2460 {2461 if (block.TryPickText(out var text))2462 {2463 Console.WriteLine(text.Text);2464 }2465 }2466 ```24672468 ```go Go2469 // Ring 3: Multiple tools, parallel calls.24702471 package main24722473 import (2474 "context"2475 "encoding/json"2476 "fmt"2477 "log"24782479 "github.com/anthropics/anthropic-sdk-go"2480 )24812482 func runTool(name string, input map[string]any) string {2483 if name == "create_calendar_event" {2484 title, _ := input["title"].(string)2485 return fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, title)2486 }2487 if name == "list_calendar_events" {2488 return `{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}`2489 }2490 return fmt.Sprintf(`{"error": "Unknown tool: %s"}`, name)2491 }24922493 func main() {2494 client := anthropic.NewClient()2495 ctx := context.Background()24962497 tools := []anthropic.ToolUnionParam{2498 {OfTool: &anthropic.ToolParam{2499 Name: "create_calendar_event",2500 Description: anthropic.String("Create a calendar event with attendees and optional recurrence."),2501 InputSchema: anthropic.ToolInputSchemaParam{2502 Properties: map[string]any{2503 "title": map[string]any{"type": "string"},2504 "start": map[string]any{"type": "string", "format": "date-time"},2505 "end": map[string]any{"type": "string", "format": "date-time"},2506 "attendees": map[string]any{2507 "type": "array",2508 "items": map[string]any{"type": "string", "format": "email"},2509 },2510 "recurrence": map[string]any{2511 "type": "object",2512 "properties": map[string]any{2513 "frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}},2514 "count": map[string]any{"type": "integer", "minimum": 1},2515 },2516 },2517 },2518 Required: []string{"title", "start", "end"},2519 },2520 }},2521 {OfTool: &anthropic.ToolParam{2522 Name: "list_calendar_events",2523 Description: anthropic.String("List all calendar events on a given date."),2524 InputSchema: anthropic.ToolInputSchemaParam{2525 Properties: map[string]any{2526 "date": map[string]any{"type": "string", "format": "date"},2527 },2528 Required: []string{"date"},2529 },2530 }},2531 }25322533 messages := []anthropic.MessageParam{2534 anthropic.NewUserMessage(anthropic.NewTextBlock(2535 "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",2536 )),2537 }25382539 response, err := client.Messages.New(ctx, anthropic.MessageNewParams{2540 Model: anthropic.ModelClaudeOpus5,2541 MaxTokens: 1024,2542 Tools: tools,2543 Messages: messages,2544 })2545 if err != nil {2546 log.Fatal(err)2547 }25482549 for response.StopReason == "tool_use" {2550 // A single response can contain multiple tool_use blocks. Process all of2551 // them and return all results together in one user message.2552 var toolResults []anthropic.ContentBlockParamUnion2553 for _, block := range response.Content {2554 if block.Type == "tool_use" {2555 var input map[string]any2556 if err := json.Unmarshal(block.Input, &input); err != nil {2557 log.Fatal(err)2558 }2559 result := runTool(block.Name, input)2560 toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false))2561 }2562 }25632564 var assistantContent []anthropic.ContentBlockParamUnion2565 for _, block := range response.Content {2566 assistantContent = append(assistantContent, block.ToParam())2567 }2568 messages = append(messages, anthropic.NewAssistantMessage(assistantContent...))2569 messages = append(messages, anthropic.NewUserMessage(toolResults...))25702571 response, err = client.Messages.New(ctx, anthropic.MessageNewParams{2572 Model: anthropic.ModelClaudeOpus5,2573 MaxTokens: 1024,2574 Tools: tools,2575 Messages: messages,2576 })2577 if err != nil {2578 log.Fatal(err)2579 }2580 }25812582 for _, block := range response.Content {2583 if block.Type == "text" {2584 fmt.Println(block.Text)2585 }2586 }2587 }2588 ```25892590 ```java Java2591 // Ring 3: Multiple tools, parallel calls.25922593 import com.anthropic.client.AnthropicClient;2594 import com.anthropic.client.okhttp.AnthropicOkHttpClient;2595 import com.anthropic.core.JsonValue;2596 import com.anthropic.models.messages.ContentBlock;2597 import com.anthropic.models.messages.ContentBlockParam;2598 import com.anthropic.models.messages.Message;2599 import com.anthropic.models.messages.MessageCreateParams;2600 import com.anthropic.models.messages.MessageParam;2601 import com.anthropic.models.messages.Model;2602 import com.anthropic.models.messages.StopReason;2603 import com.anthropic.models.messages.Tool;2604 import com.anthropic.models.messages.Tool.InputSchema;2605 import com.anthropic.models.messages.ToolResultBlockParam;2606 import com.anthropic.models.messages.ToolUseBlock;2607 import java.util.ArrayList;2608 import java.util.List;2609 import java.util.Map;26102611 String runTool(ToolUseBlock toolUse) {2612 // The raw tool input is a JSON object; read fields out of it as a map.2613 Map<String, JsonValue> input = (Map<String, JsonValue>) toolUse._input().asObject().get();2614 if (toolUse.name().equals("create_calendar_event")) {2615 String title = input.containsKey("title") ? input.get("title").asStringOrThrow() : "";2616 return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}";2617 }2618 if (toolUse.name().equals("list_calendar_events")) {2619 return "{\"events\": [{\"title\": \"Existing meeting\", \"start\": \"14:00\", \"end\": \"15:00\"}]}";2620 }2621 return "{\"error\": \"Unknown tool: " + toolUse.name() + "\"}";2622 }26232624 void main() {2625 AnthropicClient client = AnthropicOkHttpClient.fromEnv();26262627 Tool calendarTool = Tool.builder()2628 .name("create_calendar_event")2629 .description("Create a calendar event with attendees and optional recurrence.")2630 .inputSchema(InputSchema.builder()2631 .properties(JsonValue.from(Map.of(2632 "title", Map.of("type", "string"),2633 "start", Map.of("type", "string", "format", "date-time"),2634 "end", Map.of("type", "string", "format", "date-time"),2635 "attendees", Map.of(2636 "type", "array",2637 "items", Map.of("type", "string", "format", "email")2638 ),2639 "recurrence", Map.of(2640 "type", "object",2641 "properties", Map.of(2642 "frequency", Map.of("enum", List.of("daily", "weekly", "monthly")),2643 "count", Map.of("type", "integer", "minimum", 1)2644 )2645 )2646 )))2647 .required(List.of("title", "start", "end"))2648 .build())2649 .build();26502651 Tool listTool = Tool.builder()2652 .name("list_calendar_events")2653 .description("List all calendar events on a given date.")2654 .inputSchema(InputSchema.builder()2655 .properties(JsonValue.from(Map.of(2656 "date", Map.of("type", "string", "format", "date")2657 )))2658 .required(List.of("date"))2659 .build())2660 .build();26612662 List<MessageParam> messages = new ArrayList<>();2663 messages.add(MessageParam.builder()2664 .role(MessageParam.Role.USER)2665 .content("Check what I have next Monday, then schedule a planning session that avoids any conflicts.")2666 .build());26672668 Message response = client.messages().create(MessageCreateParams.builder()2669 .model(Model.CLAUDE_OPUS_5)2670 .maxTokens(1024L)2671 .addTool(calendarTool)2672 .addTool(listTool)2673 .messages(messages)2674 .build());26752676 while (response.stopReason().isPresent()2677 && response.stopReason().get().equals(StopReason.TOOL_USE)) {2678 // A single response can contain multiple tool_use blocks. Process all of2679 // them and return all results together in one user message.2680 List<ContentBlockParam> toolResults = new ArrayList<>();2681 for (ContentBlock block : response.content()) {2682 if (block.toolUse().isPresent()) {2683 ToolUseBlock toolUse = block.toolUse().get();2684 toolResults.add(ContentBlockParam.ofToolResult(2685 ToolResultBlockParam.builder()2686 .toolUseId(toolUse.id())2687 .content(runTool(toolUse))2688 .build()));2689 }2690 }26912692 messages.add(response.toParam());2693 messages.add(MessageParam.builder()2694 .role(MessageParam.Role.USER)2695 .contentOfBlockParams(toolResults)2696 .build());26972698 response = client.messages().create(MessageCreateParams.builder()2699 .model(Model.CLAUDE_OPUS_5)2700 .maxTokens(1024L)2701 .addTool(calendarTool)2702 .addTool(listTool)2703 .messages(messages)2704 .build());2705 }27062707 response.content().stream()2708 .flatMap(block -> block.text().stream())2709 .forEach(textBlock -> IO.println(textBlock.text()));2710 }2711 ```27122713 ```php PHP2714 <?php27152716 // Ring 3: Multiple tools, parallel calls.27172718 use Anthropic\Client;27192720 $client = new Client();27212722 $tools = [2723 [2724 'name' => 'create_calendar_event',2725 'description' => 'Create a calendar event with attendees and optional recurrence.',2726 'input_schema' => [2727 'type' => 'object',2728 'properties' => [2729 'title' => ['type' => 'string'],2730 'start' => ['type' => 'string', 'format' => 'date-time'],2731 'end' => ['type' => 'string', 'format' => 'date-time'],2732 'attendees' => [2733 'type' => 'array',2734 'items' => ['type' => 'string', 'format' => 'email'],2735 ],2736 'recurrence' => [2737 'type' => 'object',2738 'properties' => [2739 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']],2740 'count' => ['type' => 'integer', 'minimum' => 1],2741 ],2742 ],2743 ],2744 'required' => ['title', 'start', 'end'],2745 ],2746 ],2747 [2748 'name' => 'list_calendar_events',2749 'description' => 'List all calendar events on a given date.',2750 'input_schema' => [2751 'type' => 'object',2752 'properties' => [2753 'date' => ['type' => 'string', 'format' => 'date'],2754 ],2755 'required' => ['date'],2756 ],2757 ],2758 ];27592760 function runTool(string $name, array $input): string2761 {2762 if ($name === 'create_calendar_event') {2763 return json_encode([2764 'event_id' => 'evt_123',2765 'status' => 'created',2766 'title' => $input['title'],2767 ]);2768 }2769 if ($name === 'list_calendar_events') {2770 return json_encode([2771 'events' => [['title' => 'Existing meeting', 'start' => '14:00', 'end' => '15:00']],2772 ]);2773 }27742775 return json_encode(['error' => "Unknown tool: {$name}"]);2776 }27772778 $messages = [2779 [2780 'role' => 'user',2781 'content' => 'Check what I have next Monday, then schedule a planning session that avoids any conflicts.',2782 ],2783 ];27842785 $response = $client->messages->create(2786 model: 'claude-opus-5',2787 maxTokens: 1024,2788 tools: $tools,2789 messages: $messages,2790 );27912792 while ($response->stopReason === 'tool_use') {2793 // A single response can contain multiple tool_use blocks. Process all of2794 // them and return all results together in one user message.2795 $toolResults = [];2796 foreach ($response->content as $block) {2797 if ($block->type === 'tool_use') {2798 $toolResults[] = [2799 'type' => 'tool_result',2800 'tool_use_id' => $block->id,2801 'content' => runTool($block->name, $block->input),2802 ];2803 }2804 }28052806 $messages[] = ['role' => 'assistant', 'content' => $response->content];2807 $messages[] = ['role' => 'user', 'content' => $toolResults];28082809 $response = $client->messages->create(2810 model: 'claude-opus-5',2811 maxTokens: 1024,2812 tools: $tools,2813 messages: $messages,2814 );2815 }28162817 foreach ($response->content as $block) {2818 if ($block->type === 'text') {2819 echo $block->text, "\n";2820 }2821 }2822 ```28232824 ```ruby Ruby2825 # Ring 3: Multiple tools, parallel calls.28262827 require "anthropic"28282829 client = Anthropic::Client.new28302831 tools = [2832 {2833 name: "create_calendar_event",2834 description: "Create a calendar event with attendees and optional recurrence.",2835 input_schema: {2836 type: "object",2837 properties: {2838 title: {type: "string"},2839 start: {type: "string", format: "date-time"},2840 end: {type: "string", format: "date-time"},2841 attendees: {2842 type: "array",2843 items: {type: "string", format: "email"}2844 },2845 recurrence: {2846 type: "object",2847 properties: {2848 frequency: {enum: ["daily", "weekly", "monthly"]},2849 count: {type: "integer", minimum: 1}2850 }2851 }2852 },2853 required: ["title", "start", "end"]2854 }2855 },2856 {2857 name: "list_calendar_events",2858 description: "List all calendar events on a given date.",2859 input_schema: {2860 type: "object",2861 properties: {2862 date: {type: "string", format: "date"}2863 },2864 required: ["date"]2865 }2866 }2867 ]28682869 def run_tool(name, input)2870 case name2871 when "create_calendar_event"2872 JSON.generate({event_id: "evt_123", status: "created", title: input[:title]})2873 when "list_calendar_events"2874 JSON.generate({events: [{title: "Existing meeting", start: "14:00", end: "15:00"}]})2875 else2876 JSON.generate({error: "Unknown tool: #{name}"})2877 end2878 end28792880 messages = [2881 {2882 role: "user",2883 content: "Check what I have next Monday, then schedule a planning session that avoids any conflicts."2884 }2885 ]28862887 response = client.messages.create(2888 model: "claude-opus-5",2889 max_tokens: 1024,2890 tools: tools,2891 messages: messages2892 )28932894 while response.stop_reason == :tool_use2895 # A single response can contain multiple tool_use blocks. Process all of2896 # them and return all results together in one user message.2897 tool_results = response.content.select { |block| block.type == :tool_use }.map do |tool_use|2898 {2899 type: "tool_result",2900 tool_use_id: tool_use.id,2901 content: run_tool(tool_use.name, tool_use.input)2902 }2903 end29042905 messages << {role: "assistant", content: response.content}2906 messages << {role: "user", content: tool_results}29072908 response = client.messages.create(2909 model: "claude-opus-5",2910 max_tokens: 1024,2911 tools: tools,2912 messages: messages2913 )2914 end29152916 response.content.each do |block|2917 puts block.text if block.type == :text2918 end2919 ```2920</CodeGroup>29212922**What to expect**29232924```text Output wrap2925I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.2926```29272928For more on concurrent execution and ordering guarantees, see [Parallel tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use).29292930## Ring 4: Error handling29312932Tools fail. A calendar API might reject an event with too many attendees, or a date might be malformed. When a tool raises an error, send the error message back with `is_error: true` instead of crashing. Claude reads the error and can retry with corrected input, ask the user for clarification, or explain the limitation.29332934<CodeGroup>2935 ```bash cURL2936 #!/bin/bash2937 # Ring 4: Error handling.29382939 TOOLS='[2940 {2941 "name": "create_calendar_event",2942 "description": "Create a calendar event with attendees and optional recurrence.",2943 "input_schema": {2944 "type": "object",2945 "properties": {2946 "title": {"type": "string"},2947 "start": {"type": "string", "format": "date-time"},2948 "end": {"type": "string", "format": "date-time"},2949 "attendees": {"type": "array", "items": {"type": "string", "format": "email"}},2950 "recurrence": {2951 "type": "object",2952 "properties": {2953 "frequency": {"enum": ["daily", "weekly", "monthly"]},2954 "count": {"type": "integer", "minimum": 1}2955 }2956 }2957 },2958 "required": ["title", "start", "end"]2959 }2960 },2961 {2962 "name": "list_calendar_events",2963 "description": "List all calendar events on a given date.",2964 "input_schema": {2965 "type": "object",2966 "properties": {"date": {"type": "string", "format": "date"}},2967 "required": ["date"]2968 }2969 }2970 ]'29712972 run_tool() {2973 case "$1" in2974 create_calendar_event)2975 local count=$(echo "$2" | jq '.attendees | length // 0')2976 if [ "$count" -gt 10 ]; then2977 echo "ERROR: Too many attendees (max 10)"2978 return 12979 fi2980 jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;;2981 list_calendar_events)2982 echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;2983 *)2984 echo "ERROR: Unknown tool: $1"2985 return 1 ;;2986 esac2987 }29882989 EMAILS=$(seq 0 14 | sed 's/.*/user&@example.com/' | paste -sd, -)2990 MESSAGES="[{\"role\": \"user\", \"content\": \"Schedule an all-hands with everyone: $EMAILS\"}]"29912992 call_api() {2993 curl -s https://api.anthropic.com/v1/messages \2994 -H "x-api-key: $ANTHROPIC_API_KEY" \2995 -H "anthropic-version: 2023-06-01" \2996 -H "content-type: application/json" \2997 -d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \2998 '{model: "claude-opus-5", max_tokens: 1024, tools: $tools, messages: $messages}')"2999 }30003001 RESPONSE=$(call_api)30023003 while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do3004 TOOL_RESULTS='[]'3005 while read -r block; do3006 NAME=$(echo "$block" | jq -r '.name')3007 INPUT=$(echo "$block" | jq -c '.input')3008 ID=$(echo "$block" | jq -r '.id')3009 if OUTPUT=$(run_tool "$NAME" "$INPUT"); then3010 TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \3011 '. + [{type: "tool_result", tool_use_id: $id, content: $result}]')3012 else3013 # Signal failure so Claude can retry or ask for clarification.3014 TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \3015 '. + [{type: "tool_result", tool_use_id: $id, content: $result, is_error: true}]')3016 fi3017 done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")')30183019 MESSAGES=$(echo "$MESSAGES" | jq \3020 --argjson assistant "$(echo "$RESPONSE" | jq '.content')" \3021 --argjson results "$TOOL_RESULTS" \3022 '. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]')30233024 RESPONSE=$(call_api)3025 done30263027 echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'3028 ```30293030 ```bash CLI3031 #!/usr/bin/env bash3032 # Ring 4: Error handling.3033 # Uses jq for cross-turn message-array state — building an agentic loop in shell3034 # requires JSON manipulation beyond ant's single-call --transform scope.3035 set -euo pipefail30363037 run_tool() {3038 case "$1" in3039 create_calendar_event)3040 local count3041 count=$(jq '.attendees | length // 0' <<<"$2")3042 if [ "$count" -gt 10 ]; then3043 echo "ERROR: Too many attendees (max 10)"3044 return 13045 fi3046 jq -n --arg title "$(jq -r '.title' <<<"$2")" \3047 '{event_id: "evt_123", status: "created", title: $title}' ;;3048 list_calendar_events)3049 echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;3050 *)3051 echo "ERROR: Unknown tool: $1"3052 return 1 ;;3053 esac3054 }30553056 EMAILS=$(seq 0 14 | sed 's/.*/user&@example.com/' | paste -sd, -)3057 MESSAGES=$(jq -n --arg msg "Schedule an all-hands with everyone: $EMAILS" \3058 '[{role: "user", content: $msg}]')30593060 call_api() {3061 # ant reads the request body as YAML on stdin: no auth headers, no3062 # hand-built JSON envelope. The static keys (model, tools) live in a3063 # quoted heredoc; the growing messages array is appended as JSON,3064 # which YAML accepts as flow syntax.3065 {3066 cat <<'YAML'3067 model: claude-opus-53068 max_tokens: 10243069 tools:3070 - name: create_calendar_event3071 description: Create a calendar event with attendees and optional recurrence.3072 input_schema:3073 type: object3074 properties:3075 title: {type: string}3076 start: {type: string, format: date-time}3077 end: {type: string, format: date-time}3078 attendees:3079 type: array3080 items: {type: string, format: email}3081 recurrence:3082 type: object3083 properties:3084 frequency: {enum: [daily, weekly, monthly]}3085 count: {type: integer, minimum: 1}3086 required: [title, start, end]3087 - name: list_calendar_events3088 description: List all calendar events on a given date.3089 input_schema:3090 type: object3091 properties:3092 date: {type: string, format: date}3093 required: [date]3094 YAML3095 printf 'messages: %s\n' "$MESSAGES"3096 } | ant messages create --format json3097 }30983099 RESPONSE=$(call_api)31003101 while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do3102 TOOL_RESULTS='[]'3103 while read -r block; do3104 NAME=$(jq -r '.name' <<<"$block")3105 INPUT=$(jq -c '.input' <<<"$block")3106 ID=$(jq -r '.id' <<<"$block")3107 if OUTPUT=$(run_tool "$NAME" "$INPUT"); then3108 TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$OUTPUT" \3109 '. + [{type: "tool_result", tool_use_id: $id, content: $result}]' \3110 <<<"$TOOL_RESULTS")3111 else3112 # Signal failure so Claude can retry or ask for clarification.3113 TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$OUTPUT" \3114 '. + [{type: "tool_result", tool_use_id: $id, content: $result, is_error: true}]' \3115 <<<"$TOOL_RESULTS")3116 fi3117 done < <(jq -c '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")31183119 MESSAGES=$(jq \3120 --argjson assistant "$(jq '.content' <<<"$RESPONSE")" \3121 --argjson results "$TOOL_RESULTS" \3122 '. + [3123 {role: "assistant", content: $assistant},3124 {role: "user", content: $results}3125 ]' <<<"$MESSAGES")31263127 RESPONSE=$(call_api)3128 done31293130 jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE"3131 ```31323133 ```python Python3134 # Ring 4: Error handling.31353136 import json31373138 import anthropic31393140 client = anthropic.Anthropic()31413142 tools = [3143 {3144 "name": "create_calendar_event",3145 "description": "Create a calendar event with attendees and optional recurrence.",3146 "input_schema": {3147 "type": "object",3148 "properties": {3149 "title": {"type": "string"},3150 "start": {"type": "string", "format": "date-time"},3151 "end": {"type": "string", "format": "date-time"},3152 "attendees": {3153 "type": "array",3154 "items": {"type": "string", "format": "email"},3155 },3156 "recurrence": {3157 "type": "object",3158 "properties": {3159 "frequency": {"enum": ["daily", "weekly", "monthly"]},3160 "count": {"type": "integer", "minimum": 1},3161 },3162 },3163 },3164 "required": ["title", "start", "end"],3165 },3166 },3167 {3168 "name": "list_calendar_events",3169 "description": "List all calendar events on a given date.",3170 "input_schema": {3171 "type": "object",3172 "properties": {3173 "date": {"type": "string", "format": "date"},3174 },3175 "required": ["date"],3176 },3177 },3178 ]317931803181 def run_tool(name, tool_input):3182 if name == "create_calendar_event":3183 if "attendees" in tool_input and len(tool_input["attendees"]) > 10:3184 raise ValueError("Too many attendees (max 10)")3185 return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}3186 if name == "list_calendar_events":3187 return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}3188 raise ValueError(f"Unknown tool: {name}")318931903191 messages = [3192 {3193 "role": "user",3194 "content": "Schedule an all-hands with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),3195 }3196 ]31973198 response = client.messages.create(3199 model="claude-opus-5",3200 max_tokens=1024,3201 tools=tools,3202 messages=messages,3203 )32043205 while response.stop_reason == "tool_use":3206 tool_results = []3207 for block in response.content:3208 if block.type == "tool_use":3209 try:3210 result = run_tool(block.name, block.input)3211 tool_results.append(3212 {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}3213 )3214 except Exception as exc:3215 # Signal failure so Claude can retry or ask for clarification.3216 tool_results.append(3217 {3218 "type": "tool_result",3219 "tool_use_id": block.id,3220 "content": str(exc),3221 "is_error": True,3222 }3223 )32243225 messages.append({"role": "assistant", "content": response.content})3226 messages.append({"role": "user", "content": tool_results})32273228 response = client.messages.create(3229 model="claude-opus-5",3230 max_tokens=1024,3231 tools=tools,3232 messages=messages,3233 )32343235 final_text = next(block for block in response.content if block.type == "text")3236 print(final_text.text)3237 ```32383239 ```typescript TypeScript3240 // Ring 4: Error handling.32413242 import Anthropic from "@anthropic-ai/sdk";32433244 const client = new Anthropic();32453246 const tools: Anthropic.Tool[] = [3247 {3248 name: "create_calendar_event",3249 description:3250 "Create a calendar event with attendees and optional recurrence.",3251 input_schema: {3252 type: "object",3253 properties: {3254 title: { type: "string" },3255 start: { type: "string", format: "date-time" },3256 end: { type: "string", format: "date-time" },3257 attendees: {3258 type: "array",3259 items: { type: "string", format: "email" },3260 },3261 recurrence: {3262 type: "object",3263 properties: {3264 frequency: { enum: ["daily", "weekly", "monthly"] },3265 count: { type: "integer", minimum: 1 },3266 },3267 },3268 },3269 required: ["title", "start", "end"],3270 },3271 },3272 {3273 name: "list_calendar_events",3274 description: "List all calendar events on a given date.",3275 input_schema: {3276 type: "object",3277 properties: {3278 date: { type: "string", format: "date" },3279 },3280 required: ["date"],3281 },3282 },3283 ];32843285 function runTool(name: string, input: Record<string, unknown>) {3286 if (name === "create_calendar_event") {3287 const attendees = input.attendees as string[] | undefined;3288 if (attendees && attendees.length > 10) {3289 throw new Error("Too many attendees (max 10)");3290 }3291 return { event_id: "evt_123", status: "created", title: input.title };3292 }3293 if (name === "list_calendar_events") {3294 return {3295 events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }],3296 };3297 }3298 throw new Error(`Unknown tool: ${name}`);3299 }33003301 const emails = Array.from({ length: 15 }, (_, i) => `user${i}@example.com`);3302 const messages: Anthropic.MessageParam[] = [3303 {3304 role: "user",3305 content: `Schedule an all-hands with everyone: ${emails.join(", ")}`,3306 },3307 ];33083309 let response = await client.messages.create({3310 model: "claude-opus-5",3311 max_tokens: 1024,3312 tools,3313 messages,3314 });33153316 while (response.stop_reason === "tool_use") {3317 const toolResults: Anthropic.ToolResultBlockParam[] = [];3318 for (const block of response.content) {3319 if (block.type === "tool_use") {3320 try {3321 const result = runTool(block.name, block.input as Record<string, unknown>);3322 toolResults.push({3323 type: "tool_result",3324 tool_use_id: block.id,3325 content: JSON.stringify(result),3326 });3327 } catch (err) {3328 // Signal failure so Claude can retry or ask for clarification.3329 toolResults.push({3330 type: "tool_result",3331 tool_use_id: block.id,3332 content: String(err),3333 is_error: true,3334 });3335 }3336 }3337 }33383339 messages.push({ role: "assistant", content: response.content });3340 messages.push({ role: "user", content: toolResults });33413342 response = await client.messages.create({3343 model: "claude-opus-5",3344 max_tokens: 1024,3345 tools,3346 messages,3347 });3348 }33493350 for (const block of response.content) {3351 if (block.type === "text") {3352 console.log(block.text);3353 }3354 }3355 ```33563357 ```csharp C#3358 // Ring 4: Error handling.33593360 using System;3361 using System.Collections.Generic;3362 using System.Linq;3363 using System.Text.Json;3364 using System.Threading.Tasks;3365 using Anthropic;3366 using Anthropic.Models.Messages;33673368 AnthropicClient client = new();33693370 List<ToolUnion> tools =3371 [3372 new ToolUnion(new Tool()3373 {3374 Name = "create_calendar_event",3375 Description = "Create a calendar event with attendees and optional recurrence.",3376 InputSchema = new InputSchema()3377 {3378 Properties = new Dictionary<string, JsonElement>3379 {3380 ["title"] = JsonSerializer.SerializeToElement(new { type = "string" }),3381 ["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),3382 ["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),3383 ["attendees"] = JsonSerializer.SerializeToElement(new3384 {3385 type = "array",3386 items = new { type = "string", format = "email" },3387 }),3388 ["recurrence"] = JsonSerializer.SerializeToElement(new3389 {3390 type = "object",3391 properties = new3392 {3393 frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },3394 count = new { type = "integer", minimum = 1 },3395 },3396 }),3397 },3398 Required = ["title", "start", "end"],3399 },3400 }),3401 new ToolUnion(new Tool()3402 {3403 Name = "list_calendar_events",3404 Description = "List all calendar events on a given date.",3405 InputSchema = new InputSchema()3406 {3407 Properties = new Dictionary<string, JsonElement>3408 {3409 ["date"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date" }),3410 },3411 Required = ["date"],3412 },3413 }),3414 ];34153416 string RunTool(ToolUseBlock toolUse)3417 {3418 if (toolUse.Name == "create_calendar_event")3419 {3420 if (toolUse.Input.TryGetValue("attendees", out var attendees) && attendees.GetArrayLength() > 10)3421 {3422 throw new InvalidOperationException("Too many attendees (max 10)");3423 }3424 var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : "";3425 return JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title });3426 }3427 if (toolUse.Name == "list_calendar_events")3428 {3429 return """{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}""";3430 }3431 throw new InvalidOperationException($"Unknown tool: {toolUse.Name}");3432 }34333434 // Build a request that exceeds the tool's attendee limit so the error path runs.3435 var emails = string.Join(", ", Enumerable.Range(0, 15).Select(i => $"user{i}@example.com"));34363437 List<MessageParam> messages =3438 [3439 new() { Role = Role.User, Content = $"Schedule an all-hands with everyone: {emails}" },3440 ];34413442 var response = await client.Messages.Create(new MessageCreateParams3443 {3444 Model = Model.ClaudeOpus5,3445 MaxTokens = 1024,3446 Tools = tools,3447 Messages = messages,3448 });34493450 while (response.StopReason == StopReason.ToolUse)3451 {3452 List<ContentBlockParam> toolResults = [];3453 foreach (var block in response.Content)3454 {3455 if (block.TryPickToolUse(out var toolUse))3456 {3457 ToolResultBlockParam toolResult;3458 try3459 {3460 toolResult = new ToolResultBlockParam() { ToolUseID = toolUse.ID, Content = RunTool(toolUse) };3461 }3462 catch (Exception e)3463 {3464 // Signal failure so Claude can retry or ask for clarification.3465 toolResult = new ToolResultBlockParam()3466 {3467 ToolUseID = toolUse.ID,3468 Content = e.Message,3469 IsError = true,3470 };3471 }3472 toolResults.Add(new ContentBlockParam(toolResult));3473 }3474 }34753476 messages.Add(new()3477 {3478 Role = Role.Assistant,3479 Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(),3480 });3481 messages.Add(new() { Role = Role.User, Content = new MessageParamContent(toolResults) });34823483 response = await client.Messages.Create(new MessageCreateParams3484 {3485 Model = Model.ClaudeOpus5,3486 MaxTokens = 1024,3487 Tools = tools,3488 Messages = messages,3489 });3490 }34913492 foreach (var block in response.Content)3493 {3494 if (block.TryPickText(out var text))3495 {3496 Console.WriteLine(text.Text);3497 }3498 }3499 ```35003501 ```go Go3502 // Ring 4: Error handling.35033504 package main35053506 import (3507 "context"3508 "encoding/json"3509 "fmt"3510 "log"3511 "strings"35123513 "github.com/anthropics/anthropic-sdk-go"3514 )35153516 func runTool(name string, input map[string]any) (string, error) {3517 if name == "create_calendar_event" {3518 if attendees, ok := input["attendees"].([]any); ok && len(attendees) > 10 {3519 return "", fmt.Errorf("too many attendees (max 10)")3520 }3521 title, _ := input["title"].(string)3522 return fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, title), nil3523 }3524 if name == "list_calendar_events" {3525 return `{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}`, nil3526 }3527 return "", fmt.Errorf("unknown tool: %s", name)3528 }35293530 func main() {3531 client := anthropic.NewClient()3532 ctx := context.Background()35333534 tools := []anthropic.ToolUnionParam{3535 {OfTool: &anthropic.ToolParam{3536 Name: "create_calendar_event",3537 Description: anthropic.String("Create a calendar event with attendees and optional recurrence."),3538 InputSchema: anthropic.ToolInputSchemaParam{3539 Properties: map[string]any{3540 "title": map[string]any{"type": "string"},3541 "start": map[string]any{"type": "string", "format": "date-time"},3542 "end": map[string]any{"type": "string", "format": "date-time"},3543 "attendees": map[string]any{3544 "type": "array",3545 "items": map[string]any{"type": "string", "format": "email"},3546 },3547 "recurrence": map[string]any{3548 "type": "object",3549 "properties": map[string]any{3550 "frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}},3551 "count": map[string]any{"type": "integer", "minimum": 1},3552 },3553 },3554 },3555 Required: []string{"title", "start", "end"},3556 },3557 }},3558 {OfTool: &anthropic.ToolParam{3559 Name: "list_calendar_events",3560 Description: anthropic.String("List all calendar events on a given date."),3561 InputSchema: anthropic.ToolInputSchemaParam{3562 Properties: map[string]any{3563 "date": map[string]any{"type": "string", "format": "date"},3564 },3565 Required: []string{"date"},3566 },3567 }},3568 }35693570 // Build a request that exceeds the tool's attendee limit so the error path runs.3571 emails := make([]string, 15)3572 for i := range emails {3573 emails[i] = fmt.Sprintf("user%d@example.com", i)3574 }3575 messages := []anthropic.MessageParam{3576 anthropic.NewUserMessage(anthropic.NewTextBlock(3577 "Schedule an all-hands with everyone: " + strings.Join(emails, ", "),3578 )),3579 }35803581 response, err := client.Messages.New(ctx, anthropic.MessageNewParams{3582 Model: anthropic.ModelClaudeOpus5,3583 MaxTokens: 1024,3584 Tools: tools,3585 Messages: messages,3586 })3587 if err != nil {3588 log.Fatal(err)3589 }35903591 for response.StopReason == "tool_use" {3592 var toolResults []anthropic.ContentBlockParamUnion3593 for _, block := range response.Content {3594 if block.Type == "tool_use" {3595 var input map[string]any3596 if err := json.Unmarshal(block.Input, &input); err != nil {3597 log.Fatal(err)3598 }3599 result, toolErr := runTool(block.Name, input)3600 if toolErr != nil {3601 // Signal failure so Claude can retry or ask for clarification.3602 toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, toolErr.Error(), true))3603 } else {3604 toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false))3605 }3606 }3607 }36083609 var assistantContent []anthropic.ContentBlockParamUnion3610 for _, block := range response.Content {3611 assistantContent = append(assistantContent, block.ToParam())3612 }3613 messages = append(messages, anthropic.NewAssistantMessage(assistantContent...))3614 messages = append(messages, anthropic.NewUserMessage(toolResults...))36153616 response, err = client.Messages.New(ctx, anthropic.MessageNewParams{3617 Model: anthropic.ModelClaudeOpus5,3618 MaxTokens: 1024,3619 Tools: tools,3620 Messages: messages,3621 })3622 if err != nil {3623 log.Fatal(err)3624 }3625 }36263627 for _, block := range response.Content {3628 if block.Type == "text" {3629 fmt.Println(block.Text)3630 }3631 }3632 }3633 ```36343635 ```java Java3636 // Ring 4: Error handling.36373638 import com.anthropic.client.AnthropicClient;3639 import com.anthropic.client.okhttp.AnthropicOkHttpClient;3640 import com.anthropic.core.JsonValue;3641 import com.anthropic.models.messages.ContentBlock;3642 import com.anthropic.models.messages.ContentBlockParam;3643 import com.anthropic.models.messages.Message;3644 import com.anthropic.models.messages.MessageCreateParams;3645 import com.anthropic.models.messages.MessageParam;3646 import com.anthropic.models.messages.Model;3647 import com.anthropic.models.messages.StopReason;3648 import com.anthropic.models.messages.Tool;3649 import com.anthropic.models.messages.Tool.InputSchema;3650 import com.anthropic.models.messages.ToolResultBlockParam;3651 import com.anthropic.models.messages.ToolUseBlock;3652 import java.util.ArrayList;3653 import java.util.List;3654 import java.util.Map;3655 import java.util.stream.Collectors;3656 import java.util.stream.IntStream;36573658 String runTool(ToolUseBlock toolUse) {3659 // The raw tool input is a JSON object; read fields out of it as a map.3660 Map<String, JsonValue> input = (Map<String, JsonValue>) toolUse._input().asObject().get();3661 if (toolUse.name().equals("create_calendar_event")) {3662 int attendeeCount = input.containsKey("attendees")3663 ? ((List<?>) input.get("attendees").asArray().get()).size()3664 : 0;3665 if (attendeeCount > 10) {3666 throw new IllegalArgumentException("Too many attendees (max 10)");3667 }3668 String title = input.containsKey("title") ? input.get("title").asStringOrThrow() : "";3669 return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}";3670 }3671 if (toolUse.name().equals("list_calendar_events")) {3672 return "{\"events\": [{\"title\": \"Existing meeting\", \"start\": \"14:00\", \"end\": \"15:00\"}]}";3673 }3674 throw new IllegalArgumentException("Unknown tool: " + toolUse.name());3675 }36763677 void main() {3678 AnthropicClient client = AnthropicOkHttpClient.fromEnv();36793680 Tool calendarTool = Tool.builder()3681 .name("create_calendar_event")3682 .description("Create a calendar event with attendees and optional recurrence.")3683 .inputSchema(InputSchema.builder()3684 .properties(JsonValue.from(Map.of(3685 "title", Map.of("type", "string"),3686 "start", Map.of("type", "string", "format", "date-time"),3687 "end", Map.of("type", "string", "format", "date-time"),3688 "attendees", Map.of(3689 "type", "array",3690 "items", Map.of("type", "string", "format", "email")3691 ),3692 "recurrence", Map.of(3693 "type", "object",3694 "properties", Map.of(3695 "frequency", Map.of("enum", List.of("daily", "weekly", "monthly")),3696 "count", Map.of("type", "integer", "minimum", 1)3697 )3698 )3699 )))3700 .required(List.of("title", "start", "end"))3701 .build())3702 .build();37033704 Tool listTool = Tool.builder()3705 .name("list_calendar_events")3706 .description("List all calendar events on a given date.")3707 .inputSchema(InputSchema.builder()3708 .properties(JsonValue.from(Map.of(3709 "date", Map.of("type", "string", "format", "date")3710 )))3711 .required(List.of("date"))3712 .build())3713 .build();37143715 // Build a request that exceeds the tool's attendee limit so the error path runs.3716 String emails = IntStream.range(0, 15)3717 .mapToObj(i -> "user" + i + "@example.com")3718 .collect(Collectors.joining(", "));37193720 List<MessageParam> messages = new ArrayList<>();3721 messages.add(MessageParam.builder()3722 .role(MessageParam.Role.USER)3723 .content("Schedule an all-hands with everyone: " + emails)3724 .build());37253726 Message response = client.messages().create(MessageCreateParams.builder()3727 .model(Model.CLAUDE_OPUS_5)3728 .maxTokens(1024L)3729 .addTool(calendarTool)3730 .addTool(listTool)3731 .messages(messages)3732 .build());37333734 while (response.stopReason().isPresent()3735 && response.stopReason().get().equals(StopReason.TOOL_USE)) {3736 List<ContentBlockParam> toolResults = new ArrayList<>();3737 for (ContentBlock block : response.content()) {3738 if (block.toolUse().isPresent()) {3739 ToolUseBlock toolUse = block.toolUse().get();3740 ToolResultBlockParam.Builder resultBuilder = ToolResultBlockParam.builder()3741 .toolUseId(toolUse.id());3742 try {3743 resultBuilder.content(runTool(toolUse));3744 } catch (Exception e) {3745 // Signal failure so Claude can retry or ask for clarification.3746 resultBuilder.content(e.getMessage()).isError(true);3747 }3748 toolResults.add(ContentBlockParam.ofToolResult(resultBuilder.build()));3749 }3750 }37513752 messages.add(response.toParam());3753 messages.add(MessageParam.builder()3754 .role(MessageParam.Role.USER)3755 .contentOfBlockParams(toolResults)3756 .build());37573758 response = client.messages().create(MessageCreateParams.builder()3759 .model(Model.CLAUDE_OPUS_5)3760 .maxTokens(1024L)3761 .addTool(calendarTool)3762 .addTool(listTool)3763 .messages(messages)3764 .build());3765 }37663767 response.content().stream()3768 .flatMap(block -> block.text().stream())3769 .forEach(textBlock -> IO.println(textBlock.text()));3770 }3771 ```37723773 ```php PHP3774 <?php37753776 // Ring 4: Error handling.37773778 use Anthropic\Client;37793780 $client = new Client();37813782 $tools = [3783 [3784 'name' => 'create_calendar_event',3785 'description' => 'Create a calendar event with attendees and optional recurrence.',3786 'input_schema' => [3787 'type' => 'object',3788 'properties' => [3789 'title' => ['type' => 'string'],3790 'start' => ['type' => 'string', 'format' => 'date-time'],3791 'end' => ['type' => 'string', 'format' => 'date-time'],3792 'attendees' => [3793 'type' => 'array',3794 'items' => ['type' => 'string', 'format' => 'email'],3795 ],3796 'recurrence' => [3797 'type' => 'object',3798 'properties' => [3799 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']],3800 'count' => ['type' => 'integer', 'minimum' => 1],3801 ],3802 ],3803 ],3804 'required' => ['title', 'start', 'end'],3805 ],3806 ],3807 [3808 'name' => 'list_calendar_events',3809 'description' => 'List all calendar events on a given date.',3810 'input_schema' => [3811 'type' => 'object',3812 'properties' => [3813 'date' => ['type' => 'string', 'format' => 'date'],3814 ],3815 'required' => ['date'],3816 ],3817 ],3818 ];38193820 function runTool(string $name, array $input): string3821 {3822 if ($name === 'create_calendar_event') {3823 if (count($input['attendees'] ?? []) > 10) {3824 throw new InvalidArgumentException('Too many attendees (max 10)');3825 }38263827 return json_encode([3828 'event_id' => 'evt_123',3829 'status' => 'created',3830 'title' => $input['title'],3831 ]);3832 }3833 if ($name === 'list_calendar_events') {3834 return json_encode([3835 'events' => [['title' => 'Existing meeting', 'start' => '14:00', 'end' => '15:00']],3836 ]);3837 }38383839 throw new InvalidArgumentException("Unknown tool: {$name}");3840 }38413842 // Build a request that exceeds the tool's attendee limit so the error path runs.3843 $emails = array_map(fn (int $i): string => "user{$i}@example.com", range(0, 14));3844 $messages = [3845 [3846 'role' => 'user',3847 'content' => 'Schedule an all-hands with everyone: ' . implode(', ', $emails),3848 ],3849 ];38503851 $response = $client->messages->create(3852 model: 'claude-opus-5',3853 maxTokens: 1024,3854 tools: $tools,3855 messages: $messages,3856 );38573858 while ($response->stopReason === 'tool_use') {3859 $toolResults = [];3860 foreach ($response->content as $block) {3861 if ($block->type === 'tool_use') {3862 try {3863 $toolResults[] = [3864 'type' => 'tool_result',3865 'tool_use_id' => $block->id,3866 'content' => runTool($block->name, $block->input),3867 ];3868 } catch (Exception $e) {3869 // Signal failure so Claude can retry or ask for clarification.3870 $toolResults[] = [3871 'type' => 'tool_result',3872 'tool_use_id' => $block->id,3873 'content' => $e->getMessage(),3874 'is_error' => true,3875 ];3876 }3877 }3878 }38793880 $messages[] = ['role' => 'assistant', 'content' => $response->content];3881 $messages[] = ['role' => 'user', 'content' => $toolResults];38823883 $response = $client->messages->create(3884 model: 'claude-opus-5',3885 maxTokens: 1024,3886 tools: $tools,3887 messages: $messages,3888 );3889 }38903891 foreach ($response->content as $block) {3892 if ($block->type === 'text') {3893 echo $block->text, "\n";3894 }3895 }3896 ```38973898 ```ruby Ruby3899 # Ring 4: Error handling.39003901 require "anthropic"39023903 client = Anthropic::Client.new39043905 tools = [3906 {3907 name: "create_calendar_event",3908 description: "Create a calendar event with attendees and optional recurrence.",3909 input_schema: {3910 type: "object",3911 properties: {3912 title: {type: "string"},3913 start: {type: "string", format: "date-time"},3914 end: {type: "string", format: "date-time"},3915 attendees: {3916 type: "array",3917 items: {type: "string", format: "email"}3918 },3919 recurrence: {3920 type: "object",3921 properties: {3922 frequency: {enum: ["daily", "weekly", "monthly"]},3923 count: {type: "integer", minimum: 1}3924 }3925 }3926 },3927 required: ["title", "start", "end"]3928 }3929 },3930 {3931 name: "list_calendar_events",3932 description: "List all calendar events on a given date.",3933 input_schema: {3934 type: "object",3935 properties: {3936 date: {type: "string", format: "date"}3937 },3938 required: ["date"]3939 }3940 }3941 ]39423943 def run_tool(name, input)3944 case name3945 when "create_calendar_event"3946 attendees = input[:attendees]3947 raise ArgumentError, "Too many attendees (max 10)" if attendees && attendees.length > 103948 JSON.generate({event_id: "evt_123", status: "created", title: input[:title]})3949 when "list_calendar_events"3950 JSON.generate({events: [{title: "Existing meeting", start: "14:00", end: "15:00"}]})3951 else3952 raise ArgumentError, "Unknown tool: #{name}"3953 end3954 end39553956 # Build a request that exceeds the tool's attendee limit so the error path runs.3957 emails = (0...15).map { |i| "user#{i}@example.com" }3958 messages = [3959 {3960 role: "user",3961 content: "Schedule an all-hands with everyone: #{emails.join(", ")}"3962 }3963 ]39643965 response = client.messages.create(3966 model: "claude-opus-5",3967 max_tokens: 1024,3968 tools: tools,3969 messages: messages3970 )39713972 while response.stop_reason == :tool_use3973 tool_results = response.content.select { |block| block.type == :tool_use }.map do |tool_use|3974 begin3975 {3976 type: "tool_result",3977 tool_use_id: tool_use.id,3978 content: run_tool(tool_use.name, tool_use.input)3979 }3980 rescue => e3981 # Signal failure so Claude can retry or ask for clarification.3982 {3983 type: "tool_result",3984 tool_use_id: tool_use.id,3985 content: e.message,3986 is_error: true3987 }3988 end3989 end39903991 messages << {role: "assistant", content: response.content}3992 messages << {role: "user", content: tool_results}39933994 response = client.messages.create(3995 model: "claude-opus-5",3996 max_tokens: 1024,3997 tools: tools,3998 messages: messages3999 )4000 end40014002 response.content.each do |block|4003 puts block.text if block.type == :text4004 end4005 ```4006</CodeGroup>40074008**What to expect**40094010```text Output wrap4011I tried to schedule the all-hands but the calendar only allows 10 attendees per event. I can split this into two sessions, or you can let me know which 10 people to prioritize.4012```40134014The `is_error` flag is the only difference from a successful result. Claude sees the flag and the error text, and responds accordingly. See [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) for the full error-handling reference.40154016## Ring 5: The Tool Runner SDK abstraction40174018Rings 2 through 4 wrote the same loop by hand: call the API, check `stop_reason`, run tools, append results, repeat. The Tool Runner does this for you. Define each tool as a function, pass the list to `tool_runner`, and retrieve the final message once the loop completes. Error wrapping, result formatting, and conversation management are handled internally.40194020Each SDK provides a helper that turns an ordinary function into a runnable tool and derives the input schema from its signature; the tabs below show the idiomatic form for each language.40214022<Note>4023 Tool Runner is available in all seven SDKs: Python, TypeScript, C#, Go, Java, PHP, and Ruby. See [Tool Runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner) for the full reference. The cURL and CLI tabs show a note instead of code; keep the Ring 4 loop for curl- or CLI-based scripts.4024</Note>40254026<CodeGroup>4027 ```bash cURL4028 #!/bin/bash4029 # Ring 5: The Tool Runner SDK abstraction.40304031 # The Tool Runner SDK abstraction is available in all seven SDKs: Python,4032 # TypeScript, C#, Go, Java, PHP, and Ruby. There is no equivalent for raw4033 # curl requests. Switch to any SDK tab to see Ring 5, or keep the Ring 44034 # loop as your shell implementation.4035 ```40364037 ```bash CLI4038 #!/usr/bin/env bash4039 # Ring 5: The Tool Runner SDK abstraction.4040 set -euo pipefail40414042 # The Tool Runner SDK abstraction is available in all seven SDKs: Python,4043 # TypeScript, C#, Go, Java, PHP, and Ruby. The ant CLI exposes the Messages4044 # API directly and has no equivalent helper. Switch to any SDK tab to see4045 # Ring 5, or keep the Ring 4 loop as your CLI implementation.4046 ```40474048 ```python Python4049 # Ring 5: The Tool Runner SDK abstraction.40504051 import json40524053 import anthropic4054 from anthropic import beta_tool40554056 client = anthropic.Anthropic()405740584059 @beta_tool4060 def create_calendar_event(4061 title: str,4062 start: str,4063 end: str,4064 attendees: list[str] | None = None,4065 recurrence: dict | None = None,4066 ) -> str:4067 """Create a calendar event with attendees and optional recurrence.40684069 Args:4070 title: Event title.4071 start: Start time in ISO 8601 format.4072 end: End time in ISO 8601 format.4073 attendees: Email addresses to invite.4074 recurrence: Dict with 'frequency' (daily, weekly, monthly) and 'count'.4075 """4076 if attendees and len(attendees) > 10:4077 raise ValueError("Too many attendees (max 10)")4078 return json.dumps({"event_id": "evt_123", "status": "created", "title": title})407940804081 @beta_tool4082 def list_calendar_events(date: str) -> str:4083 """List all calendar events on a given date.40844085 Args:4086 date: Date in YYYY-MM-DD format.4087 """4088 return json.dumps({"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]})408940904091 final_message = client.beta.messages.tool_runner(4092 model="claude-opus-5",4093 max_tokens=1024,4094 tools=[create_calendar_event, list_calendar_events],4095 messages=[4096 {4097 "role": "user",4098 "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",4099 }4100 ],4101 ).until_done()41024103 for block in final_message.content:4104 if block.type == "text":4105 print(block.text)4106 ```41074108 ```typescript TypeScript4109 // Ring 5: The Tool Runner SDK abstraction.41104111 import Anthropic from "@anthropic-ai/sdk";4112 import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";4113 import { z } from "zod";41144115 const client = new Anthropic();41164117 const createCalendarEvent = betaZodTool({4118 name: "create_calendar_event",4119 description:4120 "Create a calendar event with attendees and optional recurrence.",4121 inputSchema: z.object({4122 title: z.string(),4123 start: z.string().datetime(),4124 end: z.string().datetime(),4125 attendees: z.array(z.string().email()).optional(),4126 recurrence: z4127 .object({4128 frequency: z.enum(["daily", "weekly", "monthly"]),4129 count: z.number().int().min(1),4130 })4131 .optional(),4132 }),4133 run: async (input) => {4134 if (input.attendees && input.attendees.length > 10) {4135 throw new Error("Too many attendees (max 10)");4136 }4137 return JSON.stringify({4138 event_id: "evt_123",4139 status: "created",4140 title: input.title,4141 });4142 },4143 });41444145 const listCalendarEvents = betaZodTool({4146 name: "list_calendar_events",4147 description: "List all calendar events on a given date.",4148 inputSchema: z.object({4149 date: z.string().date(),4150 }),4151 run: async () => {4152 return JSON.stringify({4153 events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }],4154 });4155 },4156 });41574158 const finalMessage = await client.beta.messages.toolRunner({4159 model: "claude-opus-5",4160 max_tokens: 1024,4161 tools: [createCalendarEvent, listCalendarEvents],4162 messages: [4163 {4164 role: "user",4165 content:4166 "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",4167 },4168 ],4169 });41704171 for (const block of finalMessage.content) {4172 if (block.type === "text") {4173 console.log(block.text);4174 }4175 }4176 ```41774178 ```csharp C#4179 // Ring 5: The Tool Runner SDK abstraction.41804181 using System;4182 using System.Collections.Generic;4183 using System.Text.Json;4184 using System.Threading.Tasks;4185 using Anthropic;4186 using Anthropic.Helpers.Beta;4187 using Anthropic.Models.Beta.Messages;4188 using MessageCreateParams = Anthropic.Models.Beta.Messages.MessageCreateParams;4189 using InputSchema = Anthropic.Models.Beta.Messages.InputSchema;4190 using Role = Anthropic.Models.Beta.Messages.Role;4191 using Model = Anthropic.Models.Messages.Model;41924193 AnthropicClient client = new();41944195 // Define each tool as a runnable tool: the definition carries the JSON Schema4196 // and the Run callback holds the implementation. Throwing an exception sends4197 // the message back to Claude as a tool result with is_error set.4198 var createCalendarEvent = new BetaRunnableTool4199 {4200 Name = "create_calendar_event",4201 Definition = new BetaTool4202 {4203 Name = "create_calendar_event",4204 Description = "Create a calendar event with attendees and optional recurrence.",4205 InputSchema = new InputSchema4206 {4207 Properties = new Dictionary<string, JsonElement>4208 {4209 ["title"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Event title" }),4210 ["start"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Start time in ISO 8601 format" }),4211 ["end"] = JsonSerializer.SerializeToElement(new { type = "string", description = "End time in ISO 8601 format" }),4212 ["attendees"] = JsonSerializer.SerializeToElement(new4213 {4214 type = "array",4215 items = new { type = "string" },4216 description = "Email addresses to invite",4217 }),4218 ["recurrence"] = JsonSerializer.SerializeToElement(new4219 {4220 type = "object",4221 properties = new4222 {4223 frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },4224 count = new { type = "integer", minimum = 1 },4225 },4226 }),4227 },4228 Required = ["title", "start", "end"],4229 },4230 },4231 Run = (toolUse, _) =>4232 {4233 if (toolUse.Input.TryGetValue("attendees", out var attendees) && attendees.GetArrayLength() > 10)4234 {4235 throw new InvalidOperationException("Too many attendees (max 10)");4236 }4237 var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : "";4238 return Task.FromResult<BetaToolResultBlockParamContent>(4239 JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title })4240 );4241 },4242 };42434244 var listCalendarEvents = new BetaRunnableTool4245 {4246 Name = "list_calendar_events",4247 Definition = new BetaTool4248 {4249 Name = "list_calendar_events",4250 Description = "List all calendar events on a given date.",4251 InputSchema = new InputSchema4252 {4253 Properties = new Dictionary<string, JsonElement>4254 {4255 ["date"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Date in YYYY-MM-DD format" }),4256 },4257 Required = ["date"],4258 },4259 },4260 Run = (toolUse, _) => Task.FromResult<BetaToolResultBlockParamContent>(4261 """{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}"""4262 ),4263 };42644265 // The runner calls the API, runs requested tools, and feeds results back4266 // until Claude produces a final answer.4267 var runner = client.Beta.Messages.ToolRunner(4268 new MessageCreateParams4269 {4270 Model = Model.ClaudeOpus5,4271 MaxTokens = 1024,4272 Messages =4273 [4274 new()4275 {4276 Role = Role.User,4277 Content = "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",4278 },4279 ],4280 },4281 [createCalendarEvent, listCalendarEvents]4282 );42834284 BetaMessage? finalMessage = null;4285 await foreach (var message in runner)4286 {4287 finalMessage = message;4288 }42894290 foreach (var block in finalMessage!.Content)4291 {4292 if (block.TryPickText(out var text))4293 {4294 Console.WriteLine(text.Text);4295 }4296 }4297 ```42984299 ```go Go4300 // Ring 5: The Tool Runner SDK abstraction.43014302 package main43034304 import (4305 "context"4306 "fmt"4307 "log"43084309 "github.com/anthropics/anthropic-sdk-go"4310 "github.com/anthropics/anthropic-sdk-go/toolrunner"4311 )43124313 // The input structs define each tool's schema. The tool runner generates the4314 // JSON Schema from the struct fields and their jsonschema tags.4315 type RecurrenceInput struct {4316 Frequency string `json:"frequency,omitempty" jsonschema:"enum=daily,enum=weekly,enum=monthly,description=How often the event repeats"`4317 Count int `json:"count,omitempty" jsonschema:"description=Number of occurrences"`4318 }43194320 type CreateCalendarEventInput struct {4321 Title string `json:"title" jsonschema:"required,description=Event title"`4322 Start string `json:"start" jsonschema:"required,description=Start time in ISO 8601 format"`4323 End string `json:"end" jsonschema:"required,description=End time in ISO 8601 format"`4324 Attendees []string `json:"attendees,omitempty" jsonschema:"description=Email addresses to invite"`4325 Recurrence *RecurrenceInput `json:"recurrence,omitempty"`4326 }43274328 type ListCalendarEventsInput struct {4329 Date string `json:"date" jsonschema:"required,description=Date in YYYY-MM-DD format"`4330 }43314332 func main() {4333 client := anthropic.NewClient()4334 ctx := context.Background()43354336 // Define each tool as a handler function. Returning an error sends the4337 // message back to Claude as a tool result with is_error set.4338 createCalendarEvent, err := toolrunner.NewBetaToolFromJSONSchema(4339 "create_calendar_event",4340 "Create a calendar event with attendees and optional recurrence.",4341 func(ctx context.Context, input CreateCalendarEventInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {4342 if len(input.Attendees) > 10 {4343 return anthropic.BetaToolResultBlockParamContentUnion{}, fmt.Errorf("too many attendees (max 10)")4344 }4345 return anthropic.BetaToolResultBlockParamContentUnion{4346 OfText: &anthropic.BetaTextBlockParam{4347 Text: fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, input.Title),4348 },4349 }, nil4350 },4351 )4352 if err != nil {4353 log.Fatal(err)4354 }43554356 listCalendarEvents, err := toolrunner.NewBetaToolFromJSONSchema(4357 "list_calendar_events",4358 "List all calendar events on a given date.",4359 func(ctx context.Context, input ListCalendarEventsInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {4360 return anthropic.BetaToolResultBlockParamContentUnion{4361 OfText: &anthropic.BetaTextBlockParam{4362 Text: `{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}`,4363 },4364 }, nil4365 },4366 )4367 if err != nil {4368 log.Fatal(err)4369 }43704371 // The runner calls the API, runs requested tools, and feeds results back4372 // until Claude produces a final answer.4373 runner := client.Beta.Messages.NewToolRunner(4374 []anthropic.BetaTool{createCalendarEvent, listCalendarEvents},4375 anthropic.BetaToolRunnerParams{4376 BetaMessageNewParams: anthropic.BetaMessageNewParams{4377 Model: anthropic.ModelClaudeOpus5,4378 MaxTokens: 1024,4379 Messages: []anthropic.BetaMessageParam{4380 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(4381 "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",4382 )),4383 },4384 },4385 },4386 )43874388 var finalMessage *anthropic.BetaMessage4389 for message, err := range runner.All(ctx) {4390 if err != nil {4391 log.Fatal(err)4392 }4393 finalMessage = message4394 }43954396 for _, block := range finalMessage.Content {4397 if block.Type == "text" {4398 fmt.Println(block.Text)4399 }4400 }4401 }4402 ```44034404 ```java Java4405 // Ring 5: The Tool Runner SDK abstraction.44064407 import com.anthropic.client.AnthropicClient;4408 import com.anthropic.client.okhttp.AnthropicOkHttpClient;4409 import com.anthropic.helpers.BetaToolRunner;4410 import com.anthropic.models.beta.messages.BetaMessage;4411 import com.anthropic.models.beta.messages.MessageCreateParams;4412 import com.anthropic.models.messages.Model;4413 import com.fasterxml.jackson.annotation.JsonClassDescription;4414 import com.fasterxml.jackson.annotation.JsonPropertyDescription;4415 import java.util.List;4416 import java.util.function.Supplier;44174418 // Define each tool as a class: the fields describe the input schema, and the4419 // get() method holds the implementation. Throwing an exception sends the4420 // message back to Claude as a tool result with is_error set.4421 @JsonClassDescription("Create a calendar event with attendees.")4422 static class CreateCalendarEvent implements Supplier<String> {4423 @JsonPropertyDescription("Event title")4424 public String title;44254426 @JsonPropertyDescription("Start time in ISO 8601 format")4427 public String start;44284429 @JsonPropertyDescription("End time in ISO 8601 format")4430 public String end;44314432 @JsonPropertyDescription("Email addresses to invite")4433 public List<String> attendees;44344435 @Override4436 public String get() {4437 if (attendees != null && attendees.size() > 10) {4438 throw new IllegalArgumentException("Too many attendees (max 10)");4439 }4440 return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}";4441 }4442 }44434444 @JsonClassDescription("List all calendar events on a given date.")4445 static class ListCalendarEvents implements Supplier<String> {4446 @JsonPropertyDescription("Date in YYYY-MM-DD format")4447 public String date;44484449 @Override4450 public String get() {4451 return "{\"events\": [{\"title\": \"Existing meeting\", \"start\": \"14:00\", \"end\": \"15:00\"}]}";4452 }4453 }44544455 void main() {4456 AnthropicClient client = AnthropicOkHttpClient.fromEnv();44574458 // The runner calls the API, runs requested tools, and feeds results back4459 // until Claude produces a final answer.4460 BetaToolRunner runner = client.beta()4461 .messages()4462 .toolRunner(MessageCreateParams.builder()4463 .model(Model.CLAUDE_OPUS_5)4464 .maxTokens(1024)4465 .addBeta("structured-outputs-2025-11-13")4466 .addUserMessage("Check what I have next Monday, then schedule a planning session that avoids any conflicts.")4467 .addTool(CreateCalendarEvent.class)4468 .addTool(ListCalendarEvents.class)4469 .build());44704471 BetaMessage finalMessage = null;4472 for (BetaMessage message : runner) {4473 finalMessage = message;4474 }44754476 finalMessage.content().stream()4477 .flatMap(block -> block.text().stream())4478 .forEach(textBlock -> IO.println(textBlock.text()));4479 }4480 ```44814482 ```php PHP4483 <?php44844485 // Ring 5: The Tool Runner SDK abstraction.44864487 use Anthropic\Client;4488 use Anthropic\Lib\Tools\BetaRunnableTool;4489 use Anthropic\Messages\Model;44904491 $client = new Client();44924493 // Define each tool as a runnable tool: the definition carries the JSON Schema4494 // and the run closure holds the implementation. Throwing an exception sends the4495 // message back to Claude as a tool result with is_error set.4496 $createCalendarEvent = new BetaRunnableTool(4497 definition: [4498 'name' => 'create_calendar_event',4499 'description' => 'Create a calendar event with attendees and optional recurrence.',4500 'input_schema' => [4501 'type' => 'object',4502 'properties' => [4503 'title' => ['type' => 'string', 'description' => 'Event title'],4504 'start' => ['type' => 'string', 'description' => 'Start time in ISO 8601 format'],4505 'end' => ['type' => 'string', 'description' => 'End time in ISO 8601 format'],4506 'attendees' => [4507 'type' => 'array',4508 'items' => ['type' => 'string'],4509 'description' => 'Email addresses to invite',4510 ],4511 'recurrence' => [4512 'type' => 'object',4513 'properties' => [4514 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']],4515 'count' => ['type' => 'integer', 'minimum' => 1],4516 ],4517 ],4518 ],4519 'required' => ['title', 'start', 'end'],4520 ],4521 ],4522 run: function (array $input): string {4523 if (count($input['attendees'] ?? []) > 10) {4524 throw new InvalidArgumentException('Too many attendees (max 10)');4525 }45264527 return json_encode([4528 'event_id' => 'evt_123',4529 'status' => 'created',4530 'title' => $input['title'],4531 ]);4532 },4533 );45344535 $listCalendarEvents = new BetaRunnableTool(4536 definition: [4537 'name' => 'list_calendar_events',4538 'description' => 'List all calendar events on a given date.',4539 'input_schema' => [4540 'type' => 'object',4541 'properties' => [4542 'date' => ['type' => 'string', 'description' => 'Date in YYYY-MM-DD format'],4543 ],4544 'required' => ['date'],4545 ],4546 ],4547 run: fn (array $input): string => json_encode([4548 'events' => [['title' => 'Existing meeting', 'start' => '14:00', 'end' => '15:00']],4549 ]),4550 );45514552 // The runner calls the API, runs requested tools, and feeds results back4553 // until Claude produces a final answer.4554 $runner = $client->beta->messages->toolRunner(4555 maxTokens: 1024,4556 messages: [4557 [4558 'role' => 'user',4559 'content' => 'Check what I have next Monday, then schedule a planning session that avoids any conflicts.',4560 ],4561 ],4562 model: Model::CLAUDE_OPUS_5,4563 tools: [$createCalendarEvent, $listCalendarEvents],4564 );45654566 $finalMessage = null;4567 foreach ($runner as $message) {4568 $finalMessage = $message;4569 }45704571 foreach ($finalMessage->content as $block) {4572 if ($block->type === 'text') {4573 echo $block->text, "\n";4574 }4575 }4576 ```45774578 ```ruby Ruby4579 # Ring 5: The Tool Runner SDK abstraction.45804581 require "anthropic"45824583 client = Anthropic::Client.new45844585 # Define each tool as a class: a typed input model describes the schema, and4586 # the call method holds the implementation. Raising an error sends the message4587 # back to Claude as a tool result with is_error set.4588 class RecurrenceInput < Anthropic::BaseModel4589 optional :frequency, Anthropic::InputSchema::EnumOf["daily", "weekly", "monthly"],4590 doc: "How often the event repeats"4591 optional :count, Integer, doc: "Number of occurrences"4592 end45934594 class CreateCalendarEventInput < Anthropic::BaseModel4595 required :title, String, doc: "Event title"4596 required :start, String, doc: "Start time in ISO 8601 format"4597 required :end, String, doc: "End time in ISO 8601 format"4598 optional :attendees, Anthropic::InputSchema::ArrayOf[String], doc: "Email addresses to invite"4599 optional :recurrence, RecurrenceInput, doc: "Optional recurrence rule"4600 end46014602 class CreateCalendarEvent < Anthropic::BaseTool4603 doc "Create a calendar event with attendees and optional recurrence."4604 input_schema CreateCalendarEventInput46054606 def call(input)4607 raise ArgumentError, "Too many attendees (max 10)" if input.attendees && input.attendees.length > 104608 JSON.generate({event_id: "evt_123", status: "created", title: input.title})4609 end4610 end46114612 class ListCalendarEventsInput < Anthropic::BaseModel4613 required :date, String, doc: "Date in YYYY-MM-DD format"4614 end46154616 class ListCalendarEvents < Anthropic::BaseTool4617 doc "List all calendar events on a given date."4618 input_schema ListCalendarEventsInput46194620 def call(input)4621 JSON.generate({events: [{title: "Existing meeting", start: "14:00", end: "15:00"}]})4622 end4623 end46244625 # The runner calls the API, runs requested tools, and feeds results back4626 # until Claude produces a final answer.4627 runner = client.beta.messages.tool_runner(4628 model: "claude-opus-5",4629 max_tokens: 1024,4630 tools: [CreateCalendarEvent.new, ListCalendarEvents.new],4631 messages: [4632 {4633 role: "user",4634 content: "Check what I have next Monday, then schedule a planning session that avoids any conflicts."4635 }4636 ]4637 )46384639 final_message = nil4640 runner.each_message { |message| final_message = message }46414642 final_message.content.each do |block|4643 puts block.text if block.type == :text4644 end4645 ```4646</CodeGroup>46474648**What to expect**46494650```text Output wrap4651I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.4652```46534654The output is identical to Ring 3. The difference is in the code: roughly half the lines, no manual loop, and the schema lives next to the implementation.46554656## What you built46574658You started with a single hardcoded tool call and ended with a production-shaped agent that handles multiple tools, parallel calls, and errors, then collapsed all of that into the Tool Runner. Along the way you saw every piece of the tool-use protocol: `tool_use` blocks, `tool_result` blocks, `tool_use_id` matching, `stop_reason` checking, and `is_error` signaling.46594660## Next steps46614662<CardGroup>4663 <Card href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools" title="Define tools">4664 Schema specification and best practices.4665 </Card>46664667 <Card href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner" title="Tool Runner">4668 The full SDK abstraction reference.4669 </Card>46704671 <Card href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/troubleshooting-tool-use" title="Troubleshooting">4672 Fix common tool-use errors.4673 </Card>4674</CardGroup>4675