Writing Tool Schemas for Claude

Introduction & Goals

Welcome to your first lesson in developing Claude agents with tool integration! In this lesson, you'll learn the foundational skill of preparing function schemas that enable Claude to understand and request the use of your custom tools through a process called function calling.

By the end of this lesson, you'll understand how to write Ruby methods in functions.rb and create JSON schemas in schemas.json that describe these methods to Claude. You'll also learn how to wire everything together in main.rb using a tool registry that maps schema names to Ruby Method objects. These schemas are the bridge that allows Claude to understand what your methods do and how to call them, even though Claude never sees your actual Ruby code. This foundational step is essential before you can build a complete Claude agent system that can execute tools and use their results.

How Claude Uses Tools Through Function Calling

Function calling is the mechanism that allows Claude to use external tools and capabilities beyond text generation. Here is how the process works conceptually:

  1. You provide Claude with function schemas (JSON descriptions of your tools).
  2. Claude analyzes user requests and determines if any of your tools would be helpful.
  3. If Claude decides a tool is needed, it responds with a tool use request that includes the function name and specific parameters.
  4. Your system receives this tool use request, looks up the corresponding Ruby method in your tool registry, and executes it with the provided parameters.
  5. Your system sends the method result back to Claude.
  6. Claude incorporates this result into its response to the user or decides to use additional tools if needed.

The key insight is that Claude only sees the schemas (JSON descriptions), never your actual Ruby code. The schemas must contain all the information Claude needs to understand what each tool does and how to use it correctly. This separation means you can organize your Ruby methods however you like — Claude relies entirely on the schema descriptions to make decisions about tool usage.

In this lesson, we'll focus on preparing the schemas, creating the tool registry, and verifying that your local tool dispatch works correctly. This prepares you for integrating with the Anthropic API's tool-calling capabilities in future lessons.

Writing Ruby Tool Methods

When creating tool methods for Claude agents, your Ruby methods serve two purposes: they contain the actual logic that will be executed, and they provide the foundation for creating accurate schemas. While Claude never sees these methods directly, writing them clearly helps you create better schema descriptions.

In your functions.rb file, you can define your tool methods:

Ruby
# frozen_string_literal: true

def sum_numbers(a, b)
  # Sum two numbers and return the result.
  a + b
end

You will notice # frozen_string_literal: true at the top of our Ruby files. This is a common Ruby pragma that prevents strings from being modified after they are created, improving performance and memory usage. While not strictly necessary for tool integrations, it is a best practice in modern Ruby development.

This method includes a clear comment that describes what it does. Ruby methods are concise and straightforward — you define parameters without type annotations, and the last expression is automatically returned. These methods should be simple and focused, performing one clear task that you can describe accurately in a schema.

Remember that Claude will only see the schema you create, not this Ruby code. The comments are for your benefit when translating the method into a schema that Claude can understand.

Creating JSON Schemas for Single Functions

JSON schemas are the structured descriptions that tell Claude exactly what each tool does and how to use it. These schemas are the only information Claude receives about your tools, making them crucial for successful function calling.

JSON
{
  "name": "sum_numbers",
  "description": "Sum two numbers and return the result",
  "input_schema": {
    "type": "object",
    "properties": {
      "a": {
        "type": "number",
        "description": "First number to add"
      },
      "b": {
        "type": "number",
        "description": "Second number to add"
      }
    },
    "required": ["a", "b"]
  }
}

Each schema contains several essential components that Claude uses to understand your tool:

  • The name field identifies the tool and should be descriptive to help Claude understand what the tool does and when to use it. While it doesn't necessarily need to match your Ruby method name, keeping them consistent helps maintain clarity in your code.
  • The description tells Claude what the tool does and when to use it.
  • The input_schema section describes the parameters using JSON Schema format, where each property represents a method parameter with its data type and description.
  • The required array lists which parameters are mandatory.

While we use "number" for our math tools, JSON Schema supports several other data types you might need for different tools, such as "string" for text, "integer" for whole numbers, "boolean" for true/false values, and "array" for lists of items.

Claude uses this schema information to decide when to use your tool and what parameters to provide. If the descriptions are unclear or incomplete, Claude may use the tool incorrectly or not at all.

Building Multiple Tool Schemas

Real Claude agents typically have access to multiple tools, each described by its own schema. Let's create a second method and organize our methods in a dedicated file. In your functions.rb file, you can collect your tool methods:

Ruby
# frozen_string_literal: true

def sum_numbers(a, b)
  # Sum two numbers and return the result.
  a + b
end

def multiply_numbers(a, b)
  # Multiply two numbers and return the result.
  a * b
end

Organizing your methods in a separate file keeps your code organized and makes it easy to require them when needed with require_relative "functions". Each method follows the same pattern with clear comments, providing consistency across your tool collection.

Organizing Multiple Schemas in JSON Format

Just as you organize your Ruby methods in functions.rb, you can organize your corresponding schemas in a schemas.json file. This creates a clean separation between your implementation and the descriptions that Claude will see:

JSON
[
  {
    "name": "sum_numbers",
    "description": "Sum two numbers and return the result",
    "input_schema": {
      "type": "object",
      "properties": {
        "a": {
          "type": "number",
          "description": "First number to add"
        },
        "b": {
          "type": "number",
          "description": "Second number to add"
        }
      },
      "required": ["a", "b"]
    }
  },
  {
    "name": "multiply_numbers",
    "description": "Multiply two numbers and return the result",
    "input_schema": {
      "type": "object",
      "properties": {
        "a": {
          "type": "number",
          "description": "First number to multiply"
        },
        "b": {
          "type": "number",
          "description": "Second number to multiply"
        }
      },
      "required": ["a", "b"]
    }
  }
]

