# Subs, a cloud native agent harness

> Source: <https://github.com/substructureai/subs>
> Published: 2026-08-29 02:35:34+00:00

Pre-1.0: APIs and the wire protocol can change between releases.

`subs`

is an agent harness for the cloud.

It runs an unprivileged agent loop with no system access. It uses MCP servers for tools. It runs locally or as a client and a server.

Declare your agents in a config file. To customize the loop, point an agent at an HTTP endpoint and answer a webhook.

`subs`

handles durability, retries, timeouts, MCP connection management, session state, session branching, AG-UI, Slack connection, LLM calls, subagents, interrupts and more.

To turn a sandbox into an MCP server, see
[mcpd](https://github.com/substructureai/mcpd).

```
curl -fsSL https://subs.dev/cli.sh | bash
```

The CLI is also the engine.

Create a `subs.toml`

.

```
name = "example"

[llm.openrouter]
type = "openrouter"

[agent.teammate]
llm = "openrouter"
model = "deepseek/deepseek-v4-flash-0731"
system = "You are a helpful teammate."
```

Set your provider key and talk to the agent.

```
export OPENROUTER_API_KEY=sk-or-...
subs chat teammate -c subs.toml
```

Add a `[serve]`

section and a `[remote]`

that points at it.

```
[serve]
port = 9999
auth = false

[remote]
url = "http://localhost:9999"
```

Start the server.

```
subs serve -c subs.toml
```

In another terminal, the same chat command now talks to it.

```
subs chat teammate -c subs.toml
```

Point `[remote]`

at the hosted engine instead of your own.

```
[remote]
url = "https://api.substructure.ai"
```

Create the project from the file, then upload your LLM key.

```
subs apply
subs auth llm.openrouter
```

The same chat command now runs the turn on the hosted engine.

```
subs chat teammate -c subs.toml
```

Say which agent takes a DM and which one answers a mention.

```
[slack]
dm = "teammate"
mentions = "teammate"
```

Apply the file again, then connect your workspace.

```
subs apply
subs slack connect
```

Mention the bot in a channel and it answers in the thread.

Declare the server and give it to an agent. Every user of the agent shares one credential.

```
[mcp.sentry]
url = "https://mcp.sentry.dev/mcp"

[agent.teammate]
llm = "openrouter"
model = "deepseek/deepseek-v4-flash-0731"
system = "You are a helpful teammate."
mcp = ["mcp.sentry"]
```

Authorize the connection.

```
subs auth mcp.sentry
```

Set `credential = "user"`

and each user connects their own account. A
user-scoped connection works only in a one-on-one chat between the agent and
that user.

```
[mcp.linear]
url = "https://mcp.linear.app/mcp"
credential = "user"

[agent.personal]
llm = "openrouter"
model = "deepseek/deepseek-v4-flash-0731"
system = "Help me with my Linear issues."
mcp = ["mcp.linear"]

[slack]
dm = "personal"
mentions = "teammate"
```

Point an agent at a URL and the engine sends every decision for that agent to your code. This gives you full control of the loop, including how the agent behaves in Slack.

```
[agent.teammate]
llm = "openrouter"
model = "deepseek/deepseek-v4-flash-0731"
system = "You are a helpful teammate."
mcp = ["mcp.sentry"]
worker = "https://example.com/agent"
```

Your endpoint reads the engine's proposal and returns it, changing only the steps you care about. There is no SDK to install.

``` python
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import type { DecisionRequest, DecisionResponse } from "./protocol.ts";

function decide({ trigger, proposed }: DecisionRequest): DecisionResponse {
    if (trigger.type === "session.start") {
        return {
            agent: {
                ...proposed.agent,
                tools: [{ name: "current_time", description: "Get the current time" }]
            }
        };
    }

    // Run our tool when the model calls it.
    if (trigger.type === "tool.execute" && trigger.name === "current_time") {
        return { actions: [{ type: "tool.result", result: new Date().toISOString() }] };
    }

    // Accept the engine's proposal for everything else.
    return proposed;
}

const app = new Hono();
app.post("/", async (c) => c.json(decide(await c.req.json())));

serve({ fetch: app.fetch, port: 4444 });
```

Only the agents that name a worker use one. The rest stay with the engine, in the same project and the same file.

Full walkthrough in the [quick start](/substructureai/subs/blob/main/docs/10-quick-start.md). Docs:
[Workers](/substructureai/subs/blob/main/docs/50-workers.md), [Connectors](/substructureai/subs/blob/main/docs/40-connectors.md),
[Slack](/substructureai/subs/blob/main/docs/130-slack.md), [Local development](/substructureai/subs/blob/main/docs/160-local-development.md)

Mention the bot or DM it. The thread is the session. Route different channels to different agents.

Docs: [Slack](/substructureai/subs/blob/main/docs/130-slack.md)

`subs chat`

holds one session open, streams the reply as it is written, and
turns an approval prompt into a picker. The session is the same kind a Slack
thread is.

Docs: [Chat](/substructureai/subs/blob/main/docs/135-chat.md)

Examples: [no-code-chat](/substructureai/subs/blob/main/examples/no-code-chat)

At each step the engine tells your code what it plans to do next. Accept the plan or do something else. A working agent is a few lines.

Docs: [How it works](/substructureai/subs/blob/main/docs/20-how-it-works.md)

Your agent is an HTTP endpoint. Generate typed bindings from the published JSON schema.

Docs: [Typed bindings](/substructureai/subs/blob/main/docs/270-typed-bindings.md)

Examples: [Go](/substructureai/subs/blob/main/examples/go-chat-with-tools), [Python](/substructureai/subs/blob/main/examples/python-fast-api-pydantic-chat-with-tools), [TypeScript](/substructureai/subs/blob/main/examples/node-hono-typescript-chat-with-tools), [Elixir](/substructureai/subs/blob/main/examples/elixir-plug-chat-with-tools)

Declare an MCP server and the engine handles the authorization, reads the tools it offers, and runs every call. Your code never holds a token.

Docs: [Connectors](/substructureai/subs/blob/main/docs/40-connectors.md)

Examples: [Node](/substructureai/subs/blob/main/examples/node-hono-connectors)

Point an agent at an [agent-plugins](https://agent-plugins.org) directory and it
gets that plugin's skills and MCP servers.

Docs: [Plugins](/substructureai/subs/blob/main/docs/45-plugins.md)

The engine calls Anthropic, OpenAI, or OpenRouter with your key. Or your worker makes the call and the engine never sees a key.

Docs: [LLMs](/substructureai/subs/blob/main/docs/70-llms.md)

Examples: [Anthropic](/substructureai/subs/blob/main/examples/node-hono-anthropic), [OpenAI](/substructureai/subs/blob/main/examples/node-hono-openai), [OpenRouter](/substructureai/subs/blob/main/examples/node-hono-openrouter)

Every step is saved before it runs. A run continues from where it stopped. The same message submitted twice runs once.

Docs: [Durability](/substructureai/subs/blob/main/docs/200-durability.md)

An agent can stop and wait for a person to approve, then continue. A waiting agent uses no compute. In Slack this is a button.

Docs: [Interrupts](/substructureai/subs/blob/main/docs/100-interrupts.md)

A tool does not have to answer immediately. Accept the call, do the work on your own schedule, and report the result later.

Docs: [Async tools](/substructureai/subs/blob/main/docs/110-async-tools.md)

History, editing, regeneration, and branching belong to the engine. A user can edit an earlier message and go a new direction. The original branch stays.

Docs: [Conversations](/substructureai/subs/blob/main/docs/120-conversations.md)

The engine streams AG-UI events, so assistant-ui and CopilotKit connect to it directly.

Docs: [AG-UI](/substructureai/subs/blob/main/docs/140-ag-ui.md)

Examples: [assistant-ui](/substructureai/subs/blob/main/examples/node-hono-assistant-ui), [CopilotKit](/substructureai/subs/blob/main/examples/node-hono-copilotkit)

A tool can run in the user's browser instead of on your server. The run waits for the browser, then continues.

Docs: [Client-side tools](/substructureai/subs/blob/main/docs/150-client-tools.md)

Examples: [Node](/substructureai/subs/blob/main/examples/node-hono-client-tool)

The engine stores your agent's state with the conversation. Your code gets it on every request and writes changes back.

Docs: [Agent state](/substructureai/subs/blob/main/docs/90-state.md)

An agent can give work to other agents. Each child runs in its own session. The parent's totals include each child's cost and token use.

Docs: [Subagents](/substructureai/subs/blob/main/docs/80-subagents.md)

Give a tool an input and output schema. The engine checks every call against it.

Docs: [Tool calls](/substructureai/subs/blob/main/docs/60-tools.md)

Set a policy on any tool or model call. The engine applies it, and keeps applying it after a restart.

Docs: [Retries and timeouts](/substructureai/subs/blob/main/docs/210-retries.md)

Run the engine on your own servers and hold every credential.

Docs: [Self-hosting](/substructureai/subs/blob/main/docs/180-self-hosting.md)

**Engine.** Runs the agent loop, in Rust. It calls the model, runs tools, saves each step, retries failures, streams events, and supervises subagents. Use the hosted version at[app.substructure.ai](https://app.substructure.ai), run it from the CLI, or embed it in your process.**Workers.** Your agent code. It receives a trigger and returns actions. It runs in your codebase with your dependencies.**Clients.** They send work and stream events back, from your backend or from the browser. Slack and AG-UI are clients.**CLI.** Set up, deploy, watch, and debug from the terminal. It also runs the engine locally.

```
curl -fsSL https://subs.dev/cli.sh | bash
```

The script verifies the release checksum and installs to `~/.local/bin`

. Set
`SUBS_INSTALL_DIR`

to install elsewhere and `SUBS_VERSION`

to pin a release. Or
install from npm:

```
npm i -g @substructure.ai/cli
```

Full documentation in [ docs/](/substructureai/subs/blob/main/docs).
