Building Tools
Build tools in TypeScript with full type safety
Tools are typed functions that agents, workflows, and views can call. This guide shows you how to build tools using TypeScript for maximum control and extensibility.
Not a developer? See Creating Tools to create tools via chat.
Anatomy of a Tool
Every tool requires four components:
- ID - Unique identifier following
RESOURCE_ACTIONpattern (e.g.,EMAIL_SEND,CUSTOMER_FETCH) - Description - Tells agents when and how to use the tool
- Schemas - Zod schemas for input validation and output typing
- Execute - The implementation logic
Basic Example
import { createTool } from "@deco/workers-runtime";
import { z } from "zod";
const createEmailTool = (env: Env) =>
createTool({
id: "EMAIL_SEND",
description: "Send an email via Gmail. Use for notifications and communications.",
inputSchema: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
}),
outputSchema: z.object({
success: z.boolean(),
messageId: z.string().optional(),
}),
execute: async ({ context }) => {
const response = await env["gmail"].SEND_EMAIL({
to: context.to,
subject: context.subject,
body: context.body,
});
return {
success: true,
messageId: response.id,
};
},
});
Key points:
- Use
contextto access validated input - Return data matching your
outputSchema - Access integrations via
env["integration-id"]whereintegration-idis the appβs ID from your workspace
Common Patterns
External API Calls
const createWeatherTool = (env: Env) =>
createTool({
id: "WEATHER_FETCH",
description: "Get current weather for a city using OpenWeather API",
inputSchema: z.object({
city: z.string().min(1),
}),
outputSchema: z.object({
temperature: z.number(),
condition: z.string(),
humidity: z.number().optional(),
}),
execute: async ({ context }) => {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${context.city}&appid=${env.OPENWEATHER_API_KEY}`
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.statusText}`);
}
const data = await response.json();
return {
temperature: data.main.temp,
condition: data.weather[0].description,
humidity: data.main.humidity,
};
},
});
Using Integrations
Call installed integrations through the environment:
const createNotificationTool = (env: Env) =>
createTool({
id: "SLACK_NOTIFY",
description: "Send a notification to a Slack channel",
inputSchema: z.object({
channel: z.string(),
message: z.string(),
}),
outputSchema: z.object({
sent: z.boolean(),
timestamp: z.string(),
}),
execute: async ({ context }) => {
const result = await env["slack"].POST_MESSAGE({
channel: context.channel,
text: context.message,
});
return {
sent: true,
timestamp: result.ts,
};
},
});
Install integrations from Apps in your workspace. Each integrationβs tools are available at env["integration-id"] where the ID is shown in the Apps section (e.g., env["gmail"] , env["slack"] ).
Database Operations
Every workspace includes built-in SQLite with Drizzle ORM:
import { getDb } from "./db";
import { customers } from "./schema";
const createCustomerTool = (env: Env) =>
createTool({
id: "CUSTOMER_CREATE",
description: "Create a new customer in the database",
inputSchema: z.object({
name: z.string().min(1),
email: z.string().email(),
city: z.string(),
state: z.string().length(2),
}),
outputSchema: z.object({
id: z.number(),
createdAt: z.date(),
}),
execute: async ({ context }) => {
const db = await getDb(env);
const [result] = await db
.insert(customers)
.values({
name: context.name,
email: context.email,
city: context.city,
state: context.state,
})
.returning();
return {
id: result.id,
createdAt: result.createdAt,
};
},
});
Each project gets isolated SQLite storage on Cloudflareβs D1. No credentials needed.
Registering Tools
Add tools to server/main.ts :
import { withRuntime } from "@deco/workers-runtime";
import { createEmailTool } from "./tools/email";
import { createCustomerTool } from "./tools/customers";
const { Workflow, ...runtime } = withRuntime<Env>({
tools: [
createEmailTool,
createCustomerTool,
// Add more tools here
],
workflows: [],
views: [],
});
export { Workflow };
export default runtime;
After registration, run npm run gen:self to generate types for your frontend.
Testing Tools
Start your dev server and test in two ways:
1. Via Admin UI:
npm run dev- Copy preview URL
- Admin β Apps β Add Integration (paste URL +
/mcp) - Test tools through the auto-generated interface
2. Via Code:
// In your React component
import { client } from "./lib/rpc";
const result = await client.tools.EMAIL_SEND({
to: "user@example.com",
subject: "Test",
body: "Hello!",
});
Tools are immediately available via typed RPC, no API layer needed.
Best Practices
Single Responsibility
β
EMAIL_SEND, CUSTOMER_CREATE, INVOICE_GENERATE
β CUSTOMER_CREATE_AND_EMAIL_AND_NOTIFY
Descriptive Names - Follow RESOURCE_ACTION pattern:
β
ORDER_TOTAL_CALCULATE, LEAD_QUALIFY
β calculateTotal, qualify_lead
Strong Typing - Zod schemas provide runtime validation and compile-time types:
inputSchema: z.object({
email: z.string().email(), // Email validation
amount: z.number().positive(), // Must be > 0
tier: z.enum(["A", "B", "C"]), // Only these values
})
Error Handling
execute: async ({ context }) => {
try {
const result = await api.call(context);
return result;
} catch (error) {
throw new Error(`Failed to process: ${error.message}`);
}
}
Organizing Tools
Group related tools in files:
server/tools/
βββ index.ts # Export all tools
βββ customers.ts # CUSTOMER_CREATE, CUSTOMER_FETCH, etc.
βββ emails.ts # EMAIL_SEND, EMAIL_VALIDATE
βββ billing.ts # INVOICE_CREATE, PAYMENT_PROCESS
Export from index.ts :
export { createCustomerTool, createCustomerFetchTool } from "./customers";
export { createEmailTool } from "./emails";
export { createInvoiceTool } from "./billing"; Found an error or want to improve this page?
Edit this page