Tools are callable capabilities that extend what AI agents can do beyond generating text. They allow agents to:
Execute code : Run custom business logic
Access data : Query databases and APIs
Perform actions : Create records, send emails, trigger workflows
Call other AI : Invoke sub-prompts or hand off to specialized agents
In AgentBuilder, “tools” includes three types: function tools (custom code), prompt tools (sub-prompts), and agent tools (handoffs). All appear to the LLM as functions it can call.
Quick Start
Create your first tool in agents/tools/search_database.ts:
import { defineTool } from '@standardagents/spec' ;
import { z } from 'zod' ;
export default defineTool ({
description: 'Search the customer database for matching records' ,
args: z . object ({
query: z . string (). describe ( 'Search query' ),
limit: z . number (). optional (). default ( 10 ). describe ( 'Maximum results' ),
}) ,
execute : async ( state , args ) => {
const results = await searchDatabase ( args . query , args . limit );
return {
status: 'success' ,
result: JSON . stringify ( results ),
};
} ,
}) ;
That’s it! Your tool is now available to any prompt that includes it in its tools array. The framework auto-discovers it from the agents/tools/ directory.
AgentBuilder supports three types of tools that all appear the same to LLMs:
Function Tools Custom TypeScript functions Defined in: agents/tools/Best for:
Database queries
API calls
File operations
Business logic
Prompt Tools Other prompts called as sub-prompts Defined in: agents/prompts/ with exposeAsTool: trueBest for:
Specialized analysis
Content generation
Data summarization
Validation tasks
Agent Tools Full agents for handoffs Defined in: agents/agents/ with exposeAsTool: trueBest for:
Specialist routing
Complex workflows
Domain expertise
Multi-turn tasks
How They Work Together
// Function tool: agents/tools/lookup_customer.ts
export default defineTool ({
description: 'Look up customer by ID' ,
args: z . object ({ customerId: z . string () }) ,
execute : async ( state , args ) => {
const customer = await db . getCustomer ( args . customerId );
return { status: 'success' , result: JSON . stringify ( customer ) };
} ,
}) ;
// Prompt tool: agents/prompts/summarizer.ts
export default definePrompt ({
name: 'summarizer' ,
exposeAsTool: true ,
toolDescription: 'Summarize text into key points' ,
prompt: 'You are an expert summarizer...' ,
model: 'gpt-4o' ,
requiredSchema: z . object ({
text: z . string (). describe ( 'Text to summarize' ),
}) ,
}) ;
// Agent tool: agents/agents/billing_specialist.ts
export default defineAgent ({
name: 'billing_specialist' ,
exposeAsTool: true ,
toolDescription: 'Hand off billing issues to specialist' ,
sideA: { prompt: 'billing_expert' } ,
}) ;
// Use all three in a prompt
export default definePrompt ({
name: 'support_router' ,
tools: [
'lookup_customer' , // Function tool
'summarizer' , // Prompt tool
'billing_specialist' , // Agent tool
] ,
// ...
}) ;
ThreadState Context
Every tool receives a ThreadState object providing execution context:
defineTool ({
description: 'My tool' ,
args: z . object ({ input: z . string () }),
execute : async ( state , args ) => {
// Access execution context
console . log ( 'Thread ID:' , state . threadId );
console . log ( 'Turn:' , state . turnCount );
console . log ( 'Agent:' , state . agentConfig . name );
// Access storage
const result = await state . storage . sql . exec ( 'SELECT * FROM data WHERE id = ?' , args . input );
// Access environment
const apiKey = state . env . MY_API_KEY ;
return { status: 'success' , result: 'Done' };
},
});
ThreadState gives tools access to thread storage, message operations, file system, and execution state. See the ThreadState documentation for details.
Tools use Zod schemas to validate arguments:
const argsSchema = z . object ({
// Required string
query: z . string (). describe ( 'Search query' ),
// Optional with default
limit: z . number (). optional (). default ( 10 ). describe ( 'Max results' ),
// Enum
format: z . enum ([ 'json' , 'text' , 'markdown' ]). describe ( 'Output format' ),
// Nested object
filters: z . object ({
category: z . string (). optional (),
minPrice: z . number (). optional (),
}). optional (). describe ( 'Filter options' ),
// Array
tags: z . array ( z . string ()). optional (). describe ( 'Filter by tags' ),
});
Always use .describe() on parameters! These descriptions help LLMs understand how to use your tools correctly.
Tools return a ToolResult object:
interface ToolResult {
status : 'success' | 'error' ;
result ?: string ;
error ?: string ;
stack ?: string ;
attachments ?: Array < ToolAttachment | AttachmentRef >;
}
interface ToolAttachment {
name : string ; // Filename (e.g., "generated.png")
mimeType : string ; // MIME type
data : string ; // Base64-encoded file data
width ?: number ; // Image width in pixels
height ?: number ; // Image height in pixels
}
Common Patterns
Text Result
return {
status: 'success' ,
result: 'Operation completed successfully' ,
};
JSON Data
return {
status: 'success' ,
result: JSON . stringify ({ id: '123' , name: 'John' }),
};
Error Handling
return {
status: 'error' ,
error: 'Failed to find user: User not found' ,
};
Thread Environment Variables (tenvs)
Tools can declare required thread environment variables using the tenvs option:
export default defineTool ({
description: 'Search through uploaded files using vector embeddings' ,
args: z . object ({
query: z . string (). describe ( 'Search query' ),
}) ,
execute : async ( state , args ) => {
// Access tenvs from state
const vectorStoreId = state . tenvs . vectorStoreId ;
const results = await searchVectorStore ( vectorStoreId , args . query );
return { status: 'success' , result: JSON . stringify ( results ) };
} ,
tenvs: z . object ({
vectorStoreId: z . string (). describe ( 'OpenAI Vector Store ID' ),
}) ,
}) ;
Tenvs are validated at runtime. If required tenvs are missing, the tool execution fails with a clear error message.
Some tools are executed by the LLM provider rather than locally. Use executionMode and executionProvider to configure this:
export default defineTool ({
description: 'Generate images using DALL-E' ,
args: z . object ({
prompt: z . string (). describe ( 'Image description' ),
}) ,
execute : async () => {
// This won't be called - the provider handles execution
return { status: 'success' , result: 'Handled by provider' };
} ,
executionMode: 'provider' ,
executionProvider: 'openai' ,
}) ;
Provider-executed tools are typically defined by provider packages like @standardagents/openai. You can enable them using the providerTools option in your model definition.
Common Use Cases
import { defineTool } from '@standardagents/spec' ;
import { z } from 'zod' ;
export default defineTool ({
description: 'Look up customer information by ID or email' ,
args: z . object ({
customerId: z . string (). optional (). describe ( 'Customer ID' ),
email: z . string (). email (). optional (). describe ( 'Customer email' ),
}) ,
execute : async ( state , args ) => {
const db = state . env . DB ;
let customer ;
if ( args . customerId ) {
customer = await db . prepare ( 'SELECT * FROM customers WHERE id = ?' )
. bind ( args . customerId ). first ();
} else {
customer = await db . prepare ( 'SELECT * FROM customers WHERE email = ?' )
. bind ( args . email ). first ();
}
if ( ! customer ) {
return {
status: 'error' ,
error: 'Customer not found' ,
};
}
return {
status: 'success' ,
result: JSON . stringify ( customer ),
};
} ,
}) ;
import { defineTool } from '@standardagents/spec' ;
import { z } from 'zod' ;
export default defineTool ({
description: 'Fetch current weather for a city' ,
args: z . object ({
city: z . string (). describe ( 'City name' ),
units: z . enum ([ 'metric' , 'imperial' ]). default ( 'metric' ),
}) ,
execute : async ( state , args ) => {
const apiKey = state . env . WEATHER_API_KEY ;
try {
const response = await fetch (
`https://api.weather.com/v1/current?city= ${ args . city } &units= ${ args . units } &key= ${ apiKey } `
);
if ( ! response . ok ) {
return {
status: 'error' ,
error: `Weather API error: ${ response . status } ` ,
};
}
const data = await response . json ();
return {
status: 'success' ,
result: `Weather in ${ args . city } : ${ data . temperature } °, ${ data . conditions } ` ,
};
} catch ( error ) {
return {
status: 'error' ,
error: `Failed to fetch weather: ${ error . message } ` ,
};
}
} ,
}) ;
Use queueTool to chain multiple tools:
import { defineTool } from '@standardagents/spec' ;
import { queueTool } from '@standardagents/builder' ;
import { z } from 'zod' ;
export default defineTool ({
description: 'Process order and queue follow-up tasks' ,
args: z . object ({
orderId: z . string (). describe ( 'Order ID' ),
}) ,
execute : async ( state , args ) => {
const order = await validateOrder ( args . orderId );
if ( ! order . valid ) {
return {
status: 'error' ,
error: 'Order validation failed' ,
};
}
// Queue additional tools to execute after this one
queueTool ( state . rootState , 'charge_payment' , {
orderId: args . orderId ,
amount: order . total ,
});
queueTool ( state . rootState , 'send_confirmation_email' , {
userId: order . userId ,
orderId: args . orderId ,
});
return {
status: 'success' ,
result: `Order ${ args . orderId } queued for processing` ,
};
} ,
}) ;
Returning File Attachments
export default defineTool ({
description: 'Generate an image based on a text prompt' ,
args: z . object ({
prompt: z . string (). describe ( 'Text description of the image' ),
}) ,
execute : async ( state , args ) => {
const image = await generateImage ( args . prompt );
return {
status: 'success' ,
result: `Generated image for: " ${ args . prompt } "` ,
attachments: [{
name: `generated- ${ Date . now () } .png` ,
mimeType: 'image/png' ,
data: image . base64 ,
width: 1024 ,
height: 1024 ,
}],
};
} ,
}) ;
ThreadState Utilities
AgentBuilder provides utility functions for tools:
Add a message to conversation without triggering execution await injectMessage ( state , {
role: 'system' ,
content: 'Important context update' ,
});
Retrieve message history const messages = await getMessages ( state , 20 );
Send custom events to frontend emitThreadEvent ( state , 'progress' , {
step: 3 ,
total: 10 ,
});
See the ThreadState documentation for complete details.
Best Practices
Tool descriptions help LLMs understand when to use each tool: Good: defineTool ({
description: 'Search the knowledge base for product documentation, FAQs, and troubleshooting guides. Returns relevant articles sorted by relevance.' ,
// ...
});
Avoid: defineTool ({
description: 'Search stuff' ,
// ...
});
Use .describe() on every parameter: args : z . object ({
query: z . string (). describe ( 'Search query - supports partial matches' ),
category: z . string (). optional (). describe ( 'Product category filter' ),
minPrice: z . number (). optional (). describe ( 'Minimum price in USD' ),
})
Return errors as tool results, don’t throw: try {
const result = await riskyOperation ( args );
return {
status: 'success' ,
result: JSON . stringify ( result ),
};
} catch ( error ) {
return {
status: 'error' ,
error: `Operation failed: ${ error . message } ` ,
};
}
File names should use snake_case: Good: search_database.ts, create_ticket.tsAvoid: SearchDatabase.ts, createTicket.ts
Use rootState for Queueing
When queueing tools from sub-prompts, use state.rootState: // Correct
queueTool ( state . rootState , 'next_tool' , args );
// Incorrect (may not work in sub-prompts)
queueTool ( state , 'next_tool' , args );
Next Steps