> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cycls.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools

> Enable built-in tools by name, or register your own Python handlers. Both stream into the same UI.

Tools come from two places. Built-in tools ship with the SDK and are enabled by
name. Custom tools are JSON schemas you define with async handlers you write.

```python theme={null}
llm = (
    cycls.LLM()
    .model("anthropic/claude-sonnet-4-6")
    .allowed_tools(["Bash", "Editor", "WebSearch", "Canvas"])   # built in
    .tools(MY_TOOLS)                                            # your schemas
    .on("create_ticket", create_ticket)                         # your handler
)
```

## Built-in tools

Each name enables one or more tools and brings its own prompt guidance, so
enabling it is the only switch you need.

| Name        | What the model gets                                                                       |
| ----------- | ----------------------------------------------------------------------------------------- |
| `Bash`      | `bash`: run shell commands in a sandbox rooted at the workspace                           |
| `Editor`    | `read` and `edit`: view and modify workspace files                                        |
| `WebSearch` | `web_search` and `web_fetch`, or the provider's native search                             |
| `Browser`   | `browser`: drive a real Chrome session, only offered when a browser service is configured |
| `DataBase`  | `database`: a per-user key-value store with prefix scans                                  |
| `Canvas`    | `canvas`: open a finished file in the side panel viewer                                   |
| `Apps`      | `build_app`: bundle a source folder into an installable mini app                          |
| `Suggest`   | `suggest`: one follow-up chip above the composer                                          |
| `Ask`       | `ask`: up to three questions on one card, which ends the turn                             |

<Tabs>
  <Tab title="Bash">
    Runs inside a `bubblewrap` sandbox with the user's workspace bound at
    `/workspace` and a sanitized environment. The working directory is the
    workspace, scratch belongs in `.tmp/`, and `/tmp` is per-command and discarded.

    ```python theme={null}
    llm = cycls.LLM().allowed_tools(["Bash"]).bash_timeout(600).sandbox(network=True)
    ```

    Network access is on by default so `curl`, `pip` and `git` work. A prompt
    injection can exfiltrate anything the sandbox can read, so turn it off when the
    agent does not need it:

    ```python theme={null}
    llm = cycls.LLM().allowed_tools(["Bash"]).sandbox(network=False)
    ```
  </Tab>

  <Tab title="Editor">
    `read` renders files, including PDFs page by page and images for vision models.
    `edit` creates and modifies files. The model is told to use these rather than
    `cat` or `sed`, which keeps writes inside the safety checks and off the output
    token budget.

    Paths are workspace relative. Traversal outside the workspace is refused, and so
    are the reserved internal directories.
  </Tab>

  <Tab title="WebSearch">
    ```python theme={null}
    llm = cycls.LLM().allowed_tools(["WebSearch"]).web_search("brave")
    ```

    `brave` is the portable pair, `web_search` plus `web_fetch`. It works on every
    provider and needs `BRAVE_API_KEY`. Results arrive in the UI as one `sources`
    row of citation chips at the end of the answer. `native` uses the provider's
    server-side search, which is Anthropic only today.
  </Tab>

  <Tab title="Browser">
    A real Chrome session driven step by step: `open`, `read`, `click`, `type`,
    `press`, `back`, `evaluate`, `screenshot`, `download`. The page persists between
    calls in a turn, so logins and multi-step forms work.

    The browser itself runs in a shared service, not in your image. Configure it with
    environment variables, and the tool is not offered when they are unset:

    ```bash theme={null}
    BROWSER_PROVIDER=cycls
    BROWSER_URL=https://cycls-browser.cycls.ai
    BROWSER_SECRET=your_shared_secret
    ```
  </Tab>

  <Tab title="DataBase">
    A per-user JSON key-value store with `get`, `put`, `delete` and `scan`. It is
    private to the user: no teammate and no app can read it. Use it for agent memory
    across chats, such as preferences and task progress.

    Anything a teammate or an app needs to read belongs in a workspace file or under
    `apps/<slug>/data/` instead.
  </Tab>

  <Tab title="Canvas and Apps">
    `Canvas` opens one finished deliverable in the side panel. It renders markdown,
    HTML, PDF, images, audio, video, code, CSV, spreadsheets and 3D models. Office
    formats render read-only through the office service, and anything else falls back
    to a download card.

    `Apps` lets the agent bundle a source folder into a single self-contained HTML
    file installed in the Apps tab. Apps run under a strict CSP with no external
    fetch, and reach their own data through a small bridge, so a dashboard stays live
    without holding any key.
  </Tab>

  <Tab title="Suggest and Ask">
    `Suggest` shows one follow-up chip above the composer, written in the user's
    voice. `Ask` puts up to three questions on one card and ends the turn, with the
    answers arriving as the next user message. The options are shortcuts, not a gate:
    the user can type anything instead.

    Users can switch both off in Settings, and `context.disabled_tools` reflects that
    for the turn.
  </Tab>
</Tabs>

<Note>
  A user can switch a tool off in Settings. `LLM.run()` drops those tools for the
  turn. A switch can never enable a tool the operator did not allow.
</Note>

## Custom tools

A tool schema is a plain dict. A handler is an async function. The return value
goes to the UI stream and to the model as the `tool_result`, so one return covers
both destinations.

```python theme={null}
TOOLS = [
    {
        "name": "create_ticket",
        "description": "Open a support ticket for the current user.",
        "inputSchema": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "priority": {"type": "string", "enum": ["low", "high"]},
            },
            "required": ["title"],
        },
    }
]


async def create_ticket(args):
    ticket = await support.create(args["title"], args.get("priority", "low"))
    return f"Created ticket {ticket.id}"


llm = (
    cycls.LLM()
    .model("anthropic/claude-sonnet-4-6")
    .tools(TOOLS)
    .on("create_ticket", create_ticket, label=lambda i: i["title"])
)
```

### Handler signature

A handler takes the tool input, and optionally a second argument with request
context.

```python theme={null}
async def create_ticket(args, ctx):
    ws = ctx.workspace          # the caller's workspace
    user = ctx.user             # the authenticated user
    return f"Created for {user.id}"
```

### Presentation

`.on()` controls how the call appears in the UI.

| Argument       | Effect                                                                                                                                 |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `label`        | a function from input to a string, rendering the step line like `Bash(command)`. Defaults to the first string value in the input       |
| `icon`         | an image URL shown where a connector logo goes                                                                                         |
| `details=True` | the step opens into Request and Response panes, and the result goes there instead of into the chat. The model still receives all of it |

```python theme={null}
llm = cycls.LLM().on(
    "search_orders",
    search_orders,
    label=lambda i: f"orders for {i['email']}",
    icon="https://example.com/logo.png",
    details=True,
)
```

### Returning UI components

A handler can return a component dict instead of text, and it renders in the
conversation.

```python theme={null}
async def render_chart(args):
    path = await make_chart(args["series"])
    return {"type": "image", "src": path, "caption": "Revenue by month"}
```

## Reserved names

`skill` and `find_tools` are reserved by the loop. `skill` loads a
[skill](/agents/knowledge#skills) on demand, and `find_tools` loads a connector's
tools when the model needs them.

## Next steps

<CardGroup cols={2}>
  <Card title="Custom tool guide" icon="code" href="/guides/custom-tool">
    A complete tool with a handler, a label and a UI return.
  </Card>

  <Card title="Connectors" icon="plug" href="/agents/connectors">
    OAuth grants, API keys, MCP servers and per-tool approvals.
  </Card>
</CardGroup>
