build-a-tool-using-agent changedagents-and-tools/tool-use/build-a-tool-using-agent
Nearest release: v2.1.304, published under an hour after this site recorded the change. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Recorded here
Lines+37added
Lines−731removed
From line
18
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits12to this page, all time
The whole hunk
from line 18, old and new numbered
/
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 18
1818
1919The 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.
2020
21<CodeGroup>
22 ```bash cURL
23 #!/bin/bash
24 # Ring 1: Single tool, single turn.
25
26 # Define one tool as a JSON fragment. The input_schema is a JSON Schema
27 # object describing the arguments Claude should pass when it calls this
28 # tool. This schema includes nested objects (recurrence), arrays
29 # (attendees), and optional fields, which is closer to real-world tools
30 # 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 ]'
57
58 USER_MSG="Schedule a 30-minute sync with [email protected] and [email protected] on Monday, March 30, 2026 at 10am."
59
60 # Send the user's request along with the tool definition. Claude decides
61 # 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-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 }')")
76
77 # 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')"
80
81 # Find the tool_use block. A response may contain text blocks before the
82 # 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')"
87
88 # 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"}'
91
92 # Send the result back. The tool_result block goes in a user message and
93 # its tool_use_id must match the id from the tool_use block above. The
94 # 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-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 }')")
119
120 # With the tool result in hand, Claude produces a final natural-language
121 # 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 ```
125
126 ```bash CLI
127 #!/usr/bin/env bash
128 # Ring 1: Single tool, single turn.
129 # Uses jq for cross-turn message-array state — building an agentic loop in shell
130 # requires JSON manipulation beyond ant's single-call --transform scope.
131 set -euo pipefail
132
133 USER_MSG="Schedule a 30-minute sync with [email protected] and [email protected] on Monday, March 30, 2026 at 10am."
134 MESSAGES=$(jq -n --arg msg "$USER_MSG" '[{role: "user", content: $msg}]')
135
136 # Define one tool. The input_schema is a JSON Schema object describing
137 # the arguments Claude should pass when it calls this tool. This schema
138 # includes nested objects (recurrence), arrays (attendees), and optional
139 # 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, no
142 # hand-built JSON envelope. The static keys (model, tools, tool_choice)
143 # live in a quoted heredoc; the growing messages array is appended as
144 # JSON, which YAML accepts as flow syntax.
145 {
146 cat <<'YAML'
147 model: claude-opus-5-5
148 max_tokens: 1024
149 tool_choice: {type: auto, disable_parallel_tool_use: true}
150 tools:
151 - name: create_calendar_event
152 description: Create a calendar event with attendees and optional recurrence.
153 input_schema:
154 type: object
155 properties:
156 title: {type: string}
157 start: {type: string, format: date-time}
158 end: {type: string, format: date-time}
159 attendees:
160 type: array
161 items: {type: string, format: email}
162 recurrence:
163 type: object
164 properties:
165 frequency: {enum: [daily, weekly, monthly]}
166 count: {type: integer, minimum: 1}
167 required: [title, start, end]
168 YAML
169 printf 'messages: %s\n' "$MESSAGES"
170 } | ant messages create --format json
171 }
172
173 # Send the user's request along with the tool definition. Claude decides
174 # whether to call the tool based on the request and the tool description.
175 RESPONSE=$(call_api)
176
177 # 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")"
180
181 # Find the tool_use block. A response may contain text blocks before the
182 # 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")"
187
188 # 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"}'
191
192 # Send the result back. The tool_result block goes in a user message and
193 # its tool_use_id must match the id from the tool_use block above. The
194 # 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")
205
206 FOLLOWUP=$(call_api)
207
208 # With the tool result in hand, Claude produces a final natural-language
209 # 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 ```
21<CodeGroup exclude="shell:cURL, shell:CLI">
22 <CodeGroupItem language="cURL">
23 This workflow needs more logic than a single shell command. Use the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) to run it.
24 </CodeGroupItem>
25
26 <CodeGroupItem language="CLI">
27 This workflow needs more logic than a single shell command. Use the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) to run it.
28 </CodeGroupItem>
21329
21430 ```python Python
21531 # Ring 1: Single tool, single turn.
from line 845
1029845
1030846The 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.
1031847
1032<CodeGroup>
1033 ```bash cURL
1034 #!/bin/bash
1035 # Ring 2: The agentic loop.
1036
1037 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 ]'
1060
1061 run_tool() {
1062 local name="$1"
1063 local input="$2"
1064 if [ "$name" = "create_calendar_event" ]; then
1065 local title=$(echo "$input" | jq -r '.title')
1066 jq -n --arg title "$title" '{event_id: "evt_123", status: "created", title: $title}'
1067 else
1068 echo "{\"error\": \"Unknown tool: $name\"}"
1069 fi
1070 }
1071
1072 # 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, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected]."}]'
1074
1075 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-5", max_tokens: 1024, tools: $tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: $messages}')"
1082 }
1083
1084 RESPONSE=$(call_api)
1085
1086 # Loop until Claude stops asking for tools. Each iteration runs the requested
1087 # tool, appends the result to history, and asks Claude to continue.
1088 while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
1089 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")
1094
1095 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 ]')
1104
1105 RESPONSE=$(call_api)
1106 done
1107
1108 echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'
1109 ```
1110
1111 ```bash CLI
1112 #!/usr/bin/env bash
1113 # Ring 2: The agentic loop.
1114 # Uses jq for cross-turn message-array state — building an agentic loop in shell
1115 # requires JSON manipulation beyond ant's single-call --transform scope.
1116 set -euo pipefail
1117
1118 run_tool() {
1119 local name="$1" input="$2"
1120 if [ "$name" = "create_calendar_event" ]; then
1121 jq -n --arg title "$(jq -r '.title' <<<"$input")" \
1122 '{event_id: "evt_123", status: "created", title: $title}'
1123 else
1124 printf '{"error": "Unknown tool: %s"}' "$name"
1125 fi
1126 }
1127
1128 # Keep the full conversation history in a JSON array so each turn sees
1129 # prior context.
1130 MESSAGES='[{"role": "user", "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected]."}]'
1131
1132 call_api() {
1133 # ant reads the request body as YAML on stdin: no auth headers, no
1134 # hand-built JSON envelope. The static keys (model, tools, tool_choice)
1135 # live in a quoted heredoc; the growing messages array is appended as
1136 # JSON, which YAML accepts as flow syntax.
1137 {
1138 cat <<'YAML'
1139 model: claude-opus-5-5
1140 max_tokens: 1024
1141 tool_choice: {type: auto, disable_parallel_tool_use: true}
1142 tools:
1143 - name: create_calendar_event
1144 description: Create a calendar event with attendees and optional recurrence.
1145 input_schema:
1146 type: object
1147 properties:
1148 title: {type: string}
1149 start: {type: string, format: date-time}
1150 end: {type: string, format: date-time}
1151 attendees:
1152 type: array
1153 items: {type: string, format: email}
1154 recurrence:
1155 type: object
1156 properties:
1157 frequency: {enum: [daily, weekly, monthly]}
1158 count: {type: integer, minimum: 1}
1159 required: [title, start, end]
1160 YAML
1161 printf 'messages: %s\n' "$MESSAGES"
1162 } | ant messages create --format json
1163 }
1164
1165 RESPONSE=$(call_api)
1166
1167 # Loop until Claude stops asking for tools. Each iteration runs the
1168 # requested tool, appends the result to history, and asks Claude to
1169 # continue.
1170 while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do
1171 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")
1176
1177 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")
1187
1188 RESPONSE=$(call_api)
1189 done
1190
1191 jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE"
1192 ```
848<CodeGroup exclude="shell:cURL, shell:CLI">
849 <CodeGroupItem language="cURL">
850 This workflow needs more logic than a single shell command. Use the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) to run it.
851 </CodeGroupItem>
852
853 <CodeGroupItem language="CLI">
854 This workflow needs more logic than a single shell command. Use the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) to run it.
855 </CodeGroupItem>
1193856
1194857 ```python Python
1195858 # Ring 2: The agentic loop.
from line 1615
19521615
19531616When 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.
19541617
1955<CodeGroup>
1956 ```bash cURL
1957 #!/bin/bash
1958 # Ring 3: Multiple tools, parallel calls.
1959
1960 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 ]'
1992
1993 run_tool() {
1994 case "$1" in
1995 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 esac
2002 }
2003
2004 MESSAGES='[{"role": "user", "content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts."}]'
2005
2006 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-5", max_tokens: 1024, tools: $tools, messages: $messages}')"
2013 }
2014
2015 RESPONSE=$(call_api)
2016
2017 while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
2018 # A single response can contain multiple tool_use blocks. Process all of
2019 # them and return all results together in one user message.
2020 TOOL_RESULTS='[]'
2021 while read -r block; do
2022 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")')
2029
2030 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}]')
2034
2035 RESPONSE=$(call_api)
2036 done
2037
2038 echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'
2039 ```
2040
2041 ```bash CLI
2042 #!/usr/bin/env bash
2043 # Ring 3: Multiple tools, parallel calls.
2044 # Uses jq for cross-turn message-array state — building an agentic loop in shell
2045 # requires JSON manipulation beyond ant's single-call --transform scope.
2046 set -euo pipefail
2047
2048 run_tool() {
2049 case "$1" in
2050 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 esac
2058 }
2059
2060 MESSAGES='[{"role": "user", "content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts."}]'
2061
2062 call_api() {
2063 # ant reads the request body as YAML on stdin: no auth headers, no
2064 # hand-built JSON envelope. The static keys (model, tools) live in a
2065 # 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-5-5
2070 max_tokens: 1024
2071 tools:
2072 - name: create_calendar_event
2073 description: Create a calendar event with attendees and optional recurrence.
2074 input_schema:
2075 type: object
2076 properties:
2077 title: {type: string}
2078 start: {type: string, format: date-time}
2079 end: {type: string, format: date-time}
2080 attendees:
2081 type: array
2082 items: {type: string, format: email}
2083 recurrence:
2084 type: object
2085 properties:
2086 frequency: {enum: [daily, weekly, monthly]}
2087 count: {type: integer, minimum: 1}
2088 required: [title, start, end]
2089 - name: list_calendar_events
2090 description: List all calendar events on a given date.
2091 input_schema:
2092 type: object
2093 properties:
2094 date: {type: string, format: date}
2095 required: [date]
2096 YAML
2097 printf 'messages: %s\n' "$MESSAGES"
2098 } | ant messages create --format json
2099 }
2100
2101 RESPONSE=$(call_api)
2102
2103 while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do
2104 # A single response can contain multiple tool_use blocks. Process all
2105 # of them and return all results together in one user message.
2106 TOOL_RESULTS='[]'
2107 while read -r block; do
2108 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")
2116
2117 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")
2124
2125 RESPONSE=$(call_api)
2126 done
2127
2128 jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE"
2129 ```
1618<CodeGroup exclude="shell:cURL, shell:CLI">
1619 <CodeGroupItem language="cURL">
1620 This workflow needs more logic than a single shell command. Use the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) to run it.
1621 </CodeGroupItem>
1622
1623 <CodeGroupItem language="CLI">
1624 This workflow needs more logic than a single shell command. Use the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) to run it.
1625 </CodeGroupItem>
21301626
21311627 ```python Python
21321628 # Ring 3: Multiple tools, parallel calls.
from line 2427
29312427
29322428Tools 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.
29332429
2934<CodeGroup>
2935 ```bash cURL
2936 #!/bin/bash
2937 # Ring 4: Error handling.
2938
2939 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 ]'
2971
2972 run_tool() {
2973 case "$1" in
2974 create_calendar_event)
2975 local count=$(echo "$2" | jq '.attendees | length // 0')
2976 if [ "$count" -gt 10 ]; then
2977 echo "ERROR: Too many attendees (max 10)"
2978 return 1
2979 fi
2980 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 esac
2987 }
2988
2989 EMAILS=$(seq 0 14 | sed 's/.*/user&@example.com/' | paste -sd, -)
2990 MESSAGES="[{\"role\": \"user\", \"content\": \"Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: $EMAILS\"}]"
2991
2992 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-5", max_tokens: 1024, tools: $tools, messages: $messages}')"
2999 }
3000
3001 RESPONSE=$(call_api)
3002
3003 while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
3004 TOOL_RESULTS='[]'
3005 while read -r block; do
3006 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"); then
3010 TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \
3011 '. + [{type: "tool_result", tool_use_id: $id, content: $result}]')
3012 else
3013 # 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 fi
3017 done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")')
3018
3019 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}]')
3023
3024 RESPONSE=$(call_api)
3025 done
3026
3027 echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'
3028 ```
3029
3030 ```bash CLI
3031 #!/usr/bin/env bash
3032 # Ring 4: Error handling.
3033 # Uses jq for cross-turn message-array state — building an agentic loop in shell
3034 # requires JSON manipulation beyond ant's single-call --transform scope.
3035 set -euo pipefail
3036
3037 run_tool() {
3038 case "$1" in
3039 create_calendar_event)
3040 local count
3041 count=$(jq '.attendees | length // 0' <<<"$2")
3042 if [ "$count" -gt 10 ]; then
3043 echo "ERROR: Too many attendees (max 10)"
3044 return 1
3045 fi
3046 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 esac
3054 }
3055
3056 EMAILS=$(seq 0 14 | sed 's/.*/user&@example.com/' | paste -sd, -)
3057 MESSAGES=$(jq -n --arg msg "Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: $EMAILS" \
3058 '[{role: "user", content: $msg}]')
3059
3060 call_api() {
3061 # ant reads the request body as YAML on stdin: no auth headers, no
3062 # hand-built JSON envelope. The static keys (model, tools) live in a
3063 # 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-5-5
3068 max_tokens: 1024
3069 tools:
3070 - name: create_calendar_event
3071 description: Create a calendar event with attendees and optional recurrence.
3072 input_schema:
3073 type: object
3074 properties:
3075 title: {type: string}
3076 start: {type: string, format: date-time}
3077 end: {type: string, format: date-time}
3078 attendees:
3079 type: array
3080 items: {type: string, format: email}
3081 recurrence:
3082 type: object
3083 properties:
3084 frequency: {enum: [daily, weekly, monthly]}
3085 count: {type: integer, minimum: 1}
3086 required: [title, start, end]
3087 - name: list_calendar_events
3088 description: List all calendar events on a given date.
3089 input_schema:
3090 type: object
3091 properties:
3092 date: {type: string, format: date}
3093 required: [date]
3094 YAML
3095 printf 'messages: %s\n' "$MESSAGES"
3096 } | ant messages create --format json
3097 }
3098
3099 RESPONSE=$(call_api)
3100
3101 while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do
3102 TOOL_RESULTS='[]'
3103 while read -r block; do
3104 NAME=$(jq -r '.name' <<<"$block")
3105 INPUT=$(jq -c '.input' <<<"$block")
3106 ID=$(jq -r '.id' <<<"$block")
3107 if OUTPUT=$(run_tool "$NAME" "$INPUT"); then
3108 TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$OUTPUT" \
3109 '. + [{type: "tool_result", tool_use_id: $id, content: $result}]' \
3110 <<<"$TOOL_RESULTS")
3111 else
3112 # 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 fi
3117 done < <(jq -c '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")
3118
3119 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")
3126
3127 RESPONSE=$(call_api)
3128 done
3129
3130 jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE"
3131 ```
2430<CodeGroup exclude="shell:cURL, shell:CLI">
2431 <CodeGroupItem language="cURL">
2432 This workflow needs more logic than a single shell command. Use the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) to run it.
2433 </CodeGroupItem>
2434
2435 <CodeGroupItem language="CLI">
2436 This workflow needs more logic than a single shell command. Use the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) to run it.
2437 </CodeGroupItem>
31322438
31332439 ```python Python
31342440 # Ring 4: Error handling.
from line 3323
40173323
40183324Rings 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 `client.beta.messages.tool_runner()` (typescript: `client.beta.messages.toolRunner()`; java: `client.beta().messages().toolRunner()`; php: `$client->beta->messages->toolRunner()`; csharp: `client.Beta.Messages.ToolRunner()`; go: `client.Beta.Messages.NewToolRunner()`), and retrieve the final message once the loop completes. Error wrapping, result formatting, and conversation management are handled internally.
40193325
4020Each 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.
4021
4022<CodeGroup exclude="shell">
3326Each SDK provides a helper that turns an ordinary function into a runnable tool and derives the input schema from its signature; the following tabs show the idiomatic form for each language.
3327
3328<CodeGroup exclude="shell:cURL, shell:CLI">
40233329 <CodeGroupItem language="cURL">
4024 The Tool Runner is a helper in the Claude SDK. Raw HTTP requests have no equivalent. To keep using curl, use the loop from [Ring 4](https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent#ring-4-error-handling).
3330 The Tool Runner is a helper in the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview). Raw HTTP requests have no equivalent.
40253331 </CodeGroupItem>
40263332
40273333 <CodeGroupItem language="CLI">
4028 The Tool Runner is a helper in the Claude SDK. The `ant` CLI has no equivalent. To keep using the CLI, use the loop from [Ring 4](https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent#ring-4-error-handling).
3334 The Tool Runner is a helper in the [Claude SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview). The `ant` CLI has no equivalent.
40293335 </CodeGroupItem>
40303336
40313337 ```python Python
40323338
No line in this hunk matches that.