---
title: "Plugins - AI Tools"
section: "Web Navigator"
route: /webnavigatorpluginsaitools
account: {accountId}
bruce_api: https://{accountId}.api.nextspace.host
guardian_api: https://guardian.nextspace.host
---
# Plugins - AI Tools

An AI Tool Plugin is a Plugin the AI agent can call. Set a Plugin's run location to**AI Tool** and it stops being a piece of UI: it has no container, no HTML, and no button. Instead it is offered to the agent alongside Navigator's built-in tools, and the agent decides when to call it and with what arguments.

This is the way to teach the agent something only your account knows: a lookup against a site system, a calculation over your own attributes, a rule about your own data. You write the function, and the agent works out when it is the right one to reach for.

## Invoke, not Run

A UI Plugin exports `Run(params)` and returns a dispose function. An AI Tool exports `Invoke(args, context)` and returns a result. There is nothing to dispose, because there is nothing on screen.

```javascript
/**
 * Called by the agent.
 * args is model-generated and matches Settings.tool.inputSchema.
 * The return value is serialized to JSON and handed back to the agent, so return data rather than a sentence.
 */
async function Invoke(args, context) {
    // Per-account values, from the Plugin's Settings.config.
    const config = context.config;

    // The Project View that was active when the agent called.
    const viewId = context.viewId;

    // Aborts when the declared timeout elapses.
    const signal = context.signal;

    return {
        answer: 42
    };
}
```

Both forms may live in the same file. Only the one the location calls for is used, so a Plugin can present a panel and expose the same capability to the agent.

## Declaring the tool

The agent cannot see your code, only what the Plugin declares. That declaration lives in the Plugin's `Settings` under a`tool` key, and Operator refuses to save an AI Tool without it.

**Settings**

```json
{
    "tool": {
        "description": "Returns the maintenance backlog for a building, newest first. Use when asked what work is outstanding on a building.",
        "inputSchema": {
            "type": "object",
            "properties": {
                "buildingId": {
                    "type": "string",
                    "description": "Entity ID of the building to report on."
                },
                "limit": {
                    "type": "integer",
                    "description": "Maximum records to return. Defaults to 20."
                }
            },
            "required": ["buildingId"],
            "additionalProperties": false
        },
        "annotations": {
            "sideEffect": "none",
            "timeoutMs": 20000
        }
    },
    "config": {
        "entityTypeId": "maintenance_task"
    }
}
```

**index.js**

```javascript
/**
 * Returns the outstanding maintenance for one building.
 */
async function Invoke(args, context) {
    // Arguments come from the model and are not trusted.
    // Validate before use, exactly as you would with user input.
    const buildingId = typeof args?.buildingId === "string"
        ? args.buildingId.trim()
        : "";
    if (!buildingId) {
        throw new Error("buildingId is required.");
    }

    const limit = Number.isInteger(args?.limit)
        ? Math.min(Math.max(args.limit, 1), 100)
        : 20;

    // An AI Tool is handed no libraries, so a request is a plain
    // fetch. Everything it needs to build one, the base URL, the
    // Entity Type and the token, comes from config rather than
    // from the code.
    const { baseUrl, sessionHeader, token, entityTypeId } = context.config;

    const url = baseUrl + "entities?entityTypeId="
        + encodeURIComponent(entityTypeId)
        + "&pageSize=" + limit;

    const headers = {};
    headers[sessionHeader] = token;

    const res = await fetch(url, {
        headers: headers,
        // Threading the signal is what makes the timeout stop work.
        signal: context.signal
    });
    if (!res.ok) {
        throw new Error("Lookup failed with status " + res.status);
    }
    const data = await res.json();

    // Return the smallest shape that answers the question. Everything
    // returned is spent as agent context, and an oversized result is
    // refused outright.
    const items = data.Items ? data.Items : [];
    return {
        buildingId: buildingId,
        count: items.length,
        tasks: items.map((entity) => ({
            id: entity.Bruce.ID,
            name: entity.Bruce.Name,
            priority: entity.Data?.Priority
        }))
    };
}
```

Note what an AI Tool is not given. There is no `pluginParams`, so no viewer, no visual register, and no `BModels` or `BEngine`: those are passed to a UI Plugin by its host, and an AI Tool has no host. An AI Tool sees its arguments, its context, and the browser's own APIs. Anything it needs to reach an API with, a base URL, a header name and a token, belongs in `Settings.config`, which is also what keeps one Plugin working across accounts without a code change.

## Rules the platform enforces

These are refused at save time rather than at call time, so a mistake here shows up in Operator, not in a conversation with the agent.

| Rule | Why |
| --- | --- |
| The Plugin ID becomes the tool name | The agent calls it as plugin_<pluginId>. The ID may hold letters, numbers, hyphens and underscores only. |
| A description is required | The tool name is an opaque ID and carries no meaning, so the description is the only thing the agent can select on. Without one the tool is registered and never chosen. Set tool.description, or fill in the Plugin's Name and Description. Say what it returns and when to use it. |
| inputSchema must be an inline object schema | Its type must be object, and $ref, $defs, allOf and not are refused: each silently drops the tool out of strict mode, so the arguments stop being structurally guaranteed and nothing reports that it happened. Inline the schema instead. |
| annotations.sideEffect | Either none, or external for a reviewed read-only external request. AI Tools are read-only in this release, so a tool that writes is refused. |
| annotations.timeoutMs | A positive number of milliseconds. Defaults to 15000 and is clamped to 120000, so no tool can pin a call open. The whole operation is inside that budget, source fetch included. |
| The result must be JSON and under 100,000 characters | A result that cannot be serialized, or that is larger than the cap, is reported to the agent as a failure rather than truncated. Return a summary and let the agent ask again for detail. |

## What to expect at call time

The Plugin's code runs in the user's own browser with the user's own permissions, and the Plugin record is re-read server side with that user's session immediately before the call is forwarded. A Plugin the user cannot see is a Plugin the agent cannot call on their behalf, regardless of what any browser has cached.

Timeouts free the caller, not the Plugin. Honouring `context.signal` is up to you, and aborting a signal cannot stop JavaScript that is already blocking the browser thread: a Plugin that blocks stalls Navigator itself. Thread the signal into every request you make.

```javascript
async function Invoke(args, context) {
    // Passing the signal through is what makes a timeout actually stop work.
    const res = await fetch(context.config?.endpoint, {
        signal: context.signal
    });

    // A long loop should check it too, since nothing will interrupt one.
    for (const item of bigList) {
        if (context.signal?.aborted) {
            throw new Error("Aborted.");
        }
    }

    return await res.json();
}
```

Throwing is the correct way to report a problem. The failure is turned into a standard envelope carrying a reason the agent can act on, so an `Error`whose message says what was wrong with the arguments lets the agent correct itself and call again.

## Before enabling one in production

An AI Tool is the one Plugin location where nobody clicks a button to run the code. Review it as you would review anything triggered automatically:

- Validate every argument. They are model-generated, they are not trusted, and a Plugin is the last thing standing between them and your API.
- Keep the tool narrow. One tool that answers one question is chosen correctly far more often than one that does six things behind a mode argument.
- Return only what the answer needs, both because the cap is real and because everything returned is spent as agent context.
- Restrict access where the data warrants it, through the Plugin's login and permission settings, rather than assuming the agent is a safe caller.

---

Urls on this page are resolved for account `{accountId}`.
Site index: https://docs.nextspace.host/llms.txt · whole site in one file: https://docs.nextspace.host/llms-full.txt
Human-readable version of this page: https://docs.nextspace.host/webnavigatorpluginsaitools
