Skip to main content

Overview

Tools are external functions or APIs that AI agents can invoke to perform actions or retrieve information. PromptSmith provides a type-safe, schema-driven approach to tool definition that:
  1. Documents tools in the system prompt for the AI model
  2. Validates parameters at runtime using Zod schemas
  3. Infers types automatically for execution functions
  4. Exports to AI SDKs seamlessly (Vercel AI SDK, Mastra)
Design Principle: Tools are registered with metadata only in the builder. The system prompt includes tool documentation, but execution logic runs in your application when the AI requests to use a tool.

Tool Definition Structure

A tool definition has three core components:
import { z } from 'zod';

const weatherTool = {
  name: "get_weather",           // Unique identifier
  description: "...",            // When and how to use it
  schema: z.object({ ... }),     // Parameter structure
  execute: async (args) => { }   // Optional: execution logic
};

Name

The tool’s unique identifier. Use snake_case for consistency:
name: "search_database"     // Good
name: "searchDatabase"      // Works, but inconsistent
name: "search-database"     // Avoid hyphens
Duplicate Names: The builder throws an error if you register multiple tools with the same name. Tool names must be unique.

Description

Explains what the tool does and when to use it. Be specific about:
  • Purpose: What does it return?
  • Use cases: When should the AI choose this tool?
  • Important details: Rate limits, data freshness, required permissions
description: "Retrieves current weather data for a given location. " +
             "Use when the user asks about weather conditions, temperature, " +
             "or forecasts. Returns temperature, conditions, humidity, and wind speed."
Why it’s good: Specific about what it returns and when to use it.

Schema

A Zod schema defining the tool’s input parameters. This serves dual purposes:
  1. Documentation: Introspected to generate parameter docs in the system prompt
  2. Validation: Can validate arguments at runtime
Use .describe() on each field to provide parameter descriptions:
schema: z.object({
  location: z.string().describe("City name or ZIP code"),
  units: z.enum(["celsius", "fahrenheit"])
    .optional()
    .describe("Temperature units (defaults to celsius)")
})
See schemas.ts:180-211 for schema parsing implementation.

Execute (Optional)

An optional function that implements the tool’s logic:
execute: async ({ location, units }) => {
  // TypeScript infers types from schema!
  // location: string
  // units: "celsius" | "fahrenheit" | undefined
  
  const response = await fetch(`https://api.weather.com/${location}?units=${units}`);
  return response.json();
}
Include execute when:
  • Using .toAiSdk() or .toAiSdkTools() for Vercel AI SDK
  • Using .toMastra() for Mastra agents
  • You want a single source of truth for tool metadata + logic
Omit execute when:
  • Handling tool execution separately in your app
  • Tools are documentation-only (teaching the AI when to ask)
  • Client-side execution (browser-based tools)

Registering Tools

Single Tool

builder.withTool({
  name: "get_weather",
  description: "Get current weather for a location",
  schema: z.object({
    location: z.string().describe("City name")
  }),
  execute: async ({ location }) => {
    return await fetchWeather(location);
  }
});
See builder.ts:289-337 for implementation.

Multiple Tools

const tools = [
  { name: "tool1", description: "...", schema: z.object({}) },
  { name: "tool2", description: "...", schema: z.object({}) }
];

builder.withTools(tools);
See builder.ts:360-366 for implementation.

Conditional Tools

const hasDatabase = config.databaseEnabled;
const hasExternalApi = config.apiKey !== null;

builder
  .withToolIf(hasDatabase, {
    name: "query_db",
    description: "Query the database",
    schema: z.object({ query: z.string() })
  })
  .withToolIf(hasExternalApi, {
    name: "fetch_external_data",
    description: "Fetch data from external API",
    schema: z.object({ endpoint: z.string() })
  });
See builder.ts:399-405 for implementation.

Zod Schema Patterns

Basic Types

z.string().describe("User's full name")
z.string().email().describe("Valid email address")
z.string().url().describe("HTTP/HTTPS URL")
z.string().min(3).max(50).describe("Username (3-50 chars)")

Complex Types

// Array of strings
z.array(z.string()).describe("List of tags")

// Array of objects
z.array(z.object({
  id: z.string(),
  name: z.string()
})).describe("List of users")

// Array with min/max length
z.array(z.string())
  .min(1)
  .max(10)
  .describe("1-10 search keywords")

Default Values

z.object({
  limit: z.number()
    .default(10)
    .describe("Maximum results (defaults to 10)"),
  
  sortOrder: z.enum(["asc", "desc"])
    .default("asc")
    .describe("Sort order (defaults to ascending)")
})

Generated Documentation

The builder introspects Zod schemas to generate parameter documentation in the system prompt.

Markdown Format

Given this schema:
schema: z.object({
  query: z.string().describe("Search query text"),
  limit: z.number().optional().describe("Maximum results to return"),
  category: z.enum(["books", "movies", "music"]).describe("Category to search")
})
Generated markdown documentation:
## search_database
Searches the product database for matching items.

**Parameters:**
- `query` (string, required): Search query text
- `limit` (number, optional): Maximum results to return
- `category` (enum, required): Category to search
See builder.ts:1954-1962 for markdown generation.

TOON Format

The same schema in TOON format (more compact):
search_database:
  Searches the product database for matching items.
  Parameters:
    query(string,required): Search query text
    limit(number,optional): Maximum results to return
    category(enum,required): Category to search
See builder.ts:2184-2199 and builder.ts:2424-2451 for TOON generation.

Type Inference

