soloagent

soloagent / docs

Documentation

Install soloagent, connect models and chat platforms, and integrate with the HTTP API.

On this page

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 | sh

It 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:

npm
# 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.tgz

On macOS or Linux with Homebrew, which brings its own Node:

brew install nearbycoder/soloagent/soloagent

No Node? Use Docker, one command, any OS:

docker run -d --name soloagent -p 7420:7420 -v soloagent:/data ghcr.io/nearbycoder/soloagent.dev

Then open http://localhost:7420 and paste the token from docker logs soloagent.

First run

shell
# 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 dashboard

A 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.

more examples
# 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_TOKEN

Deploy

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.

WhereHowGood for
Dockerdocker compose up -dAny VPS, home server, or NAS; Coolify, Dokploy, CapRover, Portainer
RailwayNew project from the GitHub repoThe fewest steps on a managed host
RenderOne-click BlueprintManaged host with a persistent disk
Fly.iofly launch + fly deployManaged host close to your users
Linux servernpm + systemdA 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:

shell
curl -fsSLO https://raw.githubusercontent.com/nearbycoder/soloagent.dev/main/docker-compose.yml
docker compose up -d
docker compose logs soloagent | grep token

Data 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

  1. Create the service

    At railway.com/new, choose GitHub Repository and pick your fork, or paste https://github.com/nearbycoder/soloagent.dev and choose Deploy Repo. The Dockerfile and railway.json are picked up automatically.

  2. Add a volume at /data

    soloagent uses the volume as its data directory automatically.

  3. Set variables and a domain

    Set SOLOAGENT_TOKEN and SOLOAGENT_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).

Deploy to Render

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:

shell
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 deploy

Create 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

shell
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 token

Optional settings go in /etc/soloagent.env (for example SOLOAGENT_TOKEN=…).

Configuration

VariableWhat it does
SOLOAGENT_TOKENAdmin token for the dashboard, TUI, and API. If unset, one is generated on first boot, stored, and printed in the logs.
SOLOAGENT_SECRETSeals 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_DIRWhere data lives. The Docker image uses /data, so mount your volume there. soloagent serve defaults to ~/.soloagent/data; --data overrides it.
PORTHTTP port, default 7420. Most hosts set it for you. soloagent serve --port overrides it.
SOLOAGENT_DEMO0 hides the built-in demo model.
SOLOAGENT_ALLOW_PRIVATE_FILE_URLS1 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_URLThe public address printed at startup. Railway's domain is detected automatically.
SOLOAGENT_BACKUP_INTERVALTake a backup on a timer, for example 24h or 6h. Off by default. See Backups.
SOLOAGENT_BACKUP_KEEPHow many scheduled backups to keep, default 7.
SOLOAGENT_UPDATE_CHECK0 turns off the check for new releases.

The server prints its URL and token when it starts:

logs
  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:

Claude Pro / Max ChatGPT GitHub Copilot SuperGrok / X Premium OpenRouter and others

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.

Server running remotely? Some providers redirect back to a 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.

FieldDescription
Name & @handleThe handle is how you address it in chat: @coder. Lowercase letters, digits, and dashes.
Avatar & colorA single glyph or emoji and an accent color, shown in the dashboard and TUI.
InstructionsThe system prompt: who it is, how it should behave, what it knows.
ModelAny connected provider and model. Different agents can use different providers.
Thinking leveloff, minimal, low, medium, high, or xhigh on models that support reasoning.
SkillsThe tool sets it may use (below). New agents get every skill except Workspace.
Max turnsThe most tool rounds one run may take, 40 by default.
ApprovalsWhich tool calls wait for a person first: off, risky, or all. See Approvals.
DefaultThe default agent answers anything not addressed to a specific agent.

Skills

Memory

Long-term notes about people, preferences, and facts, kept per agent across every conversation.

rememberrecallforget
Web

Fetch and read web pages and APIs, and search the web through DuckDuckGo.

web_fetchweb_search
Delegate

Hand 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_agent
Schedule

Durable reminders, follow-ups, and recurring automations. They sleep in storage and fire even after restarts.

schedule_reminderschedule_recurringlist_automationscancel_automation
Workspace powerful

Read, 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.

readwriteeditbash

Agents 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.

FieldDescription
IDA short slug such as github. Tools are named mcp__<id>__<tool>, e.g. mcp__github__create_issue. It can't be changed later.
Transportstdio 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, envstdio only. The program, its arguments (one per line in the dashboard), and environment variables such as API tokens.
URL, headersHTTP only. The endpoint and request headers, usually Authorization: Bearer ….
EnabledTurn 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:

shell
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:

json
{
  "id": "files",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data/shared"]
}

