Appearance
Tools
The tool registry is the lookup table that tool: <id> workflow nodes resolve against. Every tool comes from a connector. Every tool gets one entry. Every entry knows how to invoke itself.
If you just want to use one, browse Settings → Tools (with kind filter + search). If you want to add one, see the connectors reference — tools are added through their connector's manifest.
What a tool entry has
typescript
{
id: 'gws.gmail.users.messages.list',
kind: 'cli',
displayName: 'List Gmail messages',
description: 'Lists the messages in the user\'s mailbox…',
connectorId: 'gws',
outputFormat: 'json',
builtIn: true,
}id is what you put in tool: <id> YAML. Always <connectorId>.<localId>. Never invented; always derived from the connector's manifest.
The five kinds
The tool's kind tells Z.E.N. how to invoke it.
cli — spawn a binary
Most common kind. Used by GWS (gws schema, gws gmail.users.messages.list), Printing Press CLIs, any local binary you wrap in a connector manifest.
Z.E.N. spawns the binary with structured args, captures stdout, parses according to outputFormat. Stderr lands in the error message when exit is non-zero.
advancedShell two-gate: Some CLI tools want shell features (pipes, env expansion). The endpoint itself must declare advancedShell: true, AND the workflow node must opt in with tool_advanced_shell: true. Both sides must agree before the executor runs the command in shell mode. This blocks a runaway agent from escalating a deterministic CLI tool to shell execution.
mcp — call an MCP server
When mcporter has an MCP server registered, every method it exposes becomes a mcp.* tool. The mcporter adapter brokers the call — Z.E.N. doesn't speak MCP directly; mcporter is the bridge.
When you mcporter add notion and the server exposes search, query_database, etc., they show up as mcp.notion.search, mcp.notion.query_database. Add a new MCP server, refresh the connector, the tools appear.
http — plain HTTP request
URL, method, headers, body shape. Auth comes from the connector's auth block. Response parsed per outputFormat.
composio — Composio toolkit
Phase 6B. Composio adapter takes a toolkit id, fetches the tool catalog, registers each as a composio.<toolkit>.<tool> entry. The endpoint knows the Composio session id; invocations go through Composio's API.
in_process — registered function
Programmatically registered before workflow start. Used for test fixtures and tools that need closure over runtime objects. Not loaded from manifests; explicitly added via the runtime API.
Output formats
outputFormat tells the tool node how to parse what the tool returns:
| Format | What it is | $nodeId.output shape |
|---|---|---|
json | Single JSON document | The parsed object |
ndjson | Newline-delimited JSON (one record per line) | Array of parsed records |
text | Raw text | String |
binary | Raw bytes | Base64-encoded string |
Most tools are json. NDJSON is useful for streaming list outputs from CLIs.
How a tool: node invokes one
yaml
- id: list-unread
tool: gws.gmail.users.messages.list
tool_args:
userId: me
q: is:unread
tool_timeout_ms: 30000
tool_advanced_shell: false- Z.E.N. looks up
gws.gmail.users.messages.listin the in-memory tool registry. - Finds
kind: cliand the spawn details (bin, args, outputFormat). - Dispatches to the CLI invoker with
tool_argsmerged into the command. - Runs the binary with
tool_timeout_msceiling. - Parses output per
outputFormat. - Writes parsed result to
$nodeId.output. - Records the call as a
workflow_eventsrow.
Errors (non-zero exit, timeout, unknown id) become a state: failed node output with a verbatim message. See tool nodes.
How tools end up in the registry
boot
│
▼
┌────────────────────────────────────────────────┐
│ @zen/connectors discovery + bootstrap │
│ │
│ bundled adapters (GWS, mcporter, Printing │
│ Press) auto-register their connectors │
│ │
│ global manifests (~/.zen/connectors/*.yaml) │
│ project manifests (<repo>/.zen/connectors/) │
│ parsed + registered │
└─────────────────┬──────────────────────────────┘
│
▼ for each connector
┌────────────────────────────────────────────────┐
│ @zen/tools converter │
│ │
│ walks manifest.tools[], builds a ToolEndpoint │
│ for each, registers under <connectorId>.<id> │
└─────────────────┬──────────────────────────────┘
│
▼
in-memory tool registry
surfaced at /api/tools
rendered in Settings → Tools
picked from in workflow editorEvery tool you see in Settings → Tools came through this path. No manual registration. Add a connector → tools appear. Remove a connector → tools disappear.
When to use tool: vs agent: vs bash:
| You want… | Use |
|---|---|
| One specific known API call with structured args | tool: |
| One shell command you wrote | bash: |
| An AI to decide which tool(s) to call | agent: with tools available via its harness |
| A specific script in a specific runtime | script: (bun or uv) |
Tool nodes are deterministic — same args, same call, same result. No AI, no thinking, no plan. When you need that, use tool:. When you need the model to figure it out, use agent:.
Programmatic API
typescript
import { invokeTool, registerToolEndpoint, getToolInfoList } from '@zen/tools';
// register a tool endpoint programmatically (for in_process kind or tests)
registerToolEndpoint({
id: 'mycompany.echo',
kind: 'in_process',
invoke: async (args) => ({ ok: true, args }),
outputFormat: 'json',
});
// invoke at runtime
const result = await invokeTool({
toolId: 'mycompany.echo',
args: { message: 'hi' },
});Connector-loaded tools register via the converter automatically. You only need the programmatic API for in_process kinds or tests.
Related
- Tool nodes — the YAML surface
- Connectors — how tools get registered
- MCP servers — adding MCP tools via mcporter
- Issue #14 — CLI registry extensibility follow-ups