Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Building Robust Custom Tools

Welcome back. In our last lesson, we established the "contract" for a tool by defining its schema with Zod, covering the description, inputSchema, and outputSchema. This blueprint ensures our tools are predictable and that the language model understands their capabilities.

Now, we move from the blueprint to the implementation. This lesson focuses on writing the core logic of a custom tool within its execute function. We will build a complete tool, paying close attention to the three pillars of a production-ready implementation: robust input validation, comprehensive error handling, and the formatting of structured results. By the end of this lesson, you will have a practical, reusable pattern for building any tool your agentic system requires.

1. The execute Function: Bringing a Tool to Life

The execute function is where your tool performs its action. It can contain any arbitrary Node.js/TypeScript code, from simple calculations to complex asynchronous operations like database queries or external API calls. Since you're building a production system, most of your tools will likely involve asynchronous logic.

The Vercel AI SDK automatically provides the execute function with a typed object containing the arguments parsed by the LLM, which have already been validated against your inputSchema.

Let's look at a practical example. Imagine we're building a tool to get the weather. The execute function would need to call a weather API.

Vercel AI SDK Masterclass: From Fundamentals to Deep Research

The "Vercel AI SDK Masterclass" video provides a great real-world example of implementing a getWeather tool.

Watch the section from the tool implementation. Notice how the execute function is an async function that makes a fetch request, unwraps the JSON response, and returns the relevant data. This is a standard pattern you will use frequently.

The video demonstrates a key concept: the execute function is the bridge between the LLM's intent and the real-world action. Your experience with backend development and API integration in Node.js will be directly applicable here.

2. Input Validation and Error Handling

A production system cannot assume that everything will work perfectly. We must anticipate and handle potential failures. In the context of a tool, errors can occur in two main places:

  1. Invalid Input from the LLM: The model might misunderstand the prompt and generate arguments that don't match your inputSchema.
  2. Execution Failure: The logic inside your execute function might fail (e.g., a network error, an invalid API key, or an unexpected response from an external service).

Handling Invalid Input

The Vercel AI SDK provides a strong first line of defense. Before your execute function is ever called, the SDK validates the arguments generated by the model against the inputSchema you defined. If the validation fails, the SDK throws a specific error.

AI SDK Core: Tool Calling

The AI SDK Core documentation details the specific error types you can encounter.

Read the section on Handling Errors. Focus on the two primary errors mentioned: NoSuchToolError and InvalidToolInputError. Understanding InvalidToolInputError is crucial for debugging why a tool might not be running as expected.

When you encounter an InvalidToolInputError, it typically means you need to improve your tool's description or the .describe() strings on your schema fields to give the LLM better guidance.

Debugging tool calls often involves inspecting the raw step-by-step output from the AI SDK, as shown here. An `InvalidToolInputError` might be preceded by a tool call step where the arguments are visibly incorrect.

Handling Execution Errors

Errors within the execute function are your responsibility to handle. As a best practice, you should wrap your core logic in a try...catch block.

When an error is thrown from execute, the AI SDK catches it and, instead of crashing, appends a tool-error part to the generation result. This is a powerful feature for agentic loops, as it allows the agent (or a supervising agent) to see that the tool failed and decide on a next step—such as retrying or informing the user.

Let's see how the documentation describes this mechanism.

AI SDK Core: Tool Calling

This documentation page explains how execution errors are managed.

Now, review the part of the same section that discusses tool-error parts, starting from the example. This shows you how to programmatically access the errors after a generation completes.

For advanced observability, which is critical for your production system goal, the AI SDK offers lifecycle callbacks. These allow you to hook into the start and end of a tool's execution to log performance, inputs, and, most importantly, any errors that occur.

AI SDK Core: Tool Calling

This section of the documentation introduces powerful callbacks for monitoring tool execution.

Read the section on lifecycle callbacks. The experimental_onToolCallFinish callback is particularly useful. You can implement centralized logging here to capture every tool failure across your entire system, complete with timing information, the tool name, and the input that caused the error.

3. Formatting a Structured Result

