Skip to main content
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.

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

Document parser

read can send documents to a parser you deploy as a function, and give the model the Markdown it returns. The parser receives the file’s bytes and name, and returns three keys.
doc_parser.py
Deploy it, then name it in the agent container’s environment:
.providers.env
The agent calls the parser with cycls.remote, and a deployment answers only the key that deployed it, so the agent’s container needs that key. read sends PDF, Word, PowerPoint, Excel, OpenDocument, RTF and EPUB files up to 50MB, and waits up to 120 seconds. A read with pages skips the parser. A call that fails falls back to reading without it and writes a level=parser log record with the error. The default, cycls-parser, is the parser Cycls runs for its own agents and answers only Cycls’s key. Set CYCLS_PARSER to your deployment, or to an empty string to skip the call.

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.

Handler signature

A handler takes the tool input, and optionally a second argument with request context.
ctx.secret(name) reads what was stored for the connector name, the user’s own grant before the workspace’s. An API key comes back as {"key": ...}, and an OAuth grant as stored, with access_token and expires_at. ctx.secret does not refresh an expired token. A handler that raises sends Error: and the exception to the model as the result, and the turn goes on.

Presentation

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

Returning UI components

A handler can return a component dict instead of text. It renders in the conversation, and the model reads it as JSON.
To give the model and the chat different things, return two channels. _model becomes the tool result, and _ui is one event or a list of events for the chat.
A tool’s sources and the files it opens are kept with the chat, so they come back after a reload. A link in the answer to a source’s url becomes its chip, and a source can say more than its title: A file a tool opens with an open_canvas event in _ui also stands in the conversation as its card, and a picture as itself. A handler can also return a client action with an ack. The client gets the action, and the model reads ack in its place. A confirm or connect action ends the turn, because the user has to answer first.

Reserved names

skill and find_tools are reserved by the loop. skill loads a skill on demand and publishes one with action: "publish". It is offered every turn, even before any skill exists, unless you set .skills(None). find_tools loads a connector’s tools when the model needs them. A built-in tool’s name, such as bash, read or database, is taken too: a custom tool with that name runs the built-in.

Next steps

Custom tool guide

A complete tool with a handler, a label and a UI return.

Connectors

OAuth grants, API keys, MCP servers and per-tool approvals.