Skip to main content

Overview

LuaTool is the interface that all tools must implement. A tool is a single function that the AI can call to accomplish a specific task.
import { LuaTool } from 'lua-cli';
import { z } from 'zod';

export default class MyTool implements LuaTool {
  name = "my_tool";
  description = "What the tool does";
  inputSchema = z.object({ param: z.string() });
  
  async execute(input: any) {
    return { result: "success" };
  }
}

Interface Definition

interface LuaTool<TInput extends ZodType = ZodType> {
  name: string;
  description: string;
  inputSchema: TInput;
  execute: (input: z.infer<TInput>) => Promise<any>;
  condition?: () => Promise<boolean>;
}

Required Properties

name

Unique identifier for the tool.
name
string
required
Tool name using only: a-z, A-Z, 0-9, -, _Examples: "get_weather", "create-order", "sendEmail123"Invalid: "get weather", "tool.name", "send@email"
// ✅ Good
name = "get_weather";
name = "create-product";
name = "search_items";

// ❌ Bad
name = "get weather";  // No spaces
name = "tool.name";    // No dots
name = "send@email";   // No special chars

description

Clear, concise description of what the tool does.
description
string
required
One sentence describing the tool’s purposeHelps the AI understand when to use this tool
// ✅ Good
description = "Get current weather conditions for any city worldwide";
description = "Create a new product in the catalog with price and details";
description = "Search for products by name, category, or description";

// ❌ Bad
description = "Gets data";           // Too vague
description = "Does weather stuff";  // Unclear

inputSchema

Zod schema that validates and types the input.
inputSchema
ZodType
required
Zod schema defining valid inputsProvides runtime validation and TypeScript types
import { z } from 'zod';

// Simple
inputSchema = z.object({
  city: z.string()
});

// With validation
inputSchema = z.object({
  email: z.string().email(),
  age: z.number().min(0).max(120)
});

// With descriptions
inputSchema = z.object({
  city: z.string().describe("City name (e.g., 'London', 'Tokyo')"),
  units: z.enum(['metric', 'imperial']).describe("Temperature units")
});

// With optional and default values
inputSchema = z.object({
  query: z.string(),
  limit: z.number().default(10),
  offset: z.number().optional()
});

execute

Async function that implements the tool’s logic.
execute
function
required
async execute(input: z.infer<typeof this.inputSchema>): Promise<any>
  • Input is automatically validated
  • Must return a JSON-serializable value
  • Can throw errors for failures

Optional Properties

condition

Async function that determines if the tool should be available to the AI.
condition
function
async condition(): Promise<boolean>
  • Runs before the tool is offered to the AI
  • Return true to enable the tool, false to hide it
  • If the function throws an error, the tool is disabled (fail-closed)
  • Has access to all Platform APIs (User, Data, Products, etc.)
Use conditions to dynamically enable/disable tools based on:
  • User subscription status (premium features)
  • User verification or account status
  • Feature flags or A/B testing
  • Region-specific functionality
  • Time-based access
import { LuaTool, User } from 'lua-cli';
import { z } from 'zod';

export default class PremiumSearchTool implements LuaTool {
  name = "premium_search";
  description = "Advanced search with filters - premium users only";
  
  inputSchema = z.object({
    query: z.string(),
    filters: z.object({
      minPrice: z.number().optional(),
      maxPrice: z.number().optional()
    }).optional()
  });

  // Only show this tool to premium users
  condition = async () => {
    const user = await User.get();
    return user.data?.isPremium === true;
  };

  async execute(input: z.infer<typeof this.inputSchema>) {
    // This only runs if condition returned true
    return { results: [] };
  }
}
Fail-closed behavior: If your condition function throws an error or times out (30s), the tool is automatically disabled. This ensures tools aren’t accidentally exposed when conditions can’t be evaluated.

Implementation Examples

Simple Tool

import { LuaTool } from 'lua-cli';
import { z } from 'zod';

export default class GreetTool implements LuaTool {
  name = "greet_user";
  description = "Greet a user by name";
  
