Skip to main content

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.

Defining Tools

Custom tools use an actions-based structure. Each tool has a name, credentials spec, and one or more actions:
tools/my-database.ts
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 };
      },
    },
  },
});

Tool Structure

FieldRequiredDescription
nameTool slug/identifier
descriptionHuman-readable description
categoryGrouping for marketplace display
credentialsCredential requirements and OAuth config
actionsObject of named actions (at least one required)

Action Structure

Each action must have:
FieldRequiredDescription
descriptionWhat this action does
parametersJSON Schema defining accepted parameters
executeAsync 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:
  1. If the user has a connected Google account, credentials.accessToken is automatically populated with a fresh token
  2. Tokens are auto-refreshed before they expire
  3. 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:
tools/my-api.ts
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.

Using Custom Tools in Agents

Reference custom tools by slug in your agent definition:
agents/data-assistant.ts
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';