A remote server over HTTP, here GitHub's hosted one:

json
{
  "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_search tool 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.
stdio servers run on your server. They run as the same operating system user as soloagent, with its file and network access. They get 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.

PolicyAsks before
offNothing. Tools run at once. The default for agents without the Workspace skill.
riskybash, write, edit, and the tools on the risky list. The default for agents with the Workspace skill.
allEvery 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 risky policy asks about, such as forget or schedule_reminder. End a name with * to match a prefix: mcp__github__* covers every tool of that MCP server. The all policy 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. /approve approves the current thread's waiting call; /deny [reason] denies it.
  • API. GET /api/approvals?status=pending, then POST /api/approvals/:id/approve or POST /api/approvals/:id/deny with { "reason": "…" }. Both take an optional by, the name recorded as the decider (default dashboard).

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 like U012ABCDEF (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.

  1. 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.

  2. Create an app-level token

    Basic Information → App-Level Tokens → Generate with the connections:write scope. Copy the xapp-… token.

  3. Install and copy the bot token

    Install App to your workspace and copy the Bot User OAuth Token (xoxb-…).

  4. 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.

slack-manifest.yaml
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: false

The 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

  1. Create an application

    Open the Discord Developer Portal → New Application.

  2. Get the bot token

    On the Bot tab, click Reset Token and copy it. Turn on Message Content Intent under Privileged Gateway Intents.

  3. Invite the bot

    OAuth2 → URL Generator: select the bot scope 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.

  4. 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

  1. Create a bot

    Message @BotFather on Telegram and send /newbot. Pick a name and username; BotFather replies with a token.

  2. Connect

    Paste the token into a new Telegram connector, then message your bot.

  3. Groups and topics

    Add the bot to a group and mention it or reply to it. Forum topics each get their own conversation. Use /setprivacy in 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.

shell
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 fieldMeaning
textThe message. Routing works as in chat, so @coder … picks an agent.
threadKeeps a conversation going across requests. Omit it for a one-off.
userA display name for the sender.
attachmentsOptional files: [{ "name", "mimeType", "data_base64" }] or [{ "name", "mimeType", "url" }]. text may be empty when there are attachments. See Files & images.
callback_urlOptional. 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:

  1. If it starts with @handle or handle:, that agent answers. @coder fix the build and coder: fix the build are the same.
  2. Otherwise, the agent bound to the connector answers.
  3. 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

CommandWhat it does
!agentsList the agents you can talk to and their handles.
!resetStart a fresh conversation in this thread or DM. !new does the same.
!stopStop the agent's current run.
!helpShow 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.

ListMatches
PeopleThe platform user id (U024BE7LH) or display name (ann), ignoring case.
ChannelsThe 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.
AgentsAgent 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

SettingWhereWhat it limits
Daily spendAgent, budget.dailyUsdUSD spent by the agent today, at list API prices.
Daily messagesAgent, budget.dailyRunsMessages the agent accepted today.
Messages per person per hourConnector, limits.perUserPerHourMessages 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.

RoleCan
MemberChat, view agents and conversations, see usage and activity.
AdminEverything 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:

FileWhat the agent gets
Images: PNG, JPEG, GIF, WebP; BMP and TIFF are convertedThe 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 codeThe 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.
PDFA note with the name, type, and size. soloagent doesn't extract PDF text.
Anything elseA 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:

slack
@solo every weekday at 9 post the open incidents here

The 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 phraseCron
every 15 minutes*/15 * * * *
every 2 hours0 */2 * * *
daily at 17:3030 17 * * *
weekdays at 9:000 9 * * 1-5
mondays at 9:3030 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

shell
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

EnterSend. While the agent works, this steers it.
Shift + Enter or Alt + EnterNew line
↑ ↓Prompt history
EscStop the current run
Ctrl + PSwitch threads, from every channel
Ctrl + NNew conversation
Alt + ↑ / ↓Previous or next thread
Ctrl + BToggle the sidebar
Ctrl + TShow or hide thinking
Ctrl + OExpand or collapse tool output
PgUp / PgDnScroll; Ctrl + Home / End jump to the top or bottom
Ctrl + ↑ / ↓Jump between your messages
Ctrl + Shift + FSearch the transcript
Ctrl + LClear the screen
Ctrl + C twiceQuit

Slash commands

/new [@agent]Start a new conversation, optionally with another agent.
/agent <handle>Switch this thread to another agent.
/agentsPick an agent and start a chat.
/threadsBrowse 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.
/resetGive the agent a fresh context in this thread.
/stopStop the current run.
/approveApprove the tool call this thread is waiting on.
/deny [reason]Deny it. The agent sees the reason.
/compactSummarize older messages to free up context.
/providersModels 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.
/connectorsSlack, Discord, Telegram, and webhook status.
/usageTokens and cost across agents and models.
/clearClear the screen.
/helpCommands and keyboard shortcuts.
/quitExit. /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

soloagentOpen 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 agentsList agents. --json for scripts.
soloagent threadsList recent threads. Takes --limit, --agent, and --json.
soloagent automationsList 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 serveRun the server and web dashboard. Takes --port (default 7420), --data, and --host.
soloagent tokenPrint 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 disconnectForget 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.

shell
curl https://solo.example.com/api/agents \
  -H "authorization: Bearer $SOLOAGENT_TOKEN"

Routes

GETPOST/api/agentsList or create agents.
GETPOST/api/threadsList or create threads. Create takes { agentId, title }, both optional; agentId can be an id or a handle.
POST/api/threads/:id/messagesSend { text, attachments? }; attachments as on Files & images. Returns 202 with a submissionId; follow progress over the WebSocket.
POST/api/threads/:id/askSend { text, attachments? } and wait for the answer: { status, answer }, or { status: "unanswered", reason }.
GET/api/files/:sha256A shared file, with its type and name. Images are served inline, other files as downloads.
GET/api/providersList providers, their models, and which are configured.
POST/api/providers/:id/loginStart a login flow: { type: "oauth" } (the default) or { type: "api_key" }.
PUT/api/providers/:id/keySave an API key: { key }.
GETPOST/api/connectorsList or create connectors. Create takes { kind, name, agentId, config }; a webhook's config is { secret }.
GET/api/agents/exportEvery agent's full settings as JSON, without server ids.
POST/api/agents/importImport { agents, mode: "merge" | "skip-existing" }, matched by handle. ?dryRun=1 reports what would change.
GET/api/backupStream a .tar.gz backup. ?includeSecret=1 adds the credentials secret.
GET/api/version{ current, latest, url, updateAvailable }. No token needed.
GETPOST/api/automationsList or create automations.
PATCHDELETE/api/automations/:idEdit (including { enabled }) or delete an automation.
POST/api/automations/:id/runRun an automation once now.
GET/api/automations/preview?cron=&tz=The schedule in words and its next 5 run times.
GETPOST/api/mcp-serversList 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/:idRead, update, or delete an MCP server. Updates reconnect it.
POST/api/mcp-servers/:id/testConnect if needed and list its tools live: { ok, error?, tools, server }.
GETPOST/api/agents/:id/memoriesList an agent's memories, newest first (?q= searches), or add one: { text }.
PATCHDELETE/api/agents/:id/memories/:memoryIdEdit a memory's { text }, or delete it.
DELETE/api/agents/:id/memories?confirm=allDelete every memory of an agent. Returns { deleted }.
POST/api/hooks/:idInbound webhook. Authenticated by x-solo-secret, not the admin token.
GET/api/approvalsList approval requests. ?status=pending for waiting ones, decided for the rest; ?threadId= filters by thread.
POST/api/approvals/:id/approveApprove a waiting tool call. Optional { by }.
POST/api/approvals/:id/denyDeny it: { reason?, by? }. Returns 409 if it was already decided.
GETPUT/api/approval-settingsRead or change { timeoutMs, riskyTools }.
GET/api/spend/todayToday's spend and messages per agent, with budgets. /api/usage/daily?days=14 gives spend per day.
GETPOST/api/invitesList 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/rotateReplace a stored admin token and return the new one. 409 when it comes from SOLOAGENT_TOKEN.
WS/api/wsLive 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.

ask.ts
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");
stream.ts
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.

embed.ts
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_agent starts 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.
Keep the data directory. Durability is only as good as the storage behind it. In the Docker image (Docker, Railway, Render, Fly.io), that's the volume at /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.

shell
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:

shell
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 soloagent

Secrets 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>/.secret somewhere 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 -:

shell
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/soloagent

Run 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.

shell
soloagent agents export > agents.json
soloagent agents import agents.json --dry-run
soloagent agents import agents.json --mode skip-existing

Upgrading

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 withUpgrade
Homebrewbrew upgrade soloagent, then restart soloagent serve.
Installer or npmRun 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 Composedocker compose pull && docker compose up -d
Dockerdocker pull ghcr.io/nearbycoder/soloagent.dev, then remove and re-run the container with the same volume.
RailwayRedeploy the latest commit from the service's Deployments. If you deployed a fork, sync it first.
RenderManual Deploy → Deploy latest commit. Sync your fork first if you use one.
Fly.iogit pull && fly deploy from your checkout.
From sourcegit 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_SECRET or 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.