Build production-ready AI agents in any language with no SDK.
Pre-1.0: APIs and the wire protocol might change between releases.
Substructure runs the agent loop for you. It calls the model, runs the tools, saves every step, and streams events to your frontend.
You declare an agent in one file. When you need your own code in the loop, point the agent at an HTTP endpoint that you own. Everything is HTTP, so you can write that endpoint in any language. There is no SDK to install.
Run it on the hosted cloud, on your machine, or on your own servers. The same file describes all three.
curl -fsSL https://subs.dev/cli.sh | bash
subs loginDeclare the agent.
name = "oncall-bot"
[llm.openrouter]
type = "openrouter"
[agent.oncall]
llm = "openrouter"
model = "anthropic/claude-sonnet-4-5"
system = "You are the on-call assistant."
[slack]
dm = "oncall"
mentions = "oncall"
[remote]
url = "https://api.substructure.ai"[remote] says the file describes a deployment. Ship it.
subs apply
subs llm set-key openrouter
subs slack connectMention the bot in a channel and it answers in the thread. You wrote one file and no code.
Full walkthrough in the quick start.
Add a worker to any agent. The engine then asks your endpoint before each step
of a turn.
[agent.oncall]
llm = "openrouter"
model = "anthropic/claude-sonnet-4-5"
worker = "http://localhost:4444"The engine proposes each step. Return the proposal to accept it. Change the ones you care about.
// A complete worker, on Node's built-in http server. No dependencies.
import { createServer } from "node:http";
function decide({ trigger, proposed }) {
// Run the tool when the model calls it.
if (trigger.type === "tool.execute" && trigger.name === "get_time") {
return { actions: [{ type: "tool.result", result: new Date().toISOString() }] };
}
return proposed;
}
const server = createServer((req, res) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify(decide(JSON.parse(body))));
});
});
server.listen(4444);Only the agents that name a worker use one. The rest stay with the engine, in the same project and the same file.
Docs: Workers
Run the engine on your machine and iterate before you deploy. A file with no
[remote] runs the turn here, on your own key.
export OPENROUTER_API_KEY=sk-or-...
subs run --agent oncall -o pretty "what is broken?"The reply streams to your terminal. subs serve runs the same engine as an HTTP
server, with the AG-UI and REST endpoints a frontend needs.
Slack, MCP connections, and workers all work locally.
Docs: Local development
- In an existing codebase. Your agent is an HTTP endpoint. Tools are functions you already have, with your database and your permissions.
- In a new project. The engine handles sessions, history, streaming, and the client API. You write the agent and its tools.
- Long-running work. A tool can be a background job. A request for approval can wait for days.
- Durability. Every step is saved before it runs. A deploy, a crash, or a reconnect loses nothing.
Mention the bot or DM it. The thread is the session. Route different channels to different agents.
Docs: Slack
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
Your agent is an HTTP endpoint. Generate typed bindings from the published JSON schema.
Docs: Typed bindings
Examples: Go, Python, TypeScript, Elixir
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
Examples: Node
Point an agent at an agent-plugins directory and it gets that plugin's skills and MCP servers.
Docs: Plugins
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
Examples: Anthropic, OpenAI, OpenRouter
Every step is saved before it runs. A run continues from where it stopped. The same message submitted twice runs once.
Docs: Durability
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
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
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
The engine streams AG-UI events, so assistant-ui and CopilotKit connect to it directly.
Docs: AG-UI
Examples: assistant-ui, 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
Examples: Node
The engine stores your agent's state with the conversation. Your code gets it on every request and writes changes back.
Docs: Agent state
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: Sub-agents
Give a tool an input and output schema. The engine checks every call against it.
Docs: Tool calls
Set a policy on any tool or model call. The engine applies it, and keeps applying it after a restart.
Docs: Retries and timeouts
Run the engine on your own servers and hold every credential.
Docs: Self-hosting
- Engine. Runs the agent loop, in Rust. It calls the model, runs tools, saves each step, retries failures, streams events, and supervises sub-agents. Use the hosted version at 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 | bashVerifies the release checksum and installs to ~/.local/bin
(SUBS_INSTALL_DIR to move it, SUBS_VERSION to pin a tag). Or from npm:
npm i -g @substructure.ai/cliFull documentation in docs/.
Functional Source License 1.1, converting to Apache 2.0.