PromptSmith provides full type inference for tool execution functions:
const tool = {
  name: "create_user",
  description: "Create a new user account",
  schema: z.object({
    name: z.string(),
    email: z.string().email(),
    age: z.number().optional(),
    role: z.enum(["user", "admin"])
  }),
  execute: async (args) => {
    // TypeScript automatically infers:
    // args.name: string
    // args.email: string
    // args.age: number | undefined
    // args.role: "user" | "admin"
    
    const user = await db.users.create(args);
    return user;
  }
};
No need for manual type annotations! Zod schemas provide complete type information.

Framework Integration

Vercel AI SDK

Export tools for the Vercel AI SDK:
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

const builder = createPromptBuilder()
  .withIdentity("Weather assistant")
  .withTool({
    name: "get_weather",
    description: "Get current weather",
    schema: z.object({ location: z.string() }),
    execute: async ({ location }) => await fetchWeather(location)
  });

const response = await generateText({
  model: openai('gpt-4'),
  ...builder.toAiSdk(), // { system, tools }
  prompt: "What's the weather in Paris?"
});
See builder.ts:1618-1644 for AI SDK tool export.

Mastra

PromptSmith can consume Mastra tools and export to Mastra format:
import { createTool } from '@mastra/core/tools';

const mastraTool = createTool({
  id: "weather-tool",
  description: "Get weather for a location",
  inputSchema: z.object({ location: z.string() }),
  execute: async ({ context }) => {
    return await fetchWeather(context.location);
  }
});

// Automatically detected and converted!
builder.withTool(mastraTool);
See builder.ts:100-155 for Mastra tool detection and conversion.

Runtime Validation

While PromptSmith doesn’t enforce validation automatically, you can use the schema to validate tool calls:
const tool = {
  name: "create_user",
  schema: z.object({
    name: z.string().min(1),
    email: z.string().email(),
    age: z.number().int().positive().optional()
  }),
  execute: async (args) => {
    // Validate before processing
    const validated = tool.schema.parse(args);
    
    // Or use safeParse for error handling
    const result = tool.schema.safeParse(args);
    if (!result.success) {
      throw new Error(`Invalid arguments: ${result.error.message}`);
    }
    
    return await db.users.create(result.data);
  }
};

Best Practices

Descriptive Names

Use clear, action-oriented names: search_flights, create_booking, send_email

Detailed Descriptions

Explain what the tool does, when to use it, and what it returns

Document Parameters

Always use .describe() on schema fields for clear parameter docs

Provide Examples

Use .withExamples() to show the AI how to use complex tools

Tool Examples

Show the AI how to use tools:
builder
  .withTool({
    name: "book_flight",
    description: "Book a flight",
    schema: z.object({
      flightId: z.string(),
      passengers: z.array(z.object({
        name: z.string(),
        email: z.string().email()
      }))
    })
  })
  .withExamples([{
    user: "Book flight BA123 for John Doe ([email protected])",
    assistant: "I'll book that flight for you. *calls book_flight with flightId: 'BA123' and passenger details*",
    explanation: "Shows how to extract structured data from natural language and invoke the booking tool"
  }]);
See builder.ts:750-814 for examples implementation.

Common Patterns

{
  name: "search_products",
  description: "Search for products in the catalog. Use when user wants to find items.",
  schema: z.object({
    query: z.string().describe("Search keywords"),
    category: z.string().optional().describe("Filter by category"),
    minPrice: z.number().optional().describe("Minimum price filter"),
    maxPrice: z.number().optional().describe("Maximum price filter"),
    limit: z.number().default(10).describe("Max results (default 10)")
  })
}
const userSchema = z.object({
  id: z.string().optional(),
  name: z.string(),
  email: z.string().email()
});

builder
  .withTool({
    name: "create_user",
    schema: userSchema.omit({ id: true }),
    execute: async (data) => await db.users.create(data)
  })
  .withTool({
    name: "update_user",
    schema: userSchema.required({ id: true }),
    execute: async (data) => await db.users.update(data.id, data)
  })
  .withTool({
    name: "delete_user",
    schema: z.object({ id: z.string() }),
    execute: async ({ id }) => await db.users.delete(id)
  });
builder
  .withTool({
    name: "search_flights",
    description: "Search for available flights. Always use this before booking."
  })
  .withTool({
    name: "check_availability",
    description: "Check seat availability for a specific flight. Use after searching."
  })
  .withTool({
    name: "book_flight",
    description: "Book a flight. Only use after confirming availability and getting user confirmation."
  })
  .withConstraint("must", "Always search flights before booking")
  .withConstraint("must", "Always confirm with user before finalizing bookings");

Troubleshooting

Cause: Tool might not be registered, or the build format might not include it.Solution:
// Check if tool was registered
console.log(builder.getTools());

// Verify in built prompt
console.log(builder.build());
Cause: Using plain objects instead of inline schema definition.Solution:
// ❌ No inference
const schema = z.object({ ... });
const tool = { schema, execute: async (args) => { } };

// ✅ Full inference
const tool = {
  schema: z.object({ ... }),
  execute: async (args) => { } // args are typed!
};
Cause: Registering multiple tools with the same name.Solution:
// Use unique names
.withTool({ name: "search_users", ... })
.withTool({ name: "search_products", ... }) // Different name

// Or check before adding
if (!builder.getTools().some(t => t.name === 'search')) {
  builder.withTool({ name: "search", ... });
}

Builder

Learn about the SystemPromptBuilder API

Constraints

Add behavioral rules for tool usage

Examples

See real-world tool integration examples

Build docs developers (and LLMs) love