Upon successful execution, your tool must return a result. Following the schema you defined in the previous lesson, this result should be a structured object, not a simple string. This ensures the output is predictable and can be easily consumed by the LLM in subsequent steps or by other parts of your application.

The object you return from execute must conform to the shape defined in your tool's outputSchema. The AI SDK will validate this for you, providing another layer of type safety and preventing malformed data from propagating.

A successful tool call results in a structured output. Here, the input was `{ "city": "San Francisco" }`, and the tool returned a JSON object with the city, temperature, and a weather code.

This structured input/output flow is the essence of reliable tool use in agentic systems. It transforms the unpredictable nature of natural language into the deterministic world of typed data structures.

4. A Complete, Production-Ready Tool

Let's synthesize everything we've discussed into a single, complete example. We will create a tool that fetches the current price of a cryptocurrency from the CoinGecko API. This tool will include:

  • Clear descriptions for the LLM.
  • Typed input and output schemas.
  • An asynchronous API call.
  • Robust try...catch error handling.
  • Formatting of the successful result into a structured object.
import { tool } from 'ai';
import { z } from 'zod';

// Define a tool for fetching cryptocurrency prices
export const cryptoPriceTool = tool({
  description: 'Get the current price of a cryptocurrency in a given currency.',
  
  // Zod schema for input validation
  inputSchema: z.object({
    cryptoId: z.string().describe("The ID of the cryptocurrency, e.g., 'bitcoin', 'ethereum'."),
    vsCurrency: z.string().describe("The currency to compare against, e.g., 'usd', 'eur'.").default('usd'),
  }),
  
  // Zod schema for output validation
  outputSchema: z.object({
    cryptoId: z.string(),
    vsCurrency: z.string(),
    price: z.number(),
  }),

  // The core logic of the tool
  execute: async ({ cryptoId, vsCurrency }) => {
    try {
      // Perform the API call within a try...catch block
      const url = `https://api.coingecko.com/api/v3/simple/price?ids=${cryptoId}&vs_currencies=${vsCurrency}`;
      const response = await fetch(url);

      if (!response.ok) {
        // Handle non-successful HTTP responses
        throw new Error(`API request failed with status ${response.status}`);
      }
      
      const data = await response.json();

      // Check if the API returned the expected data structure
      if (!data[cryptoId] || !data[cryptoId][vsCurrency]) {
        throw new Error(`Price data not found for ${cryptoId} in ${vsCurrency}`);
      }

      // Format the successful result into a structured object
      return {
        cryptoId,
        vsCurrency,
        price: data[cryptoId][vsCurrency],
      };
      
    } catch (error) {
      // Log the error and re-throw to let the AI SDK handle it
      console.error("Error in cryptoPriceTool:", error);
      // Re-throwing the error allows the AI SDK to create a `tool-error` part
      throw error; 
    }
  },
});

This example serves as a robust template. You can adapt this pattern of schema definition, async execution with try...catch, and structured returns to build the various tools needed for your agentic infrastructure's tool store.

Conclusion

In this lesson, you have moved from defining a tool's interface to implementing its core logic. You now have a comprehensive, production-oriented pattern for creating custom tools that are robust, predictable, and observable.

Key Takeaways:

  • The execute function contains the core logic of a tool and can perform any synchronous or asynchronous action.
  • Input validation is handled automatically by the AI SDK using the inputSchema, which throws an InvalidToolInputError on failure.
  • Execution errors inside execute should be handled with try...catch blocks. Errors thrown from this function are caught by the SDK and reported as tool-error parts, enabling agents to react to tool failures.
  • The experimental_onToolCallFinish callback is a powerful mechanism for centralized error logging and observability.
  • A tool's execute function must return a structured object that conforms to its outputSchema, ensuring end-to-end type safety.

You are now equipped to build the individual capabilities—the tools—that your worker agents will use. In the next lesson, we will take the final step in this module by implementing a basic agent that can intelligently select and use the tool you've just built to answer a user's question.

Can't find a good explanation? Sign up and we'll make it for you

Sign up