Custom Tools
Build custom tools to connect agents to any API, database, or service.
Tool Types
| Type | Best For | Execution |
|---|---|---|
| HTTP | REST APIs | Server-side HTTP call |
| JavaScript | Custom logic | Sandboxed JS execution |
| Webhook | External processing | Callback to your server |
| Database | SQL queries | Direct DB connection |
Creating HTTP Tools
Connect to any REST API:
const tool = await client.tools.create({
name: 'weather_lookup',
slug: 'weather-lookup',
description: 'Get current weather for a location',
category: 'api',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'City name'
},
units: {
type: 'string',
enum: ['metric', 'imperial'],
default: 'metric'
}
},
required: ['city']
},
executionType: 'http',
executionConfig: {
url: 'https://api.weather.com/v1/current',
method: 'GET',
headers: {
'Authorization': 'Bearer ${secrets.WEATHER_API_KEY}',
'Content-Type': 'application/json'
},
queryParams: {
q: '${city}',
units: '${units}'
}
},
secretsRequired: ['WEATHER_API_KEY']
});
HTTP Configuration Options
executionConfig: {
// URL with variable substitution
url: 'https://api.example.com/users/${userId}',
// HTTP method
method: 'POST', // GET, POST, PUT, PATCH, DELETE
// Headers
headers: {
'Authorization': 'Bearer ${secrets.API_KEY}',
'X-Custom-Header': '${customValue}'
},
// Query parameters
queryParams: {
filter: '${filter}',
limit: '${limit}'
},
// Request body (for POST/PUT/PATCH)
body: {
name: '${name}',
email: '${email}',
data: '${data}'
},
// Body type
bodyType: 'json', // json, form, raw
// Timeout in ms
timeout: 30000,
// Follow redirects
followRedirects: true,
// Response handling
responseType: 'json', // json, text, binary
// Extract specific field from response
responseMapping: {
result: '$.data.result',
status: '$.meta.status'
}
}
Creating JavaScript Tools
For custom logic that doesn't fit HTTP calls:
const tool = await client.tools.create({
name: 'data_transformer',
description: 'Transform and validate data',
category: 'javascript',
inputSchema: {
type: 'object',
properties: {
data: {
type: 'array',
description: 'Array of items to transform'
},
operation: {
type: 'string',
enum: ['sum', 'average', 'filter', 'sort', 'group']
},
options: {
type: 'object',
description: 'Operation-specific options'
}
},
required: ['data', 'operation']
},
executionType: 'javascript',
executionConfig: {
code: `
const { data, operation, options = {} } = input;
switch (operation) {
case 'sum':
const field = options.field || 'value';
return {
result: data.reduce((sum, item) => sum + (item[field] || 0), 0)
};
case 'average':
const avgField = options.field || 'value';
const total = data.reduce((sum, item) => sum + (item[avgField] || 0), 0);
return {
result: data.length > 0 ? total / data.length : 0
};
case 'filter':
return {
result: data.filter(item => {
for (const [key, value] of Object.entries(options.conditions || {})) {
if (item[key] !== value) return false;
}
return true;
})
};
case 'sort':
const sortField = options.field || 'id';
const direction = options.direction || 'asc';
return {
result: [...data].sort((a, b) => {
const aVal = a[sortField];
const bVal = b[sortField];
return direction === 'asc' ? aVal - bVal : bVal - aVal;
})
};
case 'group':
const groupField = options.field || 'category';
return {
result: data.reduce((groups, item) => {
const key = item[groupField] || 'unknown';
groups[key] = groups[key] || [];
groups[key].push(item);
return groups;
}, {})
};
default:
throw new Error('Unknown operation: ' + operation);
}
`
}
});
JavaScript Context
Your code has access to:
// Input from tool call
const { param1, param2 } = input;
// Secrets (read-only)
const apiKey = secrets.API_KEY;
// Context information
const { agentId, conversationId, userId } = context;
// Built-in utilities
const uuid = utils.uuid();
const hash = utils.sha256('data');
const encoded = utils.base64Encode('data');
const decoded = utils.base64Decode(encoded);
const now = utils.now(); // ISO timestamp
// HTTP fetch (limited)
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Return result
return { result: data };
Sandbox Limitations
JavaScript tools run in a secure sandbox:
- No filesystem access
- No process/OS access
- Limited network (fetch only)
- 30 second timeout
- 128MB memory limit
- No eval/Function constructor
Creating Webhook Tools
Delegate execution to your own server:
const tool = await client.tools.create({
name: 'custom_processor',
description: 'Process data with custom backend logic',
category: 'webhook',
inputSchema: {
type: 'object',
properties: {
data: { type: 'object' },
action: { type: 'string' }
},
required: ['data', 'action']
},
executionType: 'webhook',
executionConfig: {
url: 'https://your-server.com/tools/process',
method: 'POST',
headers: {
'X-Webhook-Secret': '${secrets.WEBHOOK_SECRET}'
},
// Include signature for verification
signatureHeader: 'X-Deeployd-Signature',
signatureAlgorithm: 'sha256',
// Timeout
timeout: 60000,
// Async mode (webhook calls back with result)
async: false
},
secretsRequired: ['WEBHOOK_SECRET']
});
Webhook Server Implementation
// Your server
app.post('/tools/process', async (req, res) => {
// Verify signature
const signature = req.headers['x-deeployd-signature'];
const expectedSig = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== `sha256=${expectedSig}`) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { data, action } = req.body.input;
// Process the request
const result = await processData(data, action);
// Return result
res.json({ result });
});
Managing Secrets
Add Secrets
await client.tools.setSecrets('tool-123', {
API_KEY: 'sk-xxxx',
WEBHOOK_SECRET: 'whsec-yyyy'
});
List Required Secrets
const tool = await client.tools.get('tool-123');
console.log(tool.secretsRequired); // ['API_KEY', 'WEBHOOK_SECRET']
Secret Security
- Secrets are encrypted at rest (AES-256)
- Never logged or exposed in responses
- Injected at runtime only
- Per-tool isolation
Tool Permissions
Restrict Tool Access
const tool = await client.tools.create({
name: 'sensitive_operation',
// ... config ...
permissions: {
// Only these agents can use this tool
allowedAgents: ['agent-123', 'agent-456'],
// Require approval for each use
requireApproval: true,
// Only certain roles
allowedRoles: ['admin', 'operator']
}
});
Approval Workflow
When requireApproval: true:
Agent wants to use tool
│
▼
Approval request created
│
▼
Admin approves/rejects
│
▼
Tool executes (if approved)
Testing Tools
Test Execution
const result = await client.tools.test('tool-123', {
input: {
city: 'New York',
units: 'metric'
},
secrets: {
WEATHER_API_KEY: 'test-key'
}
});
console.log({
success: result.success,
output: result.output,
duration: result.durationMs,
error: result.error
});
Dry Run
Test without actual execution:
const validation = await client.tools.validate('tool-123', {
input: {
city: 'New York',
units: 'metric'
}
});
console.log({
valid: validation.valid,
errors: validation.errors,
resolvedUrl: validation.resolvedUrl,
resolvedBody: validation.resolvedBody
});
Example: CRM Integration Tool
const crmTool = await client.tools.create({
name: 'crm_customer_lookup',
slug: 'crm-customer-lookup',
description: 'Look up customer information from CRM by email or ID',
category: 'api',
inputSchema: {
type: 'object',
properties: {
lookupType: {
type: 'string',
enum: ['email', 'id'],
description: 'Type of lookup'
},
value: {
type: 'string',
description: 'Email address or customer ID'
},
includeOrders: {
type: 'boolean',
default: false,
description: 'Include order history'
}
},
required: ['lookupType', 'value']
},
executionType: 'http',
executionConfig: {
url: 'https://api.crm.example.com/v2/customers',
method: 'GET',
headers: {
'Authorization': 'Bearer ${secrets.CRM_API_KEY}',
'X-API-Version': '2024-01'
},
queryParams: {
'${lookupType}': '${value}',
expand: '${includeOrders ? "orders" : ""}'
},
responseMapping: {
customer: '$.data[0]',
orders: '$.data[0].orders',
totalSpent: '$.data[0].totalSpent'
}
},
secretsRequired: ['CRM_API_KEY'],
permissions: {
allowedRoles: ['sales', 'support', 'admin']
}
});
Best Practices
1. Clear Descriptions
// ✅ Good
description: 'Look up customer by email and return profile with order history'
// ❌ Bad
description: 'Get customer'
2. Validate Inputs
inputSchema: {
properties: {
email: {
type: 'string',
format: 'email' // Built-in validation
},
amount: {
type: 'number',
minimum: 0,
maximum: 10000
}
},
required: ['email', 'amount']
}
3. Handle Errors Gracefully
// In JavaScript tools
try {
const result = await fetch(url);
if (!result.ok) {
return {
error: true,
message: `API returned ${result.status}`,
retryable: result.status >= 500
};
}
return { success: true, data: await result.json() };
} catch (error) {
return {
error: true,
message: error.message,
retryable: true
};
}
4. Use Response Mapping
// Extract only what's needed
responseMapping: {
id: '$.data.id',
name: '$.data.profile.displayName',
email: '$.data.contact.email'
}
// Instead of returning entire response
5. Document Tool Usage
// In system prompt
`
You have access to the crm_customer_lookup tool.
Use it when:
- User asks about a customer
- You need to verify customer information
- Looking up order history
Example:
- "What's the order history for john@example.com?"
→ Use crm_customer_lookup with email lookup
`
Next: Explore Security Features for authentication and access control.