Tools
Tools execute actions for the agent. Built-in tools provide connection calls, stores, reference lookup, and chat controls. Custom tools implement your product's calculations, API workflows, and data transformations.
amodal/tools/
└── create_ticket/
├── tool.ts ← tool metadata and handler code
├── package.json ← (optional) npm dependencies
└── requirements.txt ← (optional) Python dependenciesIteration Loop
Custom tool code is source-controlled in the agent repo. To change a tool, edit files under amodal/tools/, commit, push to the connected GitHub branch, and redeploy the agent.
Use the Platform API for the deploy loop:
curl "$AMODAL_API_BASE/api/deployments/redeploy" \
-H "Authorization: Bearer $AMODAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"agentId":"'"$AMODAL_AGENT_ID"'","branch":"main","environment":"production"}'
curl "$AMODAL_API_BASE/api/builds/$AMODAL_BUILD_ID/logs" \
-H "Authorization: Bearer $AMODAL_API_KEY"Build logs are the first place to look for TypeScript, dependency, manifest, and validation failures. Built-in platform tools are configured separately from custom tool code; use the platform-tools API only to enable or disable built-in tools.
Built-in Tools
Built-in tools appear in the agent's tool list when their prerequisites and the agent's resource scope allow them.
| Tool | Registered when | What It Does |
|---|---|---|
| request | ≥1 REST connection without openapi or skipRequestTool: true | HTTP calls with connection authentication and policy checks. In the agent loop, confirm-tier writes pause for approval and resume the same call. Outside the loop, callers receive a preview and must call again with intent: 'confirmed_write'. |
<connection>__<operationId> | connection with openapi.source | Typed operation calls with grouped arguments, policy and confirmation. |
<connection>__discover | connection with openapi.source | Search and activate operation tools for the next reply. |
read_spilled_result | ≥1 native connection | Page a large stored connection result. |
store__<store>__get | store defined | Auto-generated per store. Reads one document by key. |
store__<store>__set | store defined | Auto-generated per store. Creates or updates one document. |
store__<store>__query | store defined | Auto-generated per store. Searches/filter documents. |
store__<store>__list | store defined | Auto-generated per store. Lists documents. |
store__<store>__remove | store sets deletable: true | Auto-generated per store. Deletes one document. |
| call_subagent | ≥1 authored subagent declared | Invoke one or more pre-authored specialist subagents (one calls entry each; max 12 per call, five concurrently). Each runs with its own AGENT.md prompt and tool subset. See Sub-Agents. |
| present | unscoped chat, or declared in a named agent's tools | Render a widget (info-card, data-table, metric, etc.) as an SSE event for the client to display inline. |
| stop_execution | always | End the current turn cleanly when the agent is done. |
| web_search | webTools configured and tool in scope | Grounded web search via Gemini Flash + Google Search. Returns a synthesized answer with cited source URLs. See Web Tools. |
| fetch_url | webTools configured and tool in scope | Fetch and extract the main content of a URL via Gemini urlContext, with a local fetch + Readability fallback for private networks. See Web Tools. |
| memory | enabled, editable, backend available, and tool in scope | Persistent memory with add, remove, list, and search actions. Entries persist across sessions and are injected into the system prompt. See Memory. |
| MCP tools | ≥1 MCP server configured | Auto-discovered from each configured MCP server. Tool names are prefixed with the server name. |
Web Tools
web_search and fetch_url give the agent grounded access to the public web. They're opt-in via a webTools block in amodal.json:
{
"webTools": {
"provider": "google",
"apiKey": "env:GOOGLE_API_KEY"
}
}Both tools use a separate Google provider with Search and urlContext grounding, regardless of the main agent's model. google is the supported web-tool provider. Omit model to use the runtime's search default, or set a Gemini model that supports these features. A named agent must declare web_search and fetch_url in its tools.
web_search
web_search({query: "kubernetes 1.31 deprecations", max_results: 5})Returns a synthesized answer (up to 2000 tokens) with cited source URLs. max_results defaults to 5, capped at 10. Include relevant dates, names, or error messages in the query.
fetch_url
fetch_url({url: "https://example.com/article", prompt: "Extract the API changes"})Public URLs use Gemini's urlContext grounding to fetch and summarize the page. Private-network URLs (localhost, RFC1918, .local) automatically route through a local fetch path with Mozilla Readability extraction.
- Per-hostname rate limit: 10 requests / 60 seconds
- Local fetch: 10s timeout, 1MB body cap
- Falls back to local fetch if Gemini urlContext fails
Error handling
Provider errors are classified by HTTP status so the agent knows whether to retry:
| Status | Tool tells the agent |
|---|---|
| 400 / 401 / 403 | Auth problem. Do not retry. Check the GOOGLE_API_KEY for webTools. |
| 429 | Rate limit or exhausted quota. Do not retry. |
| 5xx | Transient error. May retry once. |
Unexpected errors bubble as ToolExecutionError.
An End-to-End Example
This tool reads a support ticket and returns its status and recommended next action. It assumes a support-api connection.
The tool definition
amodal/tools/
└── summarize_ticket/
└── tool.tsexport default {
id: 'summarize_ticket',
exposure: { kind: 'open' },
llm_callable: true,
base: {
name: 'summarize_ticket',
description: 'Load a support ticket and return a concise, safe operator summary.',
parametersJsonSchema: {
type: 'object',
properties: {
ticket_id: { type: 'string' },
},
required: ['ticket_id'],
},
},
async handle(ctx) {
const params = ctx.input;
const ticket = await ctx.request('support-api', `/tickets/${encodeURIComponent(params.ticket_id)}`);
const category = ticket.category || 'uncategorized';
const needsVerification = category === 'account_access' && ticket.identity_verified !== true;
const nextAction = needsVerification
? 'Verify identity before recommending account changes.'
: 'Review the ticket details and answer from confirmed facts.';
return {
ticket_id: ticket.id,
status: ticket.status,
category,
summary: ticket.summary,
needs_verification: needsVerification,
next_action: nextAction,
};
},
};What happens at runtime
- The user asks what to do with ticket
T-100. - The agent calls
summarize_ticket. - The tool loads the ticket through the
support-apiconnection and applies deterministic classification rules. - The agent uses the returned fields to explain confirmed facts, missing data, and the next operator action.
Describe when to use the tool so the model can select it for the right request.
Custom Tool Definition
Custom tools have a handler that runs code. Use them for logic beyond a single connection request: calculations, data transformation, conditional workflows, or calls to systems that don't have a clean REST API.
export default {
id: 'classify_ticket_priority',
exposure: { kind: 'open' },
llm_callable: true,
base: {
name: 'classify_ticket_priority',
description: 'Classify support ticket priority from category, customer impact, and age.',
parametersJsonSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Ticket category, such as account_access or billing',
},
affected_users: {
type: 'number',
description: 'Approximate number of affected users',
},
age_hours: {
type: 'number',
description: 'How long the ticket has been open',
},
},
required: ['category', 'affected_users', 'age_hours'],
},
},
async handle(ctx) {
const params = ctx.input;
const isSecuritySensitive = params.category === 'account_access';
const hasBroadImpact = params.affected_users >= 10;
const isStale = params.age_hours >= 24;
const priority = isSecuritySensitive && hasBroadImpact ? 'high' : hasBroadImpact || isStale ? 'medium' : 'normal';
return {
priority,
reasons: {
security_sensitive: isSecuritySensitive,
broad_impact: hasBroadImpact,
stale: isStale,
},
};
},
};The handler calculates priority from explicit conditions. Test these conditions directly in code.
Tool Metadata
Define tool metadata and the handler in amodal/tools/<name>/tool.ts.
| Field | Type | Default | Description |
|---|---|---|---|
id (or base.name) | string | directory name with -→_ | Tool name (snake_case) |
base.description | string | tool name | Shown to the LLM |
base.parametersJsonSchema | JSON Schema | {"type":"object","properties":{}} | Input parameters |
exposure.kind | open | operator-gated | requires-confirmation | open | Confirmation and availability tier |
enabled | boolean | true | Whether to register the tool.ts tool |
llm_callable | boolean | true | Whether the LLM can call the tool directly |
Confirmation Tiers in Practice
Connection policies and custom-tool metadata control approval through different fields. Confirmation Model documents how the runtime applies each surface.
For a custom tool, set exposure: {kind: "requires-confirmation"} in tool.ts to pause for approval. tool.json's confirm: true | "review" marks the tool non-read-only, allowing write-intent ctx.request calls, but does not itself prompt the user.
For connection policy.json endpoints:
| Tier | Value | Behavior |
|---|---|---|
| Auto-approve | omitted | A matching policy entry allows execution without user interaction. Native writes without a matching entry are denied. |
| Confirm | true | Agent shows what it will do and waits for user approval. |
| Review | "review" | Blocked for the agent: refused with a reason stating human review is required. The runtime does not queue the call; escalation is up to the host app. |
| Never | "never" | Deny the connection operation. |
Connection policies do not accept confirm: false. In tool.ts, enabled: false prevents registration, and llm_callable: false hides the tool from the model while retaining programmatic access. The tool.json loader does not apply enabled and does not support llm_callable; do not use those fields there as access controls. A tool.json with confirm: "never" skips registration with a warning. Connection policies also support "never" to deny an endpoint.
For a support workflow:
-
Allow permitted ticket reads without confirmation.
-
Set
confirm: trueon the connection endpoint for adding a public comment. For a custom tool that prepares and submits the comment, useexposure.kind: "requires-confirmation". -
Set
confirm: "review"on an endpoint that must be handled by a separate operator workflow. The agent receives a refusal; a chat approval does not execute it. -
For batch changes, make the records and proposed changes explicit in the approval parameters. Batch size does not automatically select a confirmation tier.
-
Deny an endpoint with
confirm: "never", or disable atool.tstool withenabled: false.
Handler Context
A tool.ts handler receives ctx and reads parameters from ctx.input. A handler.ts default export receives (params, ctx). Composition and durable methods require the handler.ts + tool.json layout described in Agent Workflows.
Common context members:
| Method | Description |
|---|---|
ctx.request(connection, endpoint, options?) | Make an authenticated API call |
ctx.exec(command, options?) | Run a shell command |
ctx.store(storeName, payload) | Write a document to a store; resolves to { key: string } |
ctx.env(name) | Read an allowlisted environment variable; undefined if not allowlisted |
ctx.log(message) | Log a message |
ctx.signal | AbortSignal for cancellation |
ctx.scopeId | Per-user scope key for this turn (empty string = agent-level) |
ctx.scopeContext | The turn's scope context: the key/value facts the embedding app sent (ChatWidget scopeContext prop / request context field / JWT claim), e.g. a workspace's mission_id. Read it as a fallback when a fact isn't passed as a tool parameter. undefined when the turn carried none |
ctx.attachments | Files the user attached to this turn's message (top-level tools only; empty/undefined when the turn carried no upload) |
ctx.callTool(name, params) | Call another tool from a composite tool; see Agent Workflows |
ctx.callSubagent(ref, task, input?, opts?) | Dispatch a subagent; opts.attachments forwards files onto the subagent's user turn. See Agent Workflows |
ctx.env() uses the env allowlist in tool.json. The tool.ts layout loads an empty allowlist, so ctx.env() returns undefined there.
Tools with "execution": "durable" in their tool.json additionally get journaled pause/resume methods: ctx.requestInput, ctx.waitForApproval, ctx.sleepUntil, the ctx.step(name, fn) checkpoint for expensive handler-local work, and the replay-frozen ctx.now / ctx.random. See Durable Tools.
ctx.request in practice
The request method resolves the connection's base URL, attaches credentials and sends the authored HTTP request. It does not enforce endpoint permissions or response field restrictions. It also omits request approval, automatic retries, and native OpenAPI validation. Enforce authorization and required approval in the authored workflow or downstream API.
For authored HTTP requests, a tool whose exposure is open resolves to read-only: ctx.request rejects write-intent calls unless the tool declares exposure: { kind: "requires-confirmation" } or tool.json sets confirm: true. A read-only POST endpoint can use explicit intent: 'read'.
export default {
id: 'open_cpu_incident',
exposure: { kind: 'requires-confirmation' },
base: {
/* description, parametersJsonSchema */
},
async handle(ctx) {
const params = ctx.input;
// Connection credentials are applied by ctx.request.
const metrics = await ctx.request('datadog', '/api/v1/query', {
params: {
query: `avg:system.cpu.user{host:${params.hostname}}`,
from: params.startTime,
to: params.endTime,
},
});
// This write is covered by the tool's approval.
const incident = await ctx.request('pagerduty', '/incidents', {
method: 'POST',
data: {
incident: {
type: 'incident',
title: params.title,
service: { id: params.serviceId, type: 'service_reference' },
urgency: params.urgency,
},
},
});
return {
cpuAverage: metrics.series[0]?.pointlist?.map(([ts, val]) => val) || [],
incidentId: incident.incident.id,
};
},
};ctx.exec for shell commands
Use ctx.exec for operations that don't map to a REST API, such as data processing, file manipulation, or calling CLI tools.
export default {
id: 'count_failed_jobs',
exposure: { kind: 'open' },
base: {
/* description, parametersJsonSchema */
},
async handle(ctx) {
// Run a database query via CLI
const result = await ctx.exec('node scripts/count-failed-jobs.js', {
timeout: 10000,
});
const parsed = JSON.parse(result.stdout);
return { failedJobsLastHour: parsed[0].failed_jobs };
},
};Naming Convention
Tool names must be snake_case: lowercase letters, digits, and underscores, starting with a letter. Example: create_ticket, fetch_ticket, classify_priority.