When Claude receives this array of schemas, it can analyze user requests and select the most appropriate tool. For example, if a user asks "What's 5 plus 3?", Claude would choose sum_numbers. If they ask "What's 4 times 7?", Claude would choose multiply_numbers. The clear descriptions in each schema help Claude make these decisions accurately.

Function-to-Schema Mapping

After organizing your methods in functions.rb and schemas in schemas.json, you need to load both and create a mapping that connects them. This creates two essential components: the schemas that you provide to Claude, and the mapping that you use to execute methods when Claude requests them.

In your main.rb file:

Ruby
# frozen_string_literal: true

require "json"
require_relative "functions"

# Load the schemas from JSON file
tool_schemas = JSON.parse(File.read("schemas.json"))

# Create a Hash mapping tool names to functions
tools = {
  "sum_numbers" => method(:sum_numbers),
  "multiply_numbers" => method(:multiply_numbers)
}

The tool_schemas variable contains the parsed JSON array that you'll provide to Claude — this is how Claude learns about your available tools and their capabilities. The tools Hash is your internal mapping that you use to execute the correct Ruby method when Claude requests a tool by name.

This mapping must be precise — the keys in your tools Hash must exactly match the name fields in your schemas. If there is a mismatch, your system will not be able to execute the method when Claude requests it.

Notice that we use method(:sum_numbers) to obtain a Method object. The :sum_numbers syntax (with a leading colon) creates a Ruby symbol, which is a lightweight, immutable string commonly used for naming things like methods or hash keys. Passing this symbol to the method function allows us to store a reference to the method that can be called later with .call(...). This pattern is essential for dynamically dispatching tool requests.

For example, when Claude analyzes a user request and decides to use the "sum_numbers" tool with parameters {"a": 10, "b": 5}, the process works like this:

  1. Claude references the tool_schemas to understand available tools.
  2. Claude sends you a tool use request for "sum_numbers" with the parameters.
  3. You look up "sum_numbers" in your tools Hash.
  4. You execute tools["sum_numbers"].call(10, 5), which calls your actual sum_numbers method.
  5. You return the result back to Claude.

Claude only sees the tool_schemas, while the tools Hash is your internal system for translating Claude's requests into actual method calls.

Verifying Your Tool Schemas

Before using your tool schemas with Claude, it is important to verify they are structured correctly and contain all necessary information. In your main.rb file, you can add:

Ruby
# Print the schemas
puts JSON.pretty_generate(tool_schemas)

This displays your tool definitions in a formatted way, allowing you to review each schema's structure:

text
[
  {
    "name": "sum_numbers",
    "description": "Sum two numbers and return the result",
    "input_schema": {
      "type": "object",
      "properties": {
        "a": {
          "type": "number",
          "description": "First number to add"
        },
        "b": {
          "type": "number",
          "description": "Second number to add"
        }
      },
      "required": [
        "a",
        "b"
      ]
    }
  },
  {
    "name": "multiply_numbers",
    "description": "Multiply two numbers and return the result",
    "input_schema": {
      "type": "object",
      "properties": {
        "a": {
          "type": "number",
          "description": "First number to multiply"
        },
        "b": {
          "type": "number",
          "description": "Second number to multiply"
        }
      },
      "required": [
        "a",
        "b"
      ]
    }
  }
]

Review this output to ensure your schemas have clear descriptions, correct parameter types, and complete required field lists. These details are crucial since Claude depends entirely on this schema information to use your tools correctly.

Testing Your Function Mapping

With your schemas verified and mapping in place, you can test that your system can successfully execute tools based on their names. In your main.rb file, add:

Ruby
# Use tools from the Hash
result1 = tools["sum_numbers"].call(10, 5)
puts "sum_numbers(10, 5) = #{result1}"

result2 = tools["multiply_numbers"].call(4, 7)
puts "multiply_numbers(4, 7) = #{result2}"

Running ruby main.rb produces:

text
sum_numbers(10, 5) = 15
multiply_numbers(4, 7) = 28

This confirms your method mapping works correctly and your system can execute tools by name using the .call method on Method objects. When Claude requests tool usage through function calling, you'll receive the tool name and parameters from Claude, then use this mapping to look up and call the corresponding Ruby method yourself, before sending the result back to Claude.

Summary & Next Steps

You've now learned the essential process of preparing tools for Claude agents through function calling. This involves writing well-structured Ruby methods in functions.rb, creating detailed JSON schemas in schemas.json that describe these methods to Claude, and setting up a mapping system in main.rb using a Hash that connects schema names to Method objects.

Remember the key separation: Claude only sees the schemas and uses them to make tool selection decisions and provide parameters. Your Ruby methods and Hash-based registry handle the actual execution. The quality of your schemas directly impacts how effectively Claude can use your tools.

You've verified that your schemas are well-formed and that your local tool dispatch mechanism works correctly. This foundation — prepared schemas and a working tool registry — sets you up for integrating with the Anthropic API's tool-calling capabilities in future lessons.

In the upcoming practice exercises, you'll apply these concepts to create your own tool methods and schemas, building toward the point where you can integrate these prepared tools with Claude for powerful agent capabilities.

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