Overview
Create custom tools that agents can use alongside built-in and marketplace tools. Custom tools use the defineTool() function from @runtools/sdk and are deployed to your organization via runtools deploy.
Custom tools use an actions-based structure. Each tool has a name, credentials spec, and one or more actions:
import { defineTool } from '@runtools/sdk';
export default defineTool({
name: 'my-database',
description: 'Query my application database',
credentials: {
required: ['databaseUrl'],
schema: {
databaseUrl: { type: 'string', description: 'PostgreSQL connection string' },
},
},
actions: {
query: {
description: 'Execute a SQL query',
parameters: {
type: 'object',
properties: {
sql: { type: 'string', description: 'SQL query to execute' },
},
required: ['sql'],
},
execute: async (params, credentials) => {
// params.sql is the query, credentials.databaseUrl is the connection string
const result = await myDb.query(params.sql, credentials.databaseUrl);
return { rows: result.rows };
},
},
},
});
| Field | Required | Description |
|---|
name | ✅ | Tool slug/identifier |
description | ❌ | Human-readable description |
category | ❌ | Grouping for marketplace display |
credentials | ❌ | Credential requirements and OAuth config |
actions | ✅ | Object of named actions (at least one required) |
Action Structure
Each action must have:
| Field | Required | Description |
|---|
description | ❌ | What this action does |
parameters | ✅ | JSON Schema defining accepted parameters |
execute | ✅ | Async function (params, credentials) => result |
Credentials & OAuth
Manual Credentials
credentials: {
required: ['apiKey'],
schema: {
apiKey: { type: 'string', description: 'API key' },
},
},
OAuth Credentials
Add an oauth block to enable automatic token resolution from Connected Apps:
credentials: {
required: ['accessToken'],
schema: {
accessToken: { type: 'string', description: 'Google OAuth2 access token' },
},
oauth: {
provider: 'google', // OAuth provider
scopes: ['https://www.googleapis.com/auth/gmail.modify'], // Required scopes
credentialMapping: { accessToken: 'access_token' }, // Map credential keys → token fields
},
},
When OAuth is configured:
- If the user has a connected Google account,
credentials.accessToken is automatically populated with a fresh token
- Tokens are auto-refreshed before they expire
- No manual token management required
Available providers: google, github, slack, discord, microsoft, x, linkedin, telegram, whatsapp.
Multiple Actions
A single tool can have many actions:
import { defineTool } from '@runtools/sdk';
export default defineTool({
name: 'my-api',
description: 'Interact with my internal API',
credentials: {
required: ['apiKey'],
schema: {
apiKey: { type: 'string', description: 'API key' },
},
},
actions: {
list_users: {
description: 'List all users',
parameters: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max results' },
},
required: [],
},
execute: async (params, credentials) => {
const res = await fetch(`https://my-api.com/users?limit=${params.limit || 10}`, {
headers: { Authorization: `Bearer ${credentials.apiKey}` },
});
return await res.json();
},
},
create_user: {
description: 'Create a new user',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'User name' },
email: { type: 'string', description: 'User email' },
},
required: ['name', 'email'],
},
execute: async (params, credentials) => {
const res = await fetch('https://my-api.com/users', {
method: 'POST',
headers: {
Authorization: `Bearer ${credentials.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: params.name, email: params.email }),
});
return await res.json();
},
},
},
});
Error Handling
Return structured errors that agents can understand:
execute: async (params, credentials) => {
try {
const result = await riskyOperation(params.input);
return { success: true, result };
} catch (error) {
// Return error as result — agent will see this and can adjust
return { success: false, error: error.message };
},
},
Don’t throw errors from execute unless you want the tool call to fail entirely. Returning an error object lets the agent see the error and potentially retry or adjust its approach.
Reference custom tools by slug in your agent definition:
import { defineAgent } from '@runtools/sdk';
export default defineAgent({
slug: 'data-assistant',
model: 'claude-sonnet-4',
systemPrompt: 'You help users query their data.',
tools: ['bash', 'read_file', 'my-database'], // custom tool slug
});
Deploying & Publishing
# Deploy all tools and agents to your org
runtools deploy
# List your custom tools
runtools tool list --custom
# Make a tool public on the marketplace (optional)
runtools tool publish my-database
Custom tools are stored in the custom_tools database table and executed via Bun’s native TypeScript import — no VM overhead. The tool code runs server-side on tools.runtools.ai.
Types
import type {
ToolDefinition,
ToolAction,
CredentialSpec,
JSONSchema,
} from '@runtools/sdk';