Introduction & Overview

In the previous lessons, you learned how to create tool schemas and how to understand Claude's responses when it wants to use tools. You can now recognize when Claude requests tool execution through the stop_reason: "tool_use" signal and how to extract the necessary details from tool use blocks. However, knowing what Claude wants to do is only half the story — you still need to actually execute those tools and complete the conversation cycle.

In this lesson, you will learn how to bridge that gap by executing the methods Claude requests, capturing their results, and sending those results back to Claude in the proper format. By the end of this lesson, you will have a complete tool execution pipeline that can handle Claude's tool requests from start to finish, maintaining a proper conversation flow throughout the entire process.

The Complete Tool Execution Flow

Before we dive into the implementation, let's understand the complete workflow we will be building in this lesson. Here is the step-by-step process that transforms Claude from a simple chatbot into a capable agent:

  1. Set up the foundation — Create a tool registry hash mapping tool names to Ruby Method objects, load tool schemas from JSON, and prepare initial conversation messages.
  2. Send the initial request — Make the first API call to Claude with the user's question and available tools using client.messages.create.
  3. Detect tool use requests — Check Claude's response for the "tool_use" stop reason by examining response.stop_reason.to_s. (Just as we saw in the previous unit, we use .to_s because the Anthropic Ruby SDK returns field values as symbol-like objects; converting them ensures a robust comparison against our string literal "tool_use".)
  4. Extract tool information — Iterate through response.content to extract the method name, input parameters, and unique ID from each tool use block.
  5. Execute the requested methods — Use our tool registry hash to retrieve Method objects and call them with Claude's parameters as keyword arguments.
  6. Collect and format tool results — Build an array of hashes containing all method outputs in the specific format that Claude expects.
Setting Up the Foundation

Before diving into tool execution, we need to establish the foundation that connects Claude's tool requests to our actual Ruby methods. As we covered in previous lessons, this involves creating a tool registry hash and preparing our tool schemas and initial messages. The critical component here is the tool registry hash — this serves as the bridge between the tool names Claude uses and our actual Ruby Method objects.

This setup creates everything we need for tool execution: the tools hash enables dynamic method lookup using Method objects, the system_prompt guides Claude's behavior, the tool_schemas provide technical specifications, and the messages array starts the conversation. The tool registry is particularly important because it allows our code to execute the correct method based on Claude's string-based tool requests. By storing Method objects (created with method(:sum_numbers)), we can later call them dynamically with the parameters that Claude provides.

Sending the Initial Request

With our foundation in place, we can now send the initial request to Claude and immediately prepare for processing its response. This approach ensures that we maintain a proper conversation history from the start.

This API call provides Claude with the complete context: the user's question through messages, behavioral guidance through system, and available capabilities through tools. The Ruby SDK uses keyword arguments for all parameters, making the API call clear and self-documenting. By immediately adding Claude's response to the messages array using the << operator, we ensure proper conversation flow, whether Claude needs tools or can answer directly. Note that we append response.content directly — the SDK returns this as an array of content blocks, which is the format required for maintaining conversation history.

Detecting Tool Use Requests

After receiving Claude's response and adding it to our conversation history, we examine the stop_reason field to determine whether tool execution is needed. When Claude determines that tools are required, it signals this through the "tool_use" stop reason.

The response.stop_reason.to_s == "tool_use" condition indicates that Claude has identified tools it wants to execute and is waiting for their results before continuing. Just as we saw with the content item types in the previous unit, we use .to_s because the SDK returns these field values as symbol-like objects. Converting them ensures a robust string comparison against "tool_use".

We initialize a tool_results array to collect all the results from this tool execution cycle. The extraction process identifies each tool use block within Claude's response by checking content_item.type.to_s == "tool_use" and pulls out the three critical pieces of information: the method name, the input parameters (with a fallback to an empty hash if nil), and the unique identifier. The is particularly important because we will need it to match results back to their corresponding tool requests.

Executing the Requested Methods

With the tool information extracted, we can now execute the actual Ruby methods using our tool registry hash. This is where the tool names from Claude's requests are translated into actual method calls.

Using a begin-rescue block provides comprehensive error handling that catches not only missing tools but also any runtime errors that might occur during method execution. The tools.fetch(tool_name) call retrieves the actual Method object from our registry hash and raises a KeyError if the tool does not exist. (We use .fetch instead of the standard bracket syntax tools[tool_name] because [] would return nil for a missing key, leading to a confusing NoMethodError later. lets us immediately catch the missing tool in our block.) Before calling the method, we use to convert the input hash keys from strings (as Claude sends them) to symbols (as Ruby keyword arguments expect them). The symbol in is a Ruby shorthand that iterates over the hash and applies the method to each key. The syntax unpacks the hash as keyword arguments, allowing dynamic method execution based on Claude's string-based requests. This approach gracefully handles both missing methods and execution failures, ensuring that the conversation can continue even when errors occur.

