Defining Tools
Server tools are defined with defineTool in TypeScript or @pillar.tool() in Python. Each tool has a name, description, input schema, and an execute handler that runs on your server.
Basic definition
import { defineTool } from '@pillar-ai/server';import { z } from 'zod';const inviteMember = defineTool({name: 'invite_member',description: 'Invite a new member to the workspace',input: z.object({email: z.string().email().describe('Email of the person to invite'),role: z.enum(['admin', 'member', 'viewer']).describe('Role to assign'),}),guidance: 'Only use when the user explicitly asks to invite someone.',timeoutMs: 15_000,execute: async ({ email, role }, ctx) => {const invite = await workspace.invite(email, role);return { inviteId: invite.id, status: 'sent' };},});
Input schemas
You can define input schemas in multiple ways depending on your preference.
TypeScript: Zod schemas (recommended)
Zod gives you runtime validation and automatic TypeScript type inference:
import { z } from 'zod';const tool = defineTool({name: 'search_orders',description: 'Search orders by status and date range',input: z.object({status: z.enum(['pending', 'shipped', 'delivered']).describe('Order status'),since: z.string().optional().describe('ISO date string'),}),execute: async ({ status, since }) => {// `status` is typed as 'pending' | 'shipped' | 'delivered'// `since` is typed as string | undefined},});
Install zod and zod-to-json-schema as peer dependencies:
npm install zod zod-to-json-schema
TypeScript: Plain JSON Schema
If you prefer not to use Zod, pass a raw JSON Schema object:
import { defineTool } from '@pillar-ai/server';const inviteMember = defineTool({name: 'invite_member',description: 'Invite a new member to the workspace',inputSchema: {type: 'object',properties: {email: { type: 'string', description: 'Email of the person to invite' },role: {type: 'string',enum: ['admin', 'member', 'viewer'],description: 'Role to assign',},},required: ['email', 'role'],},execute: async ({ email, role }: { email: string; role: string }, ctx) => {const invite = await workspace.invite(email, role);return { inviteId: invite.id, status: 'sent' };},});
Python: Type hints (recommended)
The Python SDK infers JSON Schema from function type hints:
from typing import Literal, Annotated, Optional@pillar.tool(description="Search orders by status and date range")async def search_orders(status: Literal["pending", "shipped", "delivered"],since: Annotated[Optional[str], "ISO date string"] = None,ctx: ToolContext,) -> dict:...
Supported types: str, int, float, bool, Optional[T], list[T], dict, Literal[...], and Annotated[T, "description"]. Pydantic BaseModel subclasses are also supported if pydantic>=2.0 is installed.
Python: Explicit JSON Schema
Pass a schema dict directly when you need full control:
from pillar import Pillar, ToolContextpillar = Pillar(secret="plr_...")@pillar.tool(description="Invite a new member to the workspace",input_schema={"type": "object","properties": {"email": {"type": "string", "description": "Email of the person to invite"},"role": {"type": "string","enum": ["admin", "member", "viewer"],"description": "Role to assign",},},"required": ["email", "role"],},)async def invite_member(email: str, role: str, ctx: ToolContext) -> dict:invite = await workspace.invite(email, role)return {"invite_id": invite.id, "status": "sent"}
Tool properties
guidance
Instructions for the agent on when and how to use this tool. The guidance string is included in the agent's system prompt and helps it make better decisions:
const tool = defineTool({name: 'delete_project',description: 'Permanently delete a project',guidance: 'Only use when the user explicitly asks to delete. Always confirm first.',// ...});
@pillar.tool(description="Permanently delete a project",guidance="Only use when the user explicitly asks to delete. Always confirm first.",)async def delete_project(project_id: str, ctx: ToolContext) -> dict:...
timeoutMs / timeout_ms
Maximum time in milliseconds for the tool to execute. Defaults to 30,000 (30 seconds):
defineTool({timeoutMs: 60_000, // Allow up to 60 seconds// ...});
@pillar.tool(timeout_ms=60_000, description="...")async def slow_operation(ctx: ToolContext) -> dict:...
channelCompatibility / channel_compatibility
Restrict which channels can trigger this tool. Defaults to ["*"] (all channels):
defineTool({channelCompatibility: ['web', 'slack'],// ...});
@pillar.tool(channel_compatibility=["web", "slack"], description="...")async def web_only_tool(ctx: ToolContext) -> dict:...
Return values
The return value from execute is serialized to JSON and sent back to the agent. Return whatever data the agent needs to form a response:
execute: async ({ email }) => {const customer = await db.findByEmail(email);if (!customer) {return { found: false };}return {found: true,name: customer.name,plan: customer.plan,activeSubscription: customer.isActive,};},
For errors, throw an exception. The SDK catches it and returns an error payload to Pillar Cloud:
execute: async ({ email }) => {const customer = await db.findByEmail(email);if (!customer) {throw new Error(`No customer found with email: ${email}`);}return { name: customer.name };},
ToolContext
Every execute handler receives a ToolContext as its second argument (TypeScript) or as a parameter named ctx / context (Python):
| Field | Type | Description |
|---|---|---|
caller | CallerInfo | Identity and channel metadata for the user |
conversationId / conversation_id | string | Current conversation ID |
callId / call_id | string | Unique ID for this tool call |
productId / product_id | string? | Product ID, if applicable |
confirmed | boolean | true when re-executing after user confirmation |
isIdentified / is_identified | boolean | true when the caller has a resolved external user ID |
CallerInfo
| Field | Type | Description |
|---|---|---|
channel | string | 'web', 'slack', 'discord', 'email', or 'api' |
channelUserId / channel_user_id | string? | Channel-specific user ID |
externalUserId / external_user_id | string? | Your app's user ID (set via identify) |
email | string? | User's email, if known |
displayName / display_name | string? | User's display name |
Registering tools
TypeScript
Pass an array of tool definitions to registerTools:
const pillar = new Pillar({ secret: process.env.PILLAR_SECRET! });await pillar.registerTools([lookupCustomer, inviteMember, deleteProject]);
Python
Tools are automatically registered when you use the @pillar.tool() decorator. No separate registration call is needed.
By default, the SDK auto-registers tools with Pillar Cloud on the first incoming webhook request. Set autoRegister: false (TypeScript) or auto_register=False (Python) to disable this and call register() manually.
Next steps
- Framework Integration — Mount the webhook handler in Express, Django, and more
- Confirmation Flows — Ask users to confirm before destructive actions
- Webhook Security — How signatures work