Building

Tools

Give your agents a new action the model can call: a description, a JSON schema and a handler, running in the plugin sandbox.

View as Markdown

A tool is a function the model can decide to call, exactly like the built-in web_search or workspace_read_file. You describe it with a name, a description and a JSON Schema for its input; the model decides when to call it and with what arguments; your handler runs in the sandbox and whatever it returns goes back to the model.

Register a tool

Written for these docs. Official plugins register tools the same way — for example briefing_weather in plugins/morning-briefing/src/weather.ts. The shape below is exactly what the runtime and the publish check enforce.

globalThis.manifest = {
  egress: ["api.open-meteo.com"],
};

globalThis.tools.get_forecast = {
  description: "Get tomorrow's temperature range for a latitude/longitude. Use when the user asks about weather.",
  parameters: {
    type: "object",
    properties: {
      latitude: { type: "number", description: "Decimal degrees, north positive" },
      longitude: { type: "number", description: "Decimal degrees, east positive" },
    },
    required: ["latitude", "longitude"],
  },
  handler: async (input: { latitude: number; longitude: number }) => {
    const response = await host.fetch({
      url:
        "https://api.open-meteo.com/v1/forecast" +
        `?latitude=${input.latitude}&longitude=${input.longitude}` +
        "&daily=temperature_2m_max,temperature_2m_min&forecast_days=2",
    });
    if (response.status !== 200) throw new Error(`forecast service returned ${response.status}`);
    const daily = JSON.parse(response.body).daily;
    return { minC: daily.temperature_2m_min[1], maxC: daily.temperature_2m_max[1] };
  },
};
Field Rules
name (the key) ^[a-z][a-z0-9_]{0,63}$ — lowercase, digits, underscores, starts with a letter
description required, non-empty. This is what the model reads to decide when to call you: say what it does and when to use it.
parameters optional JSON Schema for the input. Defaults to { "type": "object", "properties": {} } (no arguments). Must be plain JSON.
handler required, a function (input, context) => result (sync or async).
alwaysListed optional boolean. true keeps the tool in the agent's tool list when an agent has more tools than its model takes and your other tools move behind search_tools. At most 3 per plugin; a 4th fails the publish.
deferred optional boolean. true keeps the tool behind search_tools on every agent, however few tools the agent has. Cannot be combined with alwaysListed; a tool that sets both fails the publish.

A bare function (globalThis.tools.x = async () => …) or a registration without a description fails the publish.

Keep a tool out of the list

A plugin with many tools would add all of them to every request of every agent it is installed on. Mark a tool deferred: true and the agent no longer sees it in its list; it finds the tool with search_tools and runs it with call_tool. Write the tool's description with the words someone would search for, because that text is what search_tools matches on. The owner can still pin the tool on the agent's Access tab, and a pin overrides deferred. You cannot set both deferred and alwaysListed on one tool.

What your handler receives and returns

  • input is the model's arguments object, parsed from JSON. The platform doesn't validate it against your schema — check what you depend on.
  • context is { callId: string; initiator: TurnInitiator }, frozen. initiator says what started the turn, set by the platform and never by the model: kind is "user", "channelSender", "schedule", "event", "agent" or "platform", and isVerifiedPerson is true when every input came from a signed-in user or a verified contact. It does not see text the model read through tools or earlier turns, so it is not proof that an action is free of planted instructions. Use it to refuse actions that a schedule, an event or another agent must not trigger on its own.
  • Return any JSON-serializable value. It is serialized and handed to the model as the tool's result. Returning undefined sends null.
  • Throwing is fine. A thrown error, a timeout or a memory fault becomes a failed tool result the model can see and react to; the agent's turn carries on.

Keep results compact: everything you return lands in the model's context.

How the model sees it

Your tool joins the agent's tool list next to the built-ins, named <plugin-slug>__<tool-name> — for example weather__get_forecast for a plugin with slug weather. Two plugins can therefore both have a search tool. (A tool whose final name equals a built-in tool's name is dropped with a warning.)

Which agents get it

Installing the plugin makes the tool available to every agent on the account. On each agent's Access tab the owner can switch Tools from "all" to a short list, deny your tool on one agent, or pin it. Nothing to do on your side. A crew plugin can instead fix an agent's tool list itself with tools on host.agents.create/update.

What a tool can reach

A tool call runs inside a chat, on behalf of one agent, so it gets the richest context of any capability:

  • host.fetch, host.settings, host.crypto, host.log — always.
  • host.session.* — act on the calling chat: read the conversation, post a message, manage todos, schedule a timer, spawn a subagent, message a peer (each by permission — see Permissions).
  • host.workspace.* — read and write the calling agent's files (and the shared workspace).
  • host.agents.*, host.cronJobs.*, host.models.list() — your plugin's own crew and schedules.
  • host.plugins.* — build, try and release the plugins the account owns, asking the user before anything leaves the account (details).
  • host.oauth.accounts(), host.license() — the customer's connected sign-ins and your licence.
  • host.session.llm, host.llm, host.decide — call a model.
  • host.media.* — upload and transcode audio.

Written for these docs — a tool that saves a note into the agent's workspace:

globalThis.tools.save_note = {
  description: "Save a short note into the agent's workspace under notes/.",
  parameters: {
    type: "object",
    properties: { title: { type: "string" }, text: { type: "string" } },
    required: ["title", "text"],
  },
  handler: async (input: { title: string; text: string }) => {
    const path = `notes/${input.title.replace(/[^a-z0-9-]+/gi, "-").toLowerCase()}.md`;
    await host.workspace!.write({ path, content: input.text });
    return { saved: path };
  },
};

Calling host.workspace.write derives host:workspace:write, so the installer sees "can write the agent's files" before installing.

Limits

30 s wall clock and 128 MB per call (at least 60 s and 512 MB with host:media:transcode, and at least 120 s with agentApi:plugins.write). Each call is metered as a plugin invocation.

TypeScript

The downloadable SDK types declare the registry: declare var tools: Record<string, ToolDefinition>, with ToolDefinition = { description: string; parameters: Record<string, unknown>; handler: (input: any, context: ToolCallContext) => unknown | Promise<unknown>; alwaysListed?: boolean; deferred?: boolean }. The types require parameters; the runtime defaults it to no arguments, so a tool with none still writes parameters: { type: "object", properties: {} } when you type-check.