Tool
The fundamental building block for creating interactive applications in Tool Forge.
What is a Tool?
A Tool is the fundamental building block in Tool Forge. It's a TypeScript function that defines what inputs to collect from users, what logic to execute, and what output to display. Tool Forge automatically generates the UI for your tools—you just write the backend logic.
Think of a tool as a backend-defined workflow that:
- Collects structured inputs from users
- Executes your business logic
- Returns results to the dashboard
import { defineTool } from '@toolforge-js/sdk/components'
import * as z from 'zod'
export default defineTool({
name: 'Hello World',
description: 'A simple greeting tool',
handler: async ({ io }) => {
const name = await io.textInput({
label: 'Enter your name',
validationSchema: z.string().min(1),
})
return `Hello, ${name}!`
},
})Tool Structure
Every tool is defined using the defineTool() function, which accepts an options object with the following properties:
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The display name of the tool in the dashboard |
description | string | No | A brief description of what the tool does |
handler | function | Yes | The async function containing your tool's logic |
The Handler Function
The handler is where your tool's logic lives. It receives an object with four powerful properties:
handler: async ({ io, block, signal, metadata }) => {
// Your tool logic here
}| Property | Description |
|---|---|
io | IO methods for collecting user input and displaying feedback |
block | Block methods for creating rich output components (charts, tables, KPIs) |
signal | An AbortSignal for handling user cancellation |
metadata | Information about the current tool session |
IO Methods
The io object is your primary interface for interacting with users. It provides methods to collect various types of input:
Input Methods
textInput
Collect text from users with optional validation, secure input, and multi-line support.
numberInput
Collect numeric values with min/max constraints.
selectInput
Dropdowns, radio buttons, checkboxes, and radio cards for selection.
searchInput
Async search with custom handlers for dynamic options.
dateInput
Date and datetime pickers with single, multiple, or range selection.
tableInput
Interactive tables for selecting rows of data.
formInput
Multi-field forms with Zod validation schemas.
Interaction Methods
confirm
Confirmation dialogs for critical actions requiring user approval.
message
Display informational, success, warning, or error messages.
Progress Methods
loader
Show loading states during async operations.
progressLoader
Track progress of batch operations with item counts.
Block Outputs
The block object lets you create rich, visual output components. Instead of returning plain text or JSON, you can return beautifully rendered charts, tables, and layouts.
Charts
| Method | Description |
|---|---|
block.areaChart() | Area chart for visualizing trends over time |
block.barChart() | Bar chart for comparing categorical data |
block.lineChart() | Line chart for continuous data visualization |
block.donutChart() | Donut chart for proportional data |
block.comboChart() | Combination of bar and line charts |
block.heatmapChart() | Heatmap for matrix-style data visualization |
block.sparkChart() | Small, inline chart for trends |
Progress & Metrics
| Method | Description |
|---|---|
block.progressBar() | Visual progress bar |
block.progressCircle() | Circular progress indicator |
block.kpiCard() | Key Performance Indicator card with value and optional trend |
block.tracker() | Status tracker for multiple items |
block.categoryBar() | Horizontal bar for categorical comparison |
block.barList() | Vertical list of horizontal bars |
Data Display
| Method | Description |
|---|---|
block.table() | Structured tabular data display |
block.text() | Formatted text with markdown support |
block.image() | Image display with caption |
block.object() | Key-value pair display for structured data |
Layout
| Method | Description |
|---|---|
block.layout() | Grid layout for arranging multiple blocks |
Example: Rich Output
import { defineTool } from '@toolforge-js/sdk/components'
export default defineTool({
name: 'Sales Dashboard',
description: 'View sales metrics and trends',
handler: async ({ io, block }) => {
const region = await io.selectInput({
label: 'Select Region',
options: [
{ label: 'North America', value: 'NA' },
{ label: 'Europe', value: 'EU' },
{ label: 'Asia Pacific', value: 'APAC' },
],
})
// Fetch your data based on selection
const salesData = await fetchSalesData(region)
return block.layout({
children: [
{
element: block.kpiCard({
name: 'Total Revenue',
value: salesData.totalRevenue,
valueFormat: { type: 'currency', currency: 'USD' },
}),
colSpan: 4,
},
{
element: block.kpiCard({
name: 'Orders',
value: salesData.orderCount,
}),
colSpan: 4,
},
{
element: block.kpiCard({
name: 'Avg Order Value',
value: salesData.avgOrderValue,
valueFormat: { type: 'currency', currency: 'USD' },
}),
colSpan: 4,
},
{
element: block.areaChart({
title: 'Revenue Trend',
data: salesData.monthlyTrend,
index: 'month',
categories: ['revenue'],
}),
colSpan: 12,
},
],
})
},
})Handling Cancellation
Users can stop a running tool at any time. The signal property is an AbortSignal that lets you handle this gracefully:
handler: async ({ io, signal }) => {
const items = await fetchLargeDataset()
for (const item of items) {
// Check if user cancelled
if (signal.aborted) {
return 'Operation cancelled by user'
}
await processItem(item)
}
return 'All items processed!'
}Always check signal.aborted in long-running loops to ensure your tool
responds promptly to user cancellation.
Tool Metadata
The metadata object provides context about the current tool execution:
| Property | Description |
|---|---|
sessionId | Unique identifier for this tool execution (useful as idempotency key) |
runnerId | Identifier of the runner executing the tool |
toolName | The name of the tool |
toolDescription | The description of the tool |
toolId | Unique identifier for the tool definition |
handler: async ({ io, metadata }) => {
console.log(`Session: ${metadata.sessionId}`)
console.log(`Running on: ${metadata.runnerId}`)
// Use sessionId as idempotency key for external API calls
await externalApi.processPayment({
idempotencyKey: metadata.sessionId,
// ...
})
}Tool Organization
Tools are organized in your project's tools/ directory. Each file should export a default tool definition:
tools/
├── hello-world.ts
├── orders/
│ ├── create-order.ts
│ ├── refund-order.ts
│ └── list-orders.ts
└── users/
├── create-user.ts
└── update-user.tsTool Forge automatically discovers all tools in subdirectories, making it easy to organize by domain or feature.
Files and folders starting with - or _ are ignored by Tool Forge. Use this
for utility files like -lib/helpers.ts.
Best Practices
1. Use Descriptive Names
// ✅ Good
defineTool({
name: 'Process Customer Refund',
description: 'Review and process refund requests for customer orders',
// ...
})
// ❌ Avoid
defineTool({
name: 'Refund',
// Missing description
// ...
})2. Validate Inputs Early
Use Zod schemas with IO methods to catch invalid input before processing:
const email = await io.textInput({
label: 'Customer Email',
validationSchema: z.email('Please enter a valid email'),
})3. Use Confirmations for Destructive Actions
const shouldProceed = await io.confirm({
title: 'Delete User Account',
description: 'This action cannot be undone. Are you sure?',
})
if (!shouldProceed) {
return 'Operation cancelled'
}4. Show Progress for Long Operations
const progress = await io
.progressLoader({
title: 'Processing records',
itemsInQueue: records.length,
})
.start()
for (const record of records) {
await processRecord(record)
await progress.increment()
}
await progress.stop()5. Handle Errors Gracefully
try {
const result = await riskyOperation()
return result
} catch (error) {
await io.message({
title: 'Operation Failed',
description: error.message,
type: 'error',
})
throw error // Re-throw to mark the session as failed
}