Collecting and Formatting Tool Results

After executing each tool (or encountering an error), we must format the results in the specific structure that Claude expects and collect them all before sending them back. This ensures that all tool results are sent together as a single message.

The tool result structure has specific requirements: each result needs a type: "tool_result" field, the tool_use_id must exactly match the id from the original tool use block, and the content must be converted to a string using .to_s. By collecting all results in an array using the << operator and sending them as a single message, we maintain a proper conversation structure while ensuring Claude receives results for all requested tools.

It is critical to provide a tool result for every tool use request that Claude makes. If you skip providing a result for any tool_use_id, your next API call to Claude will fail because Claude expects to receive results for all the tools it requested. This is why we handle both successful executions and errors in the same way; Claude needs to know what happened with each tool request, whether it succeeded or failed.

Getting Claude's Final Response

Once we have executed all requested tools and added their results to the messages array as a single message, we can send the updated conversation back to Claude to obtain the final response that incorporates the tool results.

This second API call uses the same parameters as the first, but now the messages array contains the complete conversation history including all tool results in a single message. Claude can now provide a comprehensive answer that incorporates the tool execution results, transforming raw calculation outputs into natural, conversational responses. We access the final text using final_response.content.first.text, which retrieves the text from the first content block in Claude's response.

The complete execution flow produces output similar to this, showing both the tool execution and Claude's final response:

This output demonstrates the complete tool execution cycle: our code executes the requested method with the provided parameters and captures the numerical result, and then Claude transforms that raw output into a natural, conversational response that directly answers the user's original question.

Handling Non-Tool Responses

Not every user request will require tool usage. When Claude can answer directly without needing to execute methods, it will respond with a different stop_reason. Since we have already added Claude's response to our messages array, we just need to handle the display of non-tool responses.

This else block catches any stop_reason that is not "tool_use" and handles it appropriately. For example, if a user asks "What is the capital of France?", Claude would respond directly without needing mathematical calculations. The most common alternative stop_reason you will encounter is "end_turn", which indicates that Claude has finished its response and does not need any tools to answer the user's question.

If you prefer to be more explicit about which stop reasons you are handling, you can replace the general else with a specific condition:

Both approaches accomplish the same goal, but the explicit elsif makes it clear that you are specifically handling the "end_turn" case. This ensures your application handles both tool-requiring and non-tool scenarios gracefully, maintaining a proper conversation flow in all cases.

Visualizing the Complete Conversation History

Throughout the tool execution cycle, maintaining a proper conversation history enables Claude to understand context and provide coherent responses. Let's examine how the complete conversation flow develops through each step of the process.

We check for strings versus arrays because message content can be in either format. As a best practice, use simple strings for your initial user prompts (like our "Please calculate 15 + 27"), but use arrays of objects when constructing complex messages that involve tool uses and results. The SDK will always return Claude's responses as arrays of structured content blocks. Without this check using content.is_a?(String), iterating over a string would print each character separately instead of the full message. The each_with_index iterator provides us with both the message and its index, allowing us to number the messages clearly.

This debugging output reveals the complete conversation structure that enables Claude's tool-using capabilities:

This conversation history shows the complete tool execution cycle: the user's initial request, Claude's response containing both an explanation and a tool request (with a Ruby hash representation of the content blocks), our tool result with the matching ID, and Claude's final response incorporating the tool output. Note that the Ruby SDK returns content blocks as objects that display as hashes when they are printed, showing fields like , , , , and . This structure demonstrates how proper message management creates a seamless conversation flow that transforms Claude into a capable agent.

Summary & Practice Preparation

You now understand the complete tool execution workflow that transforms Claude from a text-only assistant into an agent capable of performing actions and calculations. The process involves detecting tool use requests through stop reasons, executing methods through a tool registry hash of Method objects, collecting and formatting all results together, and maintaining a conversation history throughout the entire cycle.

The key components work together to create a robust system: the tool registry hash enables dynamic execution by mapping tool names to Method objects; proper result collection ensures all tools are handled; correct message formatting using Ruby hashes and arrays allows Claude to understand results; and conversation history maintains context across the entire interaction. The Ruby-specific techniques — such as using method(:function_name) to create Method objects, .to_s for stop reason comparison, transform_keys(&:to_sym) for parameter normalization, and begin-rescue blocks for error handling — combine to create a robust implementation that gracefully handles both successful executions and failures.

In the upcoming practice exercises, you will implement this complete workflow yourself, working with different types of tools and handling various scenarios, including multiple tool uses and error handling. Happy coding!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal