Skip to Content
DocumentationGuidesCreating Tools

Creating Tools

A tool is a reusable unit of logic that extends BaseTool. Tools are injected into workflows and called directly in TypeScript.

Basic Tool

import { z } from 'zod'; import { BaseTool, Tool, ToolResult } from '@loopstack/common'; @Tool({ uiConfig: { description: 'Short description of what this tool does.', }, schema: z .object({ query: z.string().describe('Search query'), limit: z.number().default(10).describe('Max results'), }) .strict(), }) export class SearchTool extends BaseTool { async call(args: { query: string; limit: number }): Promise<ToolResult<string>> { return { data: `Found results for: ${args.query}` }; } }

The @Tool Decorator

@Tool({ uiConfig: { description: 'User-facing description.', // Also seen by LLMs for function calling }, schema: InputSchema, // Zod schema for input validation })
  • uiConfig — Object or path to YAML file with tool metadata
  • schema — Zod schema that validates arguments before call() is invoked

The call() Method

The only method you need to implement. It receives validated arguments and returns a ToolResult.

async call(args: TArgs): Promise<ToolResult<TData>> { // Your logic here return { data: result }; }

ToolResult

type ToolResult<TData = any> = { type?: 'text' | 'image' | 'file'; data?: TData; error?: string; metadata?: Record<string, unknown>; };

Return patterns:

return { data: 42 }; // Simple value return { data: { name: 'result', items: [...] } }; // Typed data return { error: 'Something went wrong' }; // Error return { type: 'text', data: 'Mostly sunny, 14C.' }; // Typed output return { data: result, metadata: { tokensUsed: 150 } }; // With metadata

Dependency Injection

Use standard NestJS @Inject() to inject services:

import { Inject } from '@nestjs/common'; @Tool({ uiConfig: { description: 'Calculates the sum of two numbers.' }, schema: z.object({ a: z.number(), b: z.number() }).strict(), }) export class MathSumTool extends BaseTool { @Inject() private mathService: MathService; async call(args: { a: number; b: number }): Promise<ToolResult<number>> { return { data: this.mathService.sum(args.a, args.b) }; } }

Tool State

Tool properties are automatically persisted across invocations within the same workflow run:

@Tool({ uiConfig: { description: 'Counter tool.' } }) export class CounterTool extends BaseTool { count: number = 0; async call(): Promise<ToolResult<number>> { this.count++; return { data: this.count }; } }

The count value persists even if the workflow pauses and resumes.

Tools for LLM Function Calling

When a tool is exposed to the LLM, the description and schema tell the LLM what the tool does and what arguments it accepts:

@Tool({ uiConfig: { description: 'Retrieve weather information for a location.', }, schema: z.object({ location: z.string().describe('City or location name'), }), }) export class GetWeather extends BaseTool { async call(args: { location: string }): Promise<ToolResult> { return { type: 'text', data: 'Mostly sunny, 14C.' }; } }

In the workflow, list the tool name in the tools array:

const result = await this.claudeGenerateText.call({ claude: { model: 'claude-sonnet-4-6' }, messagesSearchTag: 'message', tools: ['getWeather'], // Matches the @InjectTool() property name });

Using Tools in Workflows

@InjectTool() private myTool: SearchTool; @Transition({ from: 'ready', to: 'done' }) async process() { const result = await this.myTool.call({ query: 'hello', limit: 5 }); this.searchResults = result.data; }

Module Registration

@Module({ providers: [SearchTool, MathService], exports: [SearchTool], }) export class MyToolModule {}

Then import the module in the workflow’s parent module.

File Structure

src/ ├── tools/ │ ├── search.tool.ts │ ├── math-sum.tool.ts │ └── index.ts # Re-exports all tools ├── services/ │ └── math.service.ts ├── my-feature.module.ts └── index.ts

Registry References

Last updated on