Building

Tools

Give agents a new function the model can call.

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 — no plugin in the official set registers a tool yet. 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) => result (sync or async).

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

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.
  • 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.

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.models.list() — your plugin's own crew.
  • 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 (60 s with host:media:transcode). Each call is metered as a plugin invocation.

TypeScript

The downloadable SDK types don't declare the tools registry yet. Until they do, add this to your project (any .ts file, or a tools.d.ts next to the SDK types):

interface ToolRegistration<TInput = any> {
  description: string;
  parameters?: Record<string, unknown>;
  handler: (input: TInput) => unknown | Promise<unknown>;
}
declare var tools: Record<string, ToolRegistration>;