Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Tools

Tools are how the agent interacts with the world beyond conversation. They make API calls, query stores, dispatch sub-agents, create tickets, send messages, and perform any action your product needs. The tool system has two layers: built-in tools that ship with every Amodal agent, and custom tools that you define for your specific business logic.

Built-in tools handle the universal operations — making authenticated HTTP requests, reading and writing stores, dispatching sub-agents, rendering widgets. You never define these; they are always available. Custom tools are where you encode product-specific actions: creating a ticket comment with your required fields, classifying a support issue with your rules, or calling an internal workflow that does not map cleanly to one REST endpoint. The split is intentional. Built-in tools give the agent its core capabilities. Custom tools give it your capabilities.

amodal/tools/
└── create_ticket/
    ├── tool.ts         ← tool metadata and handler code
    ├── package.json    ← (optional) npm dependencies
    └── requirements.txt ← (optional) Python dependencies

Iteration 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

These are always available — you don't define them. They appear in the agent's tool list automatically whenever their prerequisites are met (a connection exists, a store is configured, etc.).

ToolRegistered whenWhat It Does
request≥1 connection definedHTTP calls through a connection with automatic auth. Declares intent: 'read' | 'write' | 'confirmed_write' for access control: on confirm-tier endpoints, 'write' returns a preview and a follow-up call with 'confirmed_write' executes it.
store__<store>__getstore definedAuto-generated per store. Reads one document by key.
store__<store>__setstore definedAuto-generated per store. Creates or updates one document.
store__<store>__querystore definedAuto-generated per store. Searches/filter documents.
store__<store>__liststore definedAuto-generated per store. Lists documents.
store__<store>__removestore definedAuto-generated per store. Deletes one document.
dispatch_taskalwaysSpawn a sub-agent with its own isolated state machine. Shares the parent's tools. See Sub-Agents.
presentalwaysRender a widget (info-card, data-table, metric, etc.) as an SSE event for the client to display inline.
stop_executionalwaysEnd the current turn cleanly when the agent is done.
web_searchwebTools block configuredGrounded web search via Gemini Flash + Google Search. Returns a synthesized answer with cited source URLs. See Web Tools.
fetch_urlwebTools block configuredFetch and extract the main content of a URL via Gemini urlContext, with a local fetch + Readability fallback for private networks. See Web Tools.
memorymemory.enabled: true in configPersistent 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 configuredAuto-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",
    "model": "gemini-2.5-flash"
  }
}

Both tools route through a dedicated Gemini Flash instance with Google Search + urlContext grounding — this works regardless of the main model provider. An agent configured to use Anthropic Claude or OpenAI GPT-4o for reasoning can still use web_search / fetch_url; the search call is proxied to Gemini under the hood. Only the google provider is supported today; more providers may be added later.

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. The tool description tells the agent to write specific queries — include dates, names, error messages as relevant — since query quality drives answer quality.

fetch_url

fetch_url({url: "https://example.com/article", prompt: "Extract the API changes"})

Public URLs go through Gemini's urlContext grounding — the model fetches and summarizes the page directly. 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:

StatusTool tells the agent
400 / 401 / 403Auth problem — don't retry. Check the GOOGLE_API_KEY for webTools.
429Rate limit / quota exhausted — don't retry.
5xxTransient — may retry once.

Unexpected errors bubble as ToolExecutionError.

An End-to-End Example

To make tools concrete, here is a custom tool that summarizes a support ticket into a safe operator-facing classification.

The tool definition

amodal/tools/
└── summarize_ticket/
    └── tool.ts
tool.ts:
export 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/${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

  1. The user asks what to do with ticket T-100.
  2. The agent calls summarize_ticket.
  3. The tool loads the ticket through the support-api connection and applies deterministic classification rules.
  4. The agent uses the returned fields to explain confirmed facts, missing data, and the next operator action.

The tool description is critical here — it tells the agent when to use the tool, not just what it does.

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 does deterministic classification that would be unreliable if left to the LLM. Domain computation belongs in tool code, not in the agent's reasoning.

Tool Metadata

Define tool metadata and the handler in amodal/tools/<name>/tool.ts.

FieldTypeDefaultDescription
namestringdirectory nameTool name (snake_case)
base.descriptionstringtool nameShown to the LLM
base.parametersJsonSchemaJSON Schema{}Input parameters
exposure.kindopen | operator-gated | requires-confirmationopenConfirmation and availability tier
llm_callablebooleantrueWhether the LLM can call the tool directly

Confirmation Tiers in Practice

Every tool has a confirmation level that determines whether the agent can call it freely or needs user approval. This is the safety model for write operations; Confirmation Model maps these tiers to the connection-policy and request-intent vocabularies.

TierValueBehavior
Auto-approvefalseAgent calls freely. No user interaction.
ConfirmtrueAgent shows what it will do and waits for user approval.
Review"review"Agent shows full parameters and waits for explicit review.
Never"never"Tool is blocked from agent use (only callable by other tools).

Here is how this plays out in a support workflow:

  1. Read operations auto-approve. The agent reads a ticket, customer summary, or product catalog record. These reads happen without interrupting the user.

  2. Single writes confirm. If the agent wants to add a public ticket comment, the tool has "confirm": true. The user sees the proposed comment and approves it before anything is written.

  3. Sensitive writes require review. If the agent wants to close a ticket or apply a credit, the tool has "confirm": "review". The user reviews the full parameters before execution.

  4. Bulk operations get extra scrutiny. If the agent needs to update many records, show the affected records and require explicit approval for the batch.

  5. Dangerous operations are blocked. A delete_account tool should use "confirm": "never" or remain unavailable to the agent.

The progression is natural: reads are fast, writes are confirmed, bulk writes are reviewed individually, and destructive operations are gated. The user stays in control without being interrupted for every API call.

Handler Context

The ctx object available in every handler:

MethodDescription
ctx.request(connection, endpoint, options?)Make an authenticated API call
ctx.exec(command, options?)Run a shell command
ctx.log(message)Log a message
ctx.userUser info: { roles: string[] }
ctx.signalAbortSignal for cancellation

ctx.request in practice

The request method handles authentication automatically based on the connection config. You specify the connection name and the endpoint path — the runtime resolves the base URL, attaches credentials, and handles retries.

export default async (params, ctx) => {
  // Simple GET — auth is handled by the 'datadog' connection config
  const metrics = await ctx.request("datadog", "/api/v1/query", {
    params: {
      query: `avg:system.cpu.user{host:${params.hostname}}`,
      from: params.startTime,
      to: params.endTime,
    },
  });
 
  // POST with a body
  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 — data processing, file manipulation, or calling CLI tools.

export default async (params, 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.