  inputSchema = z.object({
    name: z.string()
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    return {
      message: `Hello, ${input.name}!`,
      timestamp: new Date().toISOString()
    };
  }
}

External API Tool

import { LuaTool } from 'lua-cli';
import { z } from 'zod';

export default class GetWeatherTool implements LuaTool {
  name = "get_weather";
  description = "Get current weather for a city";
  
  inputSchema = z.object({
    city: z.string().describe("City name"),
    units: z.enum(['metric', 'imperial']).optional().default('metric')
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const { city, units } = input;
    
    // Call external API
    const response = await fetch(
      `https://api.weather.com/v1/weather?city=${city}&units=${units}`
    );
    
    if (!response.ok) {
      throw new Error(`Weather API error: ${response.statusText}`);
    }
    
    const data = await response.json();
    
    return {
      city: data.location,
      temperature: data.temp,
      condition: data.condition,
      humidity: data.humidity
    };
  }
}

Platform API Tool

import { LuaTool, Products } from 'lua-cli';
import { z } from 'zod';

export default class SearchProductsTool implements LuaTool {
  name = "search_products";
  description = "Search for products by name or description";
  
  inputSchema = z.object({
    query: z.string().describe("Search query"),
    limit: z.number().min(1).max(100).default(10)
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const results = await Products.search(input.query);
    
    // Take only requested number of results
    const products = results.data.slice(0, input.limit);
    
    return {
      products: products.map(p => ({
        id: p.id,
        name: p.name,
        price: `$${p.price.toFixed(2)}`,
        inStock: p.inStock
      })),
      total: results.data.length,
      showing: products.length
    };
  }
}

Environment Variables Tool

import { LuaTool, env } from 'lua-cli';
import { z } from 'zod';

export default class SendEmailTool implements LuaTool {
  name = "send_email";
  description = "Send an email via SendGrid";
  
  inputSchema = z.object({
    to: z.string().email(),
    subject: z.string(),
    body: z.string()
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    // Get API key from environment
    const apiKey = env('SENDGRID_API_KEY');
    
    if (!apiKey) {
      throw new Error('SENDGRID_API_KEY not configured');
    }
    
    // Send email
    const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        personalizations: [{ to: [{ email: input.to }] }],
        from: { email: '[email protected]' },
        subject: input.subject,
        content: [{ type: 'text/plain', value: input.body }]
      })
    });
    
    if (!response.ok) {
      throw new Error(`Email failed: ${response.statusText}`);
    }
    
    return {
      success: true,
      message: `Email sent to ${input.to}`
    };
  }
}

Multi-Step Tool

import { LuaTool, Products, Baskets } from 'lua-cli';
import { z } from 'zod';

export default class QuickCheckoutTool implements LuaTool {
  name = "quick_checkout";
  description = "Search, add to cart, and checkout in one step";
  
  inputSchema = z.object({
    productName: z.string(),
    quantity: z.number().min(1).default(1),
    shippingAddress: z.object({
      street: z.string(),
      city: z.string(),
      zip: z.string()
    })
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    // Step 1: Search for product
    const products = await Products.search(input.productName);
    if (products.data.length === 0) {
      throw new Error(`Product not found: ${input.productName}`);
    }
    const product = products.data[0];
    
    // Step 2: Create basket
    const basket = await Baskets.create({ currency: 'USD' });
    
    // Step 3: Add product
    await Baskets.addItem(basket.id, {
      id: product.id,
      price: product.price,
      quantity: input.quantity
    });
    
    // Step 4: Checkout
    const order = await Baskets.placeOrder({
      shippingAddress: input.shippingAddress,
      paymentMethod: 'stripe'
    }, basket.id);
    
    return {
      orderId: order.id,
      product: product.name,
      quantity: input.quantity,
      total: `$${(product.price * input.quantity).toFixed(2)}`,
      message: 'Order created successfully'
    };
  }
}

Conditional Tool (Premium Feature)

import { LuaTool, User, Products } from 'lua-cli';
import { z } from 'zod';

export default class PremiumAdvancedSearchTool implements LuaTool {
  name = "premium_advanced_search";
  description = "Advanced search with filters and sorting - premium users only";
  
