Getting started
soloagent is one Node.js program. Run it on your laptop to try it, then put the same thing on a server when your team should reach it from Slack, Discord, or Telegram. Pick whichever install fits:
Install on your machine
With Node 22.19 or newer, the installer puts the soloagent command on your path:
curl -fsSL https://soloagent.dev/install.sh | shIt downloads the latest release and installs it with npm, so you can also do that yourself, on any OS including Windows, or try it once without installing. --allow-remote=root lets npm 12 install from a release URL; older npm versions ignore it:
# install
npm install -g --allow-remote=root https://github.com/nearbycoder/soloagent.dev/releases/latest/download/soloagent.tgz
# or run once, nothing installed
npx -y --allow-remote=root https://github.com/nearbycoder/soloagent.dev/releases/latest/download/soloagent.tgzOn macOS or Linux with Homebrew, which brings its own Node:
brew install nearbycoder/soloagent/soloagentNo Node? Use Docker, one command, any OS:
docker run -d --name soloagent -p 7420:7420 -v soloagent:/data ghcr.io/nearbycoder/soloagent.devThen open http://localhost:7420 and paste the token from docker logs soloagent.
First run
# the terminal UI, with a private server and data in ~/.soloagent
soloagent
# or the web dashboard on http://localhost:7420
soloagent serve
soloagent token # paste this into the dashboardA fresh install has one agent and the built-in demo model, so everything works before you connect an account. Then sign in to a real model, for example your Claude Pro or Max subscription, with /login anthropic in the TUI, soloagent login anthropic in your shell, or the Providers page of the dashboard.
# one-shot answers for scripts; piped input is added after the message
git diff --staged | soloagent ask "Write a commit message for this diff"
# point the TUI at a server you deployed
soloagent connect https://solo.example.com --token $SOLOAGENT_TOKENDeploy
soloagent needs three things from a host: a long-running process (chat connectors hold open connections and durable timers need a clock), a persistent directory for its two SQLite databases, and, for the dashboard and webhooks, an HTTP port. It runs as a single instance; don't scale it horizontally.
| Where | How | Good for |
|---|---|---|
| Docker | docker compose up -d | Any VPS, home server, or NAS; Coolify, Dokploy, CapRover, Portainer |
| Railway | New project from the GitHub repo | The fewest steps on a managed host |
| Render | One-click Blueprint | Managed host with a persistent disk |
| Fly.io | fly launch + fly deploy | Managed host close to your users |
| Linux server | npm + systemd | A plain server without Docker |
Docker
The image ghcr.io/nearbycoder/soloagent.dev is built for amd64 and arm64 (so Apple silicon, Raspberry Pi, and Graviton work too). The repository's docker-compose.yml is all you need:
curl -fsSLO https://raw.githubusercontent.com/nearbycoder/soloagent.dev/main/docker-compose.yml
docker compose up -d
docker compose logs soloagent | grep tokenData lives in the compose file's soloagent-data volume. To choose your own token and secret, put them in a .env file next to the compose file (see .env.example). Update with docker compose pull && docker compose up -d. Self-hosted platforms such as Coolify and Dokploy can deploy the same compose file from the repository.
Put a reverse proxy in front for HTTPS on a public server. With Caddy it is one line: solo.example.com { reverse_proxy localhost:7420 }.
Railway
Create the service
At railway.com/new, choose GitHub Repository and pick your fork, or paste
https://github.com/nearbycoder/soloagent.devand choose Deploy Repo. The Dockerfile andrailway.jsonare picked up automatically.Add a volume at
/datasoloagent uses the volume as its data directory automatically.
Set variables and a domain
Set
SOLOAGENT_TOKENandSOLOAGENT_SECRET, generate a domain, and open it.
Render
The repository includes a render.yaml Blueprint: one Docker web service with a 1 GB persistent disk at /data, a health check, and a generated token and secret. Disks need a paid instance (Starter or above).
After the deploy, read SOLOAGENT_TOKEN from the service's Environment tab and open the onrender.com URL.
Fly.io
The included fly.toml runs one always-on machine with a volume mounted at /data:
git clone https://github.com/nearbycoder/soloagent.dev && cd soloagent.dev
fly launch --copy-config --no-deploy
fly volumes create soloagent_data --size 1
fly secrets set SOLOAGENT_TOKEN=$(openssl rand -base64 32) SOLOAGENT_SECRET=$(openssl rand -base64 32)
fly deployCreate the volume in the same region as the app. Auto-stop is off on purpose: Slack, Discord, and Telegram connections and scheduled reminders need the process running.
Linux server with systemd
sudo useradd --system --create-home --home-dir /var/lib/soloagent soloagent
sudo npm install -g --allow-remote=root https://github.com/nearbycoder/soloagent.dev/releases/latest/download/soloagent.tgz
sudo curl -fsSLo /etc/systemd/system/soloagent.service https://raw.githubusercontent.com/nearbycoder/soloagent.dev/main/deploy/soloagent.service
sudo systemctl enable --now soloagent
sudo journalctl -u soloagent | grep tokenOptional settings go in /etc/soloagent.env (for example SOLOAGENT_TOKEN=…).
Configuration
| Variable | What it does |
|---|---|
SOLOAGENT_TOKEN | Admin token for the dashboard, TUI, and API. If unset, one is generated on first boot, stored, and printed in the logs. |
SOLOAGENT_SECRET | Seals provider credentials and connector tokens at rest. If unset, one is generated and kept in the data directory. Keep it stable: credentials sealed with one secret can't be opened with another. |
SOLOAGENT_DATA_DIR | Where data lives. The Docker image uses /data, so mount your volume there. soloagent serve defaults to ~/.soloagent/data; --data overrides it. |
PORT | HTTP port, default 7420. Most hosts set it for you. soloagent serve --port overrides it. |
SOLOAGENT_DEMO | 0 hides the built-in demo model. |
SOLOAGENT_ALLOW_PRIVATE_FILE_URLS | 1 lets attachment URLs point at loopback and private network addresses. Off by default, so webhook callers can't make the server fetch internal services. See Files & images. |
SOLOAGENT_PUBLIC_URL | The public address printed at startup. Railway's domain is detected automatically. |
SOLOAGENT_BACKUP_INTERVAL | Take a backup on a timer, for example 24h or 6h. Off by default. See Backups. |
SOLOAGENT_BACKUP_KEEP | How many scheduled backups to keep, default 7. |
SOLOAGENT_UPDATE_CHECK | 0 turns off the check for new releases. |
The server prints its URL and token when it starts:
soloagent is up — https://solo.example.com
data /data
token solo_••••••••••••••••••••••••
terminal soloagent connect https://solo.example.com --token <token>Where it doesn't fit
Serverless and edge platforms (Vercel functions, Netlify, Cloudflare Workers, AWS Lambda) and hosts without persistent disks (Heroku's dynos, DigitalOcean App Platform) can't run soloagent: they stop the process between requests and discard its files. Use one of the options above, or any VM.
Providers & subscriptions
soloagent reaches models through @earendil-works/pi-ai. You can sign in with a consumer subscription (or an OpenRouter account) over OAuth, or paste an API key. Each agent picks its own provider and model.
Sign in with your account
Open Providers in the dashboard and choose Sign in next to one of:
The provider's sign-in page opens in a new tab. When the server runs on your own machine, the login completes on its own.
localhost address after sign-in, which can't reach a server running somewhere else. When that happens, the page in your browser may fail to load. That's expected: copy the code shown by the provider, or the full URL from the address bar, and paste it into the prompt in the dashboard.
From the terminal, /login <provider> in the TUI and soloagent login <provider> in your shell run the same flow.
API keys
Paste a key under Providers for any of 40+ providers, including Anthropic, OpenAI, Google Gemini, Mistral, Groq, DeepSeek, xAI, OpenRouter, Vercel AI Gateway, Amazon Bedrock, Azure, Together, Fireworks, Cerebras, and Hugging Face. Over the API, send { "key": "…" } to PUT /api/providers/:id/key, or run soloagent login <provider> --key.
Keys and OAuth tokens are sealed with AES-256-GCM before they're written to disk. See Security.
Subscription sign-in is subject to each provider's terms. Product names are trademarks of their respective owners.
Agents & skills
An agent is a named assistant with its own personality, model, and abilities. Create and edit them under Agents in the dashboard, or with POST /api/agents.
| Field | Description |
|---|---|
| Name & @handle | The handle is how you address it in chat: @coder. Lowercase letters, digits, and dashes. |
| Avatar & color | A single glyph or emoji and an accent color, shown in the dashboard and TUI. |
| Instructions | The system prompt: who it is, how it should behave, what it knows. |
| Model | Any connected provider and model. Different agents can use different providers. |
| Thinking level | off, minimal, low, medium, high, or xhigh on models that support reasoning. |
| Skills | The tool sets it may use (below). New agents get every skill except Workspace. |
| Max turns | The most tool rounds one run may take, 40 by default. |
| Approvals | Which tool calls wait for a person first: off, risky, or all. See Approvals. |
| Default | The default agent answers anything not addressed to a specific agent. |
Skills
Long-term notes about people, preferences, and facts, kept per agent across every conversation.
rememberrecallforgetFetch and read web pages and APIs, and search the web through DuckDuckGo.
web_fetchweb_searchHand a sub-task to another agent and wait for its answer. The hand-off runs as a durable child conversation, which can't delegate further.
ask_agentDurable reminders, follow-ups, and recurring automations. They sleep in storage and fire even after restarts.
schedule_reminderschedule_recurringlist_automationscancel_automationRead, write, and edit files and run shell commands on the server, starting in a per-agent workspace folder. It is not a sandbox: commands run with the server's permissions. Only enable it for agents that trusted people can reach. Agents with this skill ask for approval before bash, write, and edit unless you turn approvals off.
readwriteeditbashAgents can also get tools from MCP servers you connect. See Tools (MCP).
Memory
With the Memory skill, an agent saves durable notes with remember, searches them with recall, and deletes them with forget. The newest 40 are part of its prompt in every conversation. To see and correct what an agent remembers, open the agent and choose the Memory tab: search, add, edit, or delete notes, or clear them all. Agent cards show how many memories each agent has.
The same is available over the API at /api/agents/:id/memories (see Routes). Clearing every memory needs ?confirm=all, so a stray request can't wipe them.
Tools (MCP)
Connect Model Context Protocol servers (GitHub, Linear, Postgres, Sentry, a filesystem, anything that speaks MCP) and give their tools to the agents you choose. Add servers under Tools in the dashboard, then turn them on per agent under Tools from MCP servers in the agent editor.
| Field | Description |
|---|---|
| ID | A short slug such as github. Tools are named mcp__<id>__<tool>, e.g. mcp__github__create_issue. It can't be changed later. |
| Transport | stdio runs a command on the soloagent server and talks over its stdin and stdout. HTTP connects to a remote server's Streamable HTTP endpoint. The older HTTP+SSE transport isn't supported. |
| Command, arguments, env | stdio only. The program, its arguments (one per line in the dashboard), and environment variables such as API tokens. |
| URL, headers | HTTP only. The endpoint and request headers, usually Authorization: Bearer …. |
| Enabled | Turn off to disconnect without losing settings. |
Examples
GitHub's official server over stdio. It ships as a Docker image, so Docker must be available where soloagent runs. It reads a personal access token from its environment:
curl -X POST https://solo.example.com/api/mcp-servers \
-H "authorization: Bearer $SOLOAGENT_TOKEN" -H "content-type: application/json" \
-d '{
"id": "github",
"name": "GitHub",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "github_pat_…" }
}'Servers published to npm run with npx, which is also available in the soloagent Docker image. The reference filesystem server, limited to one folder:
{
"id": "files",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data/shared"]
}A remote server over HTTP, here GitHub's hosted one:
{
"id": "github",
"transport": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": { "Authorization": "Bearer github_pat_…" }
}Then give the tools to an agent in its editor, or with PATCH /api/agents/:id and { "mcpServers": ["github"] }.
How it works
- Connections. soloagent connects to every enabled server at startup and whenever you change one. If a server fails or drops, it shows Error with the reason and reconnects with backoff, from 2 seconds up to 5 minutes. A broken server never affects the rest of soloagent.
- Test. The Test button, or
POST /api/mcp-servers/:id/test, connects if needed and lists the server's tools live. - Tool changes. Tools appear for agents as soon as the server connects, and follow the server when its tool list changes. No restart or agent edit needed.
- Large servers. When a server has more than 15 tools, they aren't offered to the model up front. The agent gets a
tool_searchtool instead, searches for what it needs, and the matches are loaded for the rest of the conversation. - Results. Text and images from tool results go to the model. A result the server marks as an error is reported to the agent as one.
- Not replayed. MCP calls can change things, so a call cut off by a crash or restart doesn't run again. The agent is told it was interrupted and can decide what to do.
PATH, HOME, locale, proxy, and Docker settings plus the variables you set, but never soloagent's own environment, such as SOLOAGENT_TOKEN or SOLOAGENT_SECRET. Only add servers you trust, and remember that anyone who can talk to an agent can use its tools.
Approvals
An agent can pause before a tool call until a person approves it. You can approve or deny in the dashboard, in the TUI, or with buttons in the Slack, Discord, or Telegram thread where the agent works. The wait is durable: it survives restarts and deploys, and an approved call runs exactly once.
Policies
Set the policy per agent under Agents → Approvals, or with the agent's approvals field.
| Policy | Asks before |
|---|---|
off | Nothing. Tools run at once. The default for agents without the Workspace skill. |
risky | bash, write, edit, and the tools on the risky list. The default for agents with the Workspace skill. |
all | Every tool call except read-only ones: read, recall, and web_search. |
Two settings apply to every agent. Change them under Approvals → Settings or with PUT /api/approval-settings:
- Timeout, 24 hours by default. A request nobody answers in time is denied.
- More risky tools: extra tool names the
riskypolicy asks about, such asforgetorschedule_reminder. End a name with*to match a prefix:mcp__github__*covers every tool of that MCP server. Theallpolicy already asks before MCP tools.
When a call is denied, the agent gets an error result such as Denied by ann: not on prod and carries on, usually by explaining or trying another way. Stopping the run cancels the request.
Where to approve
- Dashboard. The tool card in the thread shows Waiting for approval with the arguments and Approve / Deny buttons. Deny takes an optional reason, which the agent sees. Deciding is for admins; members see the request and who it waits on. The Approvals page lists every waiting request across threads, with a count in the sidebar, and the recent decisions.
- Chat apps. In Slack, Discord, and Telegram threads, the agent posts a message with Approve and Deny buttons. Once someone decides, the message changes to show the outcome and who decided.
- TUI. The tool line shows waiting for approval.
/approveapproves the current thread's waiting call;/deny [reason]denies it. - API.
GET /api/approvals?status=pending, thenPOST /api/approvals/:id/approveorPOST /api/approvals/:id/denywith{ "reason": "…" }. Both take an optionalby, the name recorded as the decider (defaultdashboard).
Chat setup
Only the people you list may press the buttons. In Connections, open the connector and fill in Approvers with platform user ids or display names, comma-separated. With an empty list, approvals happen only in the dashboard; anyone who presses a button is told so privately.
- Slack delivers button presses over Socket Mode, but only when the app has Interactivity on. The manifest below enables it (
interactivity.is_enabled: true). For an app made from an older manifest, turn it on under Interactivity & Shortcuts; no request URL is needed. User ids look likeU012ABCDEF(profile → Copy member ID). - Discord needs no setup. Copy a user id with Developer Mode on (right-click → Copy User ID).
- Telegram needs no setup. Display names are first and last name as Telegram shows them; numeric user ids work too.
- Webhook connectors have no buttons. Approve their requests in the dashboard or through the API.
Connectors
Connectors put your agents into chat apps. Add them under Connections in the dashboard, which includes these setup guides. Each connector can be bound to an agent; otherwise the default agent answers. In chat apps, replies stream live by editing a placeholder message as the answer arrives.
Slack
Slack uses Socket Mode, so no public URL is needed. It works on any server and on your laptop alike.
Create the app from a manifest
Go to api.slack.com/apps → Create New App → From a manifest, pick your workspace, and paste the manifest below.
Create an app-level token
Basic Information → App-Level Tokens → Generate with the
connections:writescope. Copy thexapp-…token.Install and copy the bot token
Install App to your workspace and copy the Bot User OAuth Token (
xoxb-…).Connect and invite
Paste both tokens into a new Slack connector. Invite the bot to a channel with
/invite @Solo, then mention it. DMs work too.
The manifest turns on Interactivity, which the approval buttons need. Socket Mode delivers the clicks, so no request URL is needed.
display_information:
name: Solo
description: Your agents, on soloagent.dev
background_color: "#15140f"
features:
app_home:
messages_tab_enabled: true
messages_tab_read_only_enabled: false
bot_user:
display_name: Solo
always_online: true
oauth_config:
scopes:
bot:
- app_mentions:read
- chat:write
- channels:read
- files:read
- groups:read
- im:history
- im:read
- im:write
- users:read
settings:
event_subscriptions:
bot_events:
- app_mention
- message.im
interactivity:
is_enabled: true
org_deploy_enabled: false
socket_mode_enabled: true
token_rotation_enabled: falseThe files:read scope lets agents see files and images people share. If you created the app before this scope was in the manifest, add files:read under OAuth & Permissions → Scopes and reinstall the app; until then, shared files are skipped.
Discord
Create an application
Open the Discord Developer Portal → New Application.
Get the bot token
On the Bot tab, click Reset Token and copy it. Turn on Message Content Intent under Privileged Gateway Intents.
Invite the bot
OAuth2 → URL Generator: select the
botscope and the permissions Send Messages, Send Messages in Threads, Read Message History, and View Channels. Open the generated URL to add the bot to your server.Connect
Paste the token into a new Discord connector. Mention the bot, reply to it, or DM it. Each channel, thread, and DM is its own conversation.
Telegram
Create a bot
Message @BotFather on Telegram and send
/newbot. Pick a name and username; BotFather replies with a token.Connect
Paste the token into a new Telegram connector, then message your bot.
Groups and topics
Add the bot to a group and mention it or reply to it. Forum topics each get their own conversation. Use
/setprivacyin BotFather to control what the bot can see.
Webhook
Wire agents into anything that can send an HTTP request. Create a Webhook connector with a shared secret; callers send it in the x-solo-secret header.
curl -X POST https://solo.example.com/api/hooks/<connector-id> \
-H 'x-solo-secret: YOUR_SECRET' \
-H 'content-type: application/json' \
-d '{"text": "@coder what changed in the last deploy?", "thread": "deploys", "user": "ci"}'| Body field | Meaning |
|---|---|
text | The message. Routing works as in chat, so @coder … picks an agent. |
thread | Keeps a conversation going across requests. Omit it for a one-off. |
user | A display name for the sender. |
attachments | Optional files: [{ "name", "mimeType", "data_base64" }] or [{ "name", "mimeType", "url" }]. text may be empty when there are attachments. See Files & images. |
callback_url | Optional. Without it, the request waits and returns { "thread", "status", "answer" }. With it, the request returns 202 with { "accepted": true, "thread" } right away, and the answer is POSTed to that URL as { "thread", "text" }, with the same x-solo-secret header so you can check it came from soloagent. |
Chat routing & commands
Every inbound message, from any connector, is routed to one agent:
- If it starts with
@handleorhandle:, that agent answers.@coder fix the buildandcoder: fix the buildare the same. - Otherwise, the agent bound to the connector answers.
- Otherwise, the default agent answers.
Addressing applies to that one message, and each agent keeps its own conversation in a chat thread.
Steering
If you send another message while an agent is still working, it steers the run in progress: the agent sees your new message after its current step (a model reply or a round of tool calls) and adjusts, rather than finishing the whole task first and answering later.
Chat commands
| Command | What it does |
|---|---|
!agents | List the agents you can talk to and their handles. |
!reset | Start a fresh conversation in this thread or DM. !new does the same. |
!stop | Stop the agent's current run. |
!help | Show help for routing and commands. |
Teams & access
By default anyone in a connected Slack, Discord, or Telegram can talk to any agent, and the dashboard has one admin token. For a team, limit who can reach which agents, cap what they cost, and give people their own sign-in.
Access lists
Each connector has three lists, edited under Access in its settings. An empty list means everyone, everywhere, or every agent.
| List | Matches |
|---|---|
| People | The platform user id (U024BE7LH) or display name (ann), ignoring case. |
| Channels | The channel id or name, such as #eng. A Discord thread matches its parent channel. When this list is set, a DM is allowed only if the People list names the sender. |
| Agents | Agent handles or ids people can reach through this connector, whether by @handle or as the connector's default. |
Someone who isn't allowed gets one short reply, "You don't have access to @x here. Ask an admin.", at most once every ten minutes, and the attempt is logged in Activity. Nothing reaches the model. !agents and !help list only the agents reachable on that connector.
Webhook connectors are authenticated by their secret, so only the Agents list applies to them. A denied webhook call answers 403.
Workspace safety
An agent with the Workspace skill can run shell commands on your server, so Slack, Discord, and Telegram can't reach it unless the agent's Reachable from chat apps setting (chatAccess) is on. It is off by default. The dashboard, TUI, API, and webhooks still reach it. If you turn it on, use the connector's access lists to name the people who may use it.
Budgets & rate limits
| Setting | Where | What it limits |
|---|---|---|
| Daily spend | Agent, budget.dailyUsd | USD spent by the agent today, at list API prices. |
| Daily messages | Agent, budget.dailyRuns | Messages the agent accepted today. |
| Messages per person per hour | Connector, limits.perUserPerHour | Messages one person may send through the connector in a rolling hour. |
Empty or null means no limit. Days are UTC. Limits are checked before a message is sent to the model: over a limit, chat users get a short reply, and the API and dashboard get 429 with a message. Each is logged in Activity. A run that's already going finishes, so a busy agent can end slightly over its spend budget.
Spend is recorded for every provider response, including a delegate's, with its agent, connector, thread, model, tokens, and cost. Agent cards show today's spend against the budget, and Usage shows the last 14 days. Subscription logins that don't report a price are counted at list prices.
Users & roles
Invite people from Team in the dashboard. An invite is a one-time link, /invite/<code>, that expires after 7 days; the invitee picks a name and password and is signed in. Afterwards they sign in with Team member on the sign-in page. Passwords are hashed with scrypt.
| Role | Can |
|---|---|
| Member | Chat, view agents and conversations, see usage and activity. |
| Admin | Everything a member can, plus approve or deny tool calls, and manage providers, connectors, MCP servers, agents, automations, the team, and the admin token. |
The server enforces roles on every route; members get 403 on admin routes. Messages a signed-in person sends carry their name. Admins can change roles, remove people (which signs them out everywhere), and revoke unused invites.
The admin token stays the superuser, for the CLI, the TUI, and scripts. Rotate it from Settings; the new token is shown once, and the old one stops working. If you set SOLOAGENT_TOKEN, rotate it by changing the variable and restarting.
Files & images
People can share files and images with agents from Slack, Discord, Telegram, webhooks, the dashboard (paste, drag and drop, or the paperclip), and the CLI (soloagent ask "describe this" --file ./screenshot.png). Each file becomes part of the message the agent sees:
| File | What the agent gets |
|---|---|
| Images: PNG, JPEG, GIF, WebP; BMP and TIFF are converted | The image itself, resized to fit the model's limits (by default 2000×2000 pixels and about 4.5 MB). Models without image input see a placeholder instead. HEIC and other formats the image processor can't decode get a note. |
| Text: plain text, Markdown, CSV, JSON, YAML, XML, SVG, logs, and source code | The contents in a fenced block with the file name. Up to 100 KB per file and 200 KB per message; longer files are cut, with a note saying so. |
| A note with the name, type, and size. soloagent doesn't extract PDF text. | |
| Anything else | A note with the name, type, and size. |
Agents with the Workspace skill also get every file in attachments/ in their workspace, and notes point there, so they can work on PDFs, archives, and long files with their tools.
Limits
Files can be up to 20 MB, with up to 10 per message. Downloads from chat platforms and URLs time out after 30 seconds. Telegram bots can only download files up to 20 MB. Attachment URLs must be http or https and may not point at loopback or private network addresses unless SOLOAGENT_ALLOW_PRIVATE_FILE_URLS=1 is set.
Sending files over the API
POST /api/threads/:id/messages, /ask, and webhooks take an attachments array of { name, mimeType, data_base64 } or { name, mimeType, url }. mimeType is optional; it's guessed from the file name. The SDK passes them through: solo.send(threadId, "what's wrong here?", { attachments }).
Slack
Slack needs the files:read scope, which is in the manifest above. Apps created before it was added must add the scope and be reinstalled.
Where files are stored
Original files are kept on your server's disk under <data dir>/files/, named by their SHA-256 hash, so a file shared twice is stored once. Messages keep only each file's name, type, size, and hash. Images go to the model at request time and aren't copied into the conversation database. Files are never deleted automatically; deleting a thread leaves its files in place. They're readable only with the admin token, through GET /api/files/:sha256, and they're sent to your model provider along with the message, like any other message content.
Automations
An automation is a prompt that runs on a schedule: “every weekday at 9:00, post a standup summary in #eng”, “every Monday, summarize last week's deploys”, “every hour, check the status page and alert if anything is down”. Reminders fire once; automations repeat.
Create them under Automations in the dashboard, with POST /api/automations, or by asking an agent that has the Schedule skill. In Slack:
@solo every weekday at 9 post the open incidents hereThe agent calls schedule_recurring. It can also list_automations and cancel_automation.
Schedules
A schedule is a 5-field cron expression read in an IANA time zone (the server's by default). The dashboard also offers simple presets, which are stored as cron:
| Preset or phrase | Cron |
|---|---|
| every 15 minutes | */15 * * * * |
| every 2 hours | 0 */2 * * * |
| daily at 17:30 | 30 17 * * * |
| weekdays at 9:00 | 0 9 * * 1-5 |
| mondays at 9:30 | 30 9 * * 1 |
The fields are minute, hour, day of month, month, and day of week. Each takes *, numbers, lists (1,15), ranges (1-5), and steps (*/10); months and days also take names (jan, mon-fri). As in classic cron, when both day fields are set, a day matching either one fires. @hourly, @daily, @weekly, and @monthly work too.
Daylight saving time. A time skipped by a spring-forward jump (02:30 on that night in most of the US) fires once, at the moment of the jump. A time repeated by a fall-back fires once, on its first occurrence.
Where runs land
- A thread. Pick any existing thread, web or chat. Each run is a message from
Automation: <name>in that thread, answered by the thread's agent. In a chat thread the answer is posted to Slack, Discord, or Telegram like any other reply. - A chat channel. Pick a connection and a channel id (Slack channel id, Discord channel id, Telegram chat id, or a webhook callback URL). Each run posts a new top-level message in the channel; on Slack, replies to it start a normal thread with the agent. The automation keeps one conversation of its own, so the agent remembers earlier runs (“since yesterday…”).
Catch up once
Each enabled automation is a durable task that sleeps until its next run. If the server is down when a run is due, that run fires once when the server starts again, and the schedule continues from then. Missed runs are not replayed one by one: an hourly automation that was down for a day runs once on startup, not 24 times.
Disabling, editing, or deleting an automation cancels or reschedules its task immediately. Run now runs it once outside its schedule, even when it is disabled. Runs use the same path as a chat message, so the agent's model, skills, and limits apply.
API
curl https://solo.example.com/api/automations \
-H "authorization: Bearer $SOLOAGENT_TOKEN" -H "content-type: application/json" \
-d '{
"name": "Standup summary",
"prompt": "Summarize the PRs merged since the last run and the open incidents.",
"preset": { "kind": "weekdays", "time": "09:00" },
"timezone": "America/Chicago",
"target": { "kind": "channel", "connectorId": "sla_…", "channel": "C0123456789", "location": "#eng" }
}'Give cron (an expression or a phrase like "weekdays at 9:00") or preset. A thread target is { "kind": "thread", "threadId": "thr_…" }. List them from the terminal with soloagent automations.
Terminal UI
soloagent opens a full-screen terminal UI built on pi-tui. With no arguments it uses a local server, starting one in-process if soloagent serve isn't already running; after soloagent connect <url> --token … it talks to that remote server instead. It streams answers live and shows tool calls as they run.
Keybindings
| Enter | Send. While the agent works, this steers it. |
| Shift + Enter or Alt + Enter | New line |
| ↑ ↓ | Prompt history |
| Esc | Stop the current run |
| Ctrl + P | Switch threads, from every channel |
| Ctrl + N | New conversation |
| Alt + ↑ / ↓ | Previous or next thread |
| Ctrl + B | Toggle the sidebar |
| Ctrl + T | Show or hide thinking |
| Ctrl + O | Expand or collapse tool output |
| PgUp / PgDn | Scroll; Ctrl + Home / End jump to the top or bottom |
| Ctrl + ↑ / ↓ | Jump between your messages |
| Ctrl + Shift + F | Search the transcript |
| Ctrl + L | Clear the screen |
| Ctrl + C twice | Quit |
Slash commands
/new [@agent] | Start a new conversation, optionally with another agent. |
/agent <handle> | Switch this thread to another agent. |
/agents | Pick an agent and start a chat. |
/threads | Browse conversations from every channel. |
/model [provider/model] | Change the current agent's model. |
/title <text> | Rename this thread. |
/attach <path> | Attach a file or image to your next message. Repeat for more; /attach clear drops them. |
/reset | Give the agent a fresh context in this thread. |
/stop | Stop the current run. |
/approve | Approve the tool call this thread is waiting on. |
/deny [reason] | Deny it. The agent sees the reason. |
/compact | Summarize older messages to free up context. |
/providers | Models and subscriptions, configured or not. |
/login <provider> | Sign in to a provider, for example /login anthropic. |
/key <provider> <api-key> | Save an API key for a provider. |
/connectors | Slack, Discord, Telegram, and webhook status. |
/usage | Tokens and cost across agents and models. |
/clear | Clear the screen. |
/help | Commands and keyboard shortcuts. |
/quit | Exit. /exit and /q work too. |
Type / in the input to search the commands. Start a message with // to send text that begins with a slash.
CLI commands
soloagent | Open the TUI. --agent <handle>, --thread <id>, and --new pick where it starts. |
soloagent ask "…" | Print one answer and exit. Piped input is added after the message; ask - reads the whole message from stdin. Takes --agent, --thread, --json, and --file <path> (repeatable) to attach files and images. |
soloagent agents | List agents. --json for scripts. |
soloagent threads | List recent threads. Takes --limit, --agent, and --json. |
soloagent automations | List automations with their schedules, targets, and next runs. --json for scripts. |
soloagent login [provider] | Sign in to a provider from the shell; --key saves an API key instead. |
soloagent serve | Run the server and web dashboard. Takes --port (default 7420), --data, and --host. |
soloagent token | Print the local server's admin token, to sign in to its dashboard. |
soloagent connect <url> --token … | Check and remember a remote server. The TUI, ask, agents, threads, and login then use it. |
soloagent disconnect | Forget the remote server and run locally again. |
HTTP API & SDK
The dashboard and TUI use the same public API, so anything they do, your scripts can do. All routes live under /api and require the admin token in an authorization: Bearer … header (or a signed-in user's session cookie), except inbound webhooks, which carry their own secret, invite links, and /api/version. Only the WebSocket accepts the token as ?token=; REST routes reject it. Requests without a valid token get 401.
curl https://solo.example.com/api/agents \
-H "authorization: Bearer $SOLOAGENT_TOKEN"Routes
| GETPOST | /api/agents | List or create agents. |
| GETPOST | /api/threads | List or create threads. Create takes { agentId, title }, both optional; agentId can be an id or a handle. |
| POST | /api/threads/:id/messages | Send { text, attachments? }; attachments as on Files & images. Returns 202 with a submissionId; follow progress over the WebSocket. |
| POST | /api/threads/:id/ask | Send { text, attachments? } and wait for the answer: { status, answer }, or { status: "unanswered", reason }. |
| GET | /api/files/:sha256 | A shared file, with its type and name. Images are served inline, other files as downloads. |
| GET | /api/providers | List providers, their models, and which are configured. |
| POST | /api/providers/:id/login | Start a login flow: { type: "oauth" } (the default) or { type: "api_key" }. |
| PUT | /api/providers/:id/key | Save an API key: { key }. |
| GETPOST | /api/connectors | List or create connectors. Create takes { kind, name, agentId, config }; a webhook's config is { secret }. |
| GET | /api/agents/export | Every agent's full settings as JSON, without server ids. |
| POST | /api/agents/import | Import { agents, mode: "merge" | "skip-existing" }, matched by handle. ?dryRun=1 reports what would change. |
| GET | /api/backup | Stream a .tar.gz backup. ?includeSecret=1 adds the credentials secret. |
| GET | /api/version | { current, latest, url, updateAvailable }. No token needed. |
| GETPOST | /api/automations | List or create automations. |
| PATCHDELETE | /api/automations/:id | Edit (including { enabled }) or delete an automation. |
| POST | /api/automations/:id/run | Run an automation once now. |
| GET | /api/automations/preview?cron=&tz= | The schedule in words and its next 5 run times. |
| GETPOST | /api/mcp-servers | List or create MCP servers. Create takes { id, name, transport, command, args, env } or { id, name, transport: "http", url, headers }. Env values and headers come back masked as ••••••••; sending the mask back keeps the saved value. |
| GETPATCHDELETE | /api/mcp-servers/:id | Read, update, or delete an MCP server. Updates reconnect it. |
| POST | /api/mcp-servers/:id/test | Connect if needed and list its tools live: { ok, error?, tools, server }. |
| GETPOST | /api/agents/:id/memories | List an agent's memories, newest first (?q= searches), or add one: { text }. |
| PATCHDELETE | /api/agents/:id/memories/:memoryId | Edit a memory's { text }, or delete it. |
| DELETE | /api/agents/:id/memories?confirm=all | Delete every memory of an agent. Returns { deleted }. |
| POST | /api/hooks/:id | Inbound webhook. Authenticated by x-solo-secret, not the admin token. |
| GET | /api/approvals | List approval requests. ?status=pending for waiting ones, decided for the rest; ?threadId= filters by thread. |
| POST | /api/approvals/:id/approve | Approve a waiting tool call. Optional { by }. |
| POST | /api/approvals/:id/deny | Deny it: { reason?, by? }. Returns 409 if it was already decided. |
| GETPUT | /api/approval-settings | Read or change { timeoutMs, riskyTools }. |
| GET | /api/spend/today | Today's spend and messages per agent, with budgets. /api/usage/daily?days=14 gives spend per day. |
| GETPOST | /api/invites | List or create invites (admins): { role, name }. Create returns the one-time url. /api/users lists people; PATCH and DELETE /api/users/:id change a role or remove someone. |
| POST | /api/admin-token/rotate | Replace a stored admin token and return the new one. 409 when it comes from SOLOAGENT_TOKEN. |
| WS | /api/ws | Live events. Pass the token as ?token=. |
WebSocket events
Connect to /api/ws and send { "type": "subscribe", "threadId": "…" } to receive a thread's thread.snapshot, thread.messages, and thread.live events (streaming text, thinking, and tool calls). Send { "type": "tasks.subscribe" } for the live durable task graph. Server-wide events such as thread.updated, agent.updated, connector.updated, approval.updated, automation.updated, and mcp.updated arrive without subscribing. A thread's live.approvals lists the tool calls it waits on.
TypeScript SDK
@soloagent/client, in packages/client, is a typed client for the REST API and WebSocket with no dependencies. It runs in Node 22.19+ and in browsers. It isn't on npm yet, so use it from a clone of the repository.
import { SoloClient } from "@soloagent/client";
const solo = new SoloClient({
baseUrl: "https://solo.example.com",
token: process.env.SOLOAGENT_TOKEN,
});
const thread = await solo.createThread({ agentId: "coder" });
// Wait for the full answer
const result = await solo.ask(thread.id, "What changed in the last deploy?");
console.log(result.answer);
// Or fire and forget, and follow along live
await solo.send(thread.id, "Now draft release notes");const stream = solo.connect(); // reconnects automatically
stream.subscribe(thread.id);
stream.on((event) => {
if (event.type === "thread.live") render(event.live); // streaming text, thinking, tools
if (event.type === "thread.messages") append(event.messages); // committed messages
});
// later
stream.close();Embed the core
The runtime, @soloagent/core in packages/core, is a library too. Open it against a data directory and use it in-process, without the HTTP server.
import { SoloAgent } from "@soloagent/core";
const solo = await SoloAgent.open({ dataDir: "./data" });
// … create agents and threads, send messages …
await solo.close();Durability
Agent runs execute on @earendil-works/pi-durable, Pi's durable execution harness, backed by SQLite in your data directory.
- Commit before show. Each model turn and each tool call is recorded as a durable step and committed before it's shown in any UI or chat app. Streaming text is saved as it arrives, so a crash loses at most about a second of a half-written answer.
- Resume after a crash. If the process exits mid-run (a crash, a restart, a deploy), the run resumes from its last committed step when the server starts again. Completed steps aren't repeated. An interrupted model request is retried; a tool call cut off midway runs again only if it's safe to repeat, and otherwise the agent is told it was interrupted.
- Durable sleep. Scheduled reminders and automations are durable timers. They wait in storage, not in memory, so they still fire after restarts. An automation that came due while the server was down runs once on startup.
- Durable approvals. A tool call waiting for approval has not started, so a restart simply asks again for the same request. Once approved, it runs once. The restart limit of Pi Durable still applies: after five restarts in a row while one request waits, that call ends with an error.
- Durable delegation.
ask_agentstarts a child conversation that is itself durable. The parent waits for it, across restarts if needed. - Visible. The dashboard shows the live durable task graph and upcoming schedules.
/data. Elsewhere, make sure SOLOAGENT_DATA_DIR points at persistent storage.
Backups & upgrades
A backup is one .tar.gz of the data directory: consistent snapshots of both SQLite databases, agent workspaces, uploaded files, and a manifest.json. It's safe to take while agents are running.
soloagent backup # ./soloagent-backup-<time>.tar.gz
soloagent backup --out solo.tar.gz --url https://solo.example.com --token "$SOLOAGENT_TOKEN"
soloagent restore solo.tar.gz # into ~/.soloagent/data, or --data <dir>soloagent backup talks to your remote server or local server if one is configured or running. Otherwise it reads ~/.soloagent/data directly, or the directory you pass with --data. You can also download a backup from Settings in the dashboard, or GET /api/backup.
soloagent restore checks the archive before it touches anything: the manifest, every file's checksum, and each database's integrity. It refuses to run while a server is using that data directory, and it won't replace existing data without --force. With --force, the backups/ folder is kept.
With Docker Compose, the image has the CLI and uses /data:
docker compose exec -T soloagent soloagent backup --out - > solo.tar.gz
docker compose stop soloagent
docker compose run --rm -v "$PWD:/backup" soloagent soloagent restore /backup/solo.tar.gz --force
docker compose start soloagentSecrets and restores
Provider logins and bot tokens are sealed with SOLOAGENT_SECRET, or with the .secret file generated in the data directory. Backups leave the secret out by default, so a restored server needs the same secret to open them:
- If you set
SOLOAGENT_SECRET, set the same value on the restored server. - If you didn't, either keep a copy of
<data>/.secretsomewhere safe, or back up with--include-secret. Anyone with that archive can then read every credential, so store it like a password.
The restore warns you when the secret is missing or doesn't match the one the backup was made with. Agents, threads, and memory don't depend on the secret; only credentials do.
Scheduled backups
Set SOLOAGENT_BACKUP_INTERVAL (24h, 6h, 30m…) and the server writes a backup to <data>/backups/ on that interval, keeping the newest SOLOAGENT_BACKUP_KEEP (default 7). Scheduled backups never include the secret. They sit on the same disk as your data, so copy them somewhere else too.
To send backups to S3 or any other object store, write to stdout with --out -:
soloagent backup --out - | aws s3 cp - s3://my-bucket/soloagent/$(date +%F).tar.gz
aws s3 cp s3://my-bucket/soloagent/2026-11-03.tar.gz - | soloagent restore - --data /var/lib/soloagentRun the first line from cron on any machine that can reach the server, with SOLOAGENT_URL and SOLOAGENT_TOKEN set.
Moving agents between servers
Export writes every agent's settings (instructions, model, skills, and anything else stored on the agent) to JSON. Import matches agents by handle: new handles are created, and existing ones are updated (merge, the default) or left alone (skip-existing). Import never changes which agent is the default. The dashboard's Agents page has the same Export and Import buttons, with a preview before anything changes.
soloagent agents export > agents.json
soloagent agents import agents.json --dry-run
soloagent agents import agents.json --mode skip-existingUpgrading
The server checks GitHub for a new release when it starts and every 12 hours. When there is one, the dashboard shows a note in the sidebar, Settings shows how to upgrade, and the terminal UI mentions it on its welcome screen. soloagent --version also says when an update is available; it checks at most once a day. Set SOLOAGENT_UPDATE_CHECK=0 to turn checks off.
Your data and unfinished durable work carry over. Database migrations run when the new version starts. Take a backup first.
| Installed with | Upgrade |
|---|---|
| Homebrew | brew upgrade soloagent, then restart soloagent serve. |
| Installer or npm | Run curl -fsSL https://soloagent.dev/install.sh | sh again (or npm install -g the release tarball), then restart soloagent serve or sudo systemctl restart soloagent. |
| Docker Compose | docker compose pull && docker compose up -d |
| Docker | docker pull ghcr.io/nearbycoder/soloagent.dev, then remove and re-run the container with the same volume. |
| Railway | Redeploy the latest commit from the service's Deployments. If you deployed a fork, sync it first. |
| Render | Manual Deploy → Deploy latest commit. Sync your fork first if you use one. |
| Fly.io | git pull && fly deploy from your checkout. |
| From source | git pull && npm install && npm run build, then restart. |
Security
- The admin token and users. The API, TUI, and WebSocket take the admin token:
SOLOAGENT_TOKEN, or the one generated on first boot. Treat it like a password and rotate it if it leaks. People sign in to the dashboard with their own name and password and an admin or member role. Sessions live in an HTTP-only cookie. - Sealed credentials. Provider keys, OAuth tokens, connector tokens, webhook secrets, and MCP server env values and headers are encrypted at rest with AES-256-GCM, using a key derived from
SOLOAGENT_SECRETor the secret generated in the data directory. - Webhook secrets. Each webhook connector has its own shared secret, sent in
x-solo-secret. It grants only that connector's access. - Workspace is powerful. The Workspace skill can run shell commands on your server with the server's permissions; its folder is a starting point, not a sandbox. Chat apps can't reach such an agent unless you turn on its chat access; if you do, limit who can reach it with the connector's access lists.
- MCP servers run with your server's access. A stdio MCP server is a program running as the soloagent user, and an agent with its tools can do whatever those tools allow. See Tools (MCP).
- Your data stays with you. Threads, memory, schedules, and credentials live in your data directory. Model calls go directly from your server to the providers you connect.
Found a vulnerability? Please report it privately through GitHub security advisories.
Something missing or wrong? Open an issue.