subs, the cloud native agent harness.
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 can run locally or as a client and server.
substructure.ai builds subs and offers a hosted deployment.
Run a local agent.
Create a subs.toml.
subs.tomlname = "example" [llm.openrouter] type = "openrouter" [agent.teammate] llm = "openrouter" model = "deepseek/deepseek-v4-flash-0731" system = "You are a helpful teammate."Talk to it.
Use server/client mode.
Add this to subs.toml.
subs.tomlname = "example"[llm.openrouter]type = "openrouter"[agent.teammate]llm = "openrouter"model = "deepseek/deepseek-v4-flash-0731"system = "You are a helpful teammate."[serve]port = 9999auth = false[remote]url = "http://localhost:9999"Start the server.
In another terminal, the same command now talks to it.
Run it on substructure.ai
Point the same file at the hosted engine instead of your own.
subs.tomlname = "example"[llm.openrouter]type = "openrouter"[agent.teammate]llm = "openrouter"model = "deepseek/deepseek-v4-flash-0731"system = "You are a helpful teammate."[remote]url = "https://api.substructure.ai"Create the project from the file, then give it your LLM key.
The same command as before. The turn runs on our machines now.
Put the agent in Slack.
Three lines: which agent takes a DM, and which answers a mention.
subs.tomlname = "example"[llm.openrouter]type = "openrouter"[agent.teammate]llm = "openrouter"model = "deepseek/deepseek-v4-flash-0731"system = "You are a helpful teammate."[agent.teammate.slack]name = "Teammate"[remote]url = "https://api.substructure.ai"Apply it again, then set up its Slack app.
Mention the bot in a channel and it answers in the thread. You wrote one file and no code.
Connect the agent to an MCP server
Add the server. Give it to an agent. The credential is shared amongst all your users.
subs.tomlname = "example"[llm.openrouter]type = "openrouter"[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"][agent.teammate.slack]name = "Teammate"[remote]url = "https://api.substructure.ai"Authorize the MCP.
Scope MCP connections to each user
Mark a connection per-user. Each user connects their own account. User scoped MCP connections restricted to one-on-one agent and user chat contexts.
subs.tomlname = "example"[llm.openrouter]type = "openrouter"[mcp.sentry]url = "https://mcp.sentry.dev/mcp"[mcp.linear]url = "https://mcp.linear.app/mcp"credential = "user"[agent.teammate]llm = "openrouter"model = "deepseek/deepseek-v4-flash-0731"system = "You are a helpful teammate."mcp = ["sentry"][agent.teammate.slack]name = "Teammate"[agent.personal]llm = "openrouter"model = "deepseek/deepseek-v4-flash-0731"system = "Help me with my Linear issues."mcp = ["mcp.linear"][agent.personal.slack]name = "Personal"answers = "dm"[remote]url = "https://api.substructure.ai"Control the agent loop with webhooks.
Configure the engine to send a webhook to your system and take complete control over the agent loop and Slack agent behavior.
Point an agent at a URL that implements the substructure webhook.
subs.tomlname = "example"[llm.openrouter]type = "openrouter"[mcp.sentry]url = "https://mcp.sentry.dev/mcp"[mcp.linear]url = "https://mcp.linear.app/mcp"credential = "user"[worker.main]url = "https://example.com/agent"[agent.teammate]llm = "openrouter"model = "deepseek/deepseek-v4-flash-0731"system = "You are a helpful teammate."mcp = ["sentry"]worker = "main"[agent.teammate.slack]name = "Teammate"[agent.personal]llm = "openrouter"model = "deepseek/deepseek-v4-flash-0731"system = "Help me with my Linear issues."mcp = ["linear"][agent.personal.slack]name = "Personal"answers = "dm"You handle a webhook, customizing just the steps you care about. You don't even need an SDK.
server.tsimport { 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 });Bring your own LLM.
Bring your own key and the engine makes requests on your behalf. If you don't want to put your key in the engine, you can make calls in a webhook worker.
- Anthropic
- OpenAI
- OpenRouter
Everything else your agents can do.
Slack
Mention the bot or DM it. The thread is the session. Route different channels to different agents.
Workers
At each step the engine tells your code what it plans to do next. Accept the plan or do something else.
Any LLM
The engine calls Anthropic, OpenAI, or OpenRouter with your key. Or your worker makes the call and the engine never sees a key.
CLI
Set up, deploy, watch, and debug from the terminal. It also runs the engine locally.
MCP connectors
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.
Plugins
Point an agent at an agent-plugins directory and it gets that plugin's skills and MCP servers.
AG-UI
The engine streams AG-UI events, so assistant-ui and CopilotKit connect to it directly.
Any language
Your agent is an HTTP endpoint. Generate typed bindings from the published JSON schema.
Subagents
An agent gives work to other agents. Each child runs in its own session, and the parent's totals include its cost.
Human approval
An agent can stop and wait for a person, then continue. A waiting agent uses no compute. In Slack it is a button.
Tool calls
Give a tool an input and output schema. The engine checks every call against it.
Tools that take hours
A tool does not have to answer immediately. Accept the call, do the work on your own schedule, report the result later.
Durability
Every step is saved before it runs. A deploy, a crash, or a reconnect loses nothing.
Conversations
History, editing, regeneration, and branching belong to the engine. Edit an earlier message and go a new direction; the original branch stays.
Host it yourself
Run the engine on your own servers and hold every credential.