  inputSchema = z.object({
    query: z.string().describe("Search query"),
    filters: z.object({
      category: z.string().optional(),
      minPrice: z.number().optional(),
      maxPrice: z.number().optional()
    }).optional(),
    sortBy: z.enum(["relevance", "price_asc", "price_desc", "newest"]).optional()
  });

  // Condition: Only show to premium users
  condition = async () => {
    const user = await User.get();
    
    // Check subscription status
    const isPremium = user.data?.subscription === "premium" 
                   || user.data?.isPremium === true;
    
    return isPremium;
  };

  async execute(input: z.infer<typeof this.inputSchema>) {
    const { query, filters, sortBy } = input;
    
    const searchResult = await Products.search({ query, limit: 20 });
    let products = searchResult.products;
    
    // Apply price filters
    if (filters?.minPrice !== undefined || filters?.maxPrice !== undefined) {
      products = products.filter(p => {
        if (filters.minPrice && p.price < filters.minPrice) return false;
        if (filters.maxPrice && p.price > filters.maxPrice) return false;
        return true;
      });
    }
    
    return {
      query,
      totalResults: products.length,
      results: products.slice(0, 10),
      sortedBy: sortBy || "relevance"
    };
  }
}

Input Schema Patterns

Optional Fields

inputSchema = z.object({
  required: z.string(),
  optional: z.string().optional(),
  withDefault: z.string().default('default value')
});

Validation

inputSchema = z.object({
  email: z.string().email(),
  age: z.number().min(18).max(120),
  phone: z.string().regex(/^\+?[1-9]\d{1,14}$/),
  url: z.string().url(),
  password: z.string().min(8)
});

Nested Objects

inputSchema = z.object({
  user: z.object({
    name: z.string(),
    email: z.string().email()
  }),
  preferences: z.object({
    notifications: z.boolean(),
    language: z.string()
  }).optional()
});

Arrays

inputSchema = z.object({
  items: z.array(z.object({
    id: z.string(),
    quantity: z.number()
  })),
  tags: z.array(z.string()).optional()
});

Enums

inputSchema = z.object({
  status: z.enum(['pending', 'active', 'completed']),
  priority: z.enum(['low', 'medium', 'high']).default('medium')
});

Error Handling

Throwing Errors

async execute(input: any) {
  // Validate business logic
  if (input.amount <= 0) {
    throw new Error("Amount must be positive");
  }
  
  // API errors
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`API error: ${response.statusText}`);
  }
  
  // Not found errors
  const item = await findItem(input.id);
  if (!item) {
    throw new Error(`Item not found: ${input.id}`);
  }
  
  return result;
}

Try-Catch Pattern

async execute(input: any) {
  try {
    const result = await externalService.call(input);
    
    if (!result.success) {
      throw new Error(result.error || 'Operation failed');
    }
    
    return result.data;
  } catch (error) {
    // Add context to errors
    throw new Error(`Failed to process request: ${error.message}`);
  }
}

Return Value Patterns

Structured Data

// ✅ Good - Structured
return {
  success: true,
  data: { id, name, price },
  metadata: { timestamp, version }
};

// ❌ Bad - Unstructured string
return "Product created with ID 123";

Lists

return {
  items: [...],
  total: 100,
  page: 1,
  hasMore: true
};

Status Updates

return {
  status: 'completed',
  message: 'Order shipped successfully',
  trackingNumber: 'ABC123',
  estimatedDelivery: '2025-10-10'
};

Best Practices

async execute(input: z.infer<typeof this.inputSchema>) {
  // input is fully typed!
  const { city, units } = input;
}
inputSchema = z.object({
  city: z.string().describe("City name (e.g., 'London', 'Tokyo')"),
  units: z.enum(['metric', 'imperial']).describe("Temperature units")
});
Always return objects, not strings:
// ✅ Good
return { temperature: 72, condition: "sunny" };

// ❌ Bad
return "The temperature is 72 and sunny";
Provide helpful error messages:
if (!apiKey) {
  throw new Error('API_KEY environment variable is required. Set it in .env file.');
}
One tool = one responsibility:
// ✅ Good - Single purpose
class CreateProductTool { ... }
class UpdateProductTool { ... }

// ❌ Bad - Multiple purposes
class ProductTool {
  // Does create, update, delete, search...
}

Next Steps