Docs / Autopilot / Unattended runs

Running unattended and on a server

Run autopilot serve for long unattended runs with jobs, timers, budgets and email decisions, and deploy it on a Linux server.

Overview

autopilot serve starts a headless HTTP server. Through its /runs API, an agent can work on a task much longer than one model turn: it starts a managed job, yields with wait_for, and is invoked again when the job finishes or a timer fires. No model request stays open while the job runs.

To send a task from your laptop to a server and get a pull request back, see Send tasks to your server.

Start the server

KIMIFLARE_SERVER_PASSWORD='use-a-secret' autopilot serve
Option Default Description
--port <n> 4096 Port to listen on
--hostname <host> 127.0.0.1 Address to listen on
  • The server refuses to listen on any address other than localhost unless KIMIFLARE_SERVER_PASSWORD is set. If you expose it beyond the machine, put TLS in front of it.
  • Creating runs requires KIMIFLARE_SERVER_PASSWORD.
  • Requests use HTTP Basic auth with the user name kimiflare and the server password.
  • Requests carrying a browser Origin from another site get 403. On a localhost listener, the Host header must be a localhost name. To let a specific web app call the API, list its origins in KIMIFLARE_SERVER_CORS_ORIGINS (comma-separated).
  • Run the server under a service manager so scheduled check-ins fire.

Create a run

curl -u "kimiflare:$KIMIFLARE_SERVER_PASSWORD" \
  -H 'Content-Type: application/json' \
  -d '{
    "task": "Run the training job, inspect its checkpoints and health periodically, and report when it completes or needs intervention.",
    "cwd": "/workspace/model-training",
    "worktree": false,
    "allowedTools": ["job_start", "job_status", "job_logs", "job_cancel"]
  }' \
  http://127.0.0.1:4096/runs

Run fields

Field Type Description
task string What the agent should do
cwd string Working directory. Required when worktree is on.
worktree boolean Run in an isolated Git worktree. Default true; set false for work that doesn't need one.
allowedTools string array Tools available to the run. wait_for is always added.
model string Model id. Default: the configured model.
maxRuntimeMs number or null Runtime budget for the whole run, including waiting
maxTotalTokens number or null Cumulative token budget
maxCostUsd number or null Cost budget, at least $0.01
maxToolIterations number or null Tool-action budget for the whole run

New runs have no runtime, token, cost or tool-action budget by default. Usage is still accounted for. A single model turn is still bounded by the per-turn iteration limit and loop guardrails, and provider rate limits and machine resources still apply.

Endpoints

Method and path Description
GET / Health check and version
GET /doc OpenAPI documentation
GET /runs List runs
POST /runs Create a run
GET /runs/:id Get a run
GET /runs/:id/events A run's events
POST /runs/:id/resume Resume a paused run with new budgets or a new model
POST /runs/:id/cancel Cancel a run
POST /runs/stop-all Kill switch: cancel every active run and refuse new ones
DELETE /runs/stop-all Lift the kill switch

Keep a job running and check in

The agent uses job_start for a detached command, then yields instead of polling:

job_start({ command: "python train.py --checkpoint-every 10m", idempotency_key: "training-run-1" })
wait_for({ job_id: "<job-id>", poll_interval_ms: 300000 })
Tool Purpose
job_start Start a detached command. timeout_ms is optional (maximum seven days); omit it for no Autopilot-enforced timeout.
job_status Check a job
job_logs Read the job's output, which is appended to log files
job_cancel Stop a job
wait_for Yield the run until a job finishes or a timer fires. One interval is at most seven days; checks repeat for the life of the run.

job_start is not an interactive shell. Stdin is closed, and Autopilot can't send later keystrokes to the process. Use it for commands that manage themselves, such as a training program with checkpoints. For interactive control, GPU allocation, cluster placement or infrastructure-level retries, use an external job scheduler.

Budget pauses and resumption

If an explicit budget is reached, or cost can't be enforced because the provider didn't report authoritative cost, the run moves to needs_input. It is not marked completed or failed, and the background job is not killed. Resume with a revised limit, or null to remove it:

curl -u "kimiflare:$KIMIFLARE_SERVER_PASSWORD" \
  -H 'Content-Type: application/json' \
  -d '{"maxRuntimeMs": null, "maxTotalTokens": null}' \
  -X POST http://127.0.0.1:4096/runs/<run-id>/resume

The resume request must change at least one budget or the model. Omitted fields keep their current values.

When a turn outgrows the context window

An unattended run's single turn keeps growing. Older turns are compacted, but the active turn is kept word for word. If it no longer fits the model's context window, the run moves to needs_input with a context_budget_exceeded stop reason, and connected clients receive a run.needs_input event. Resume on a model with a larger window:

curl -u "kimiflare:$KIMIFLARE_SERVER_PASSWORD" \
  -H 'Content-Type: application/json' \
  -d '{"model": "google/gemini-3.8-flash"}' \
  -X POST http://127.0.0.1:4096/runs/<run-id>/resume

Email notifications

Autopilot can email you when a run completes, fails, needs input, is cancelled or is interrupted. It sends through Resend.

export RESEND_API_KEY=re_...
export AUTOPILOT_NOTIFY_EMAIL_TO=you@example.com    # comma-separated for several
export AUTOPILOT_NOTIFY_EMAIL_FROM="Autopilot <bot@your-verified-domain.com>"   # optional

Or in the config file:

{ "runNotifications": { "email": { "to": ["you@example.com"], "from": "Autopilot <bot@your-domain.com>", "events": ["needs_input", "failed", "completed"] } } }

The email gives the run id, the task (with credential-looking strings redacted), status and stop reason, branch, tokens and cost, and the command to inspect or resume it. It never includes transcript content.

  • A notification is recorded in the same database transaction as the state change, so a provider outage never blocks a run.
  • Temporary rejections (429, 5xx) are retried. Other rejections are marked failed.
  • Unknown outcomes are retried with the same idempotency key, so Resend delivers at most once.
  • Notifications older than 24 hours are not sent.

Decide by email

With notifications on, you can answer a "needs input" email by replying. Autopilot polls a mailbox over IMAP, so the server needs no public endpoint. Use a mailbox dedicated to Autopilot.

Variable Description
AUTOPILOT_DECISIONS_REPLY_TO Reply-To address on "needs input" emails
AUTOPILOT_DECISIONS_IMAP_HOST IMAP host
AUTOPILOT_DECISIONS_IMAP_USER IMAP user
AUTOPILOT_DECISIONS_IMAP_PASSWORD IMAP password, such as an app password
AUTOPILOT_DECISIONS_IMAP_PORT IMAP port (default 993)
AUTOPILOT_DECISIONS_IMAP_MAILBOX Mailbox (default INBOX)

Reply with one line:

Reply Effect
resume Continue. If a budget ran out, that one budget is doubled, once.
resume model <id> Continue on another catalog model
cancel Stop the run
answer: <text> Pass the text (up to 2,000 characters) to the agent, then continue

A reply is acted on only if it comes from a notification recipient, passes DKIM or DMARC on your mail server, carries the email's [ref:...] token (single use, valid for 72 hours), and arrives while the run is still waiting. A reply can't change tools, permissions or other runs.

Restarts

Waiting runs and their timers are stored in SQLite, and jobs are detached from the server process. If the server restarts during an agent turn, the run resumes automatically only when its latest checkpoint has no tool call with an unknown outcome. Otherwise it becomes interrupted_unknown, so an action such as starting a second training job is never replayed.

This is process-restart recovery, not machine-reboot durability. A detached local process does not survive a host reboot, power loss or container replacement. For multi-week work, checkpoint to durable storage and run the workload under a host or cluster service manager.

Deploy on a Linux server

These steps follow docs/deploy-linux-server.md, written for a host such as Ubuntu 24.04.

Host and user

Install Node.js (a version that meets the requirements), Git, SQLite and restic, then create a dedicated account:

sudo useradd --create-home --shell /bin/bash autopilot
sudo mkdir -p /srv/autopilot && sudo chown autopilot:autopilot /srv/autopilot
sudo npm install -g autopilot-ai

Clone the repositories runs will work on into /srv/autopilot as the autopilot user. Give that user only the credentials runs need.

Secrets

sudo mkdir -p /etc/autopilot
sudo install -m 600 /dev/null /etc/autopilot/autopilot.env
sudoedit /etc/autopilot/autopilot.env
KIMIFLARE_SERVER_PASSWORD=<long random string>
OPENROUTER_API_KEY=sk-or-...
AUTOPILOT_MAX_CONCURRENT_RUNS=4
RESEND_API_KEY=re_...
AUTOPILOT_NOTIFY_EMAIL_TO=you@example.com
AUTOPILOT_NOTIFY_EMAIL_FROM=Autopilot <bot@your-verified-domain.com>

Service

The repository ships a systemd unit, deploy/autopilot.service:

sudo cp deploy/autopilot.service /etc/systemd/system/autopilot.service
sudo systemctl daemon-reload
sudo systemctl enable --now autopilot
journalctl -u autopilot -f

The unit runs as autopilot, listens on 127.0.0.1:4096 only, restarts on failure, and keeps the filesystem read-only except the user's home and /srv/autopilot. It uses KillMode=process, so managed jobs keep running across restarts.

Access

Don't expose the server publicly. Tunnel to it over SSH:

ssh -L 4096:127.0.0.1:4096 autopilot@your-host
curl -u "kimiflare:$KIMIFLARE_SERVER_PASSWORD" http://127.0.0.1:4096/runs

Limits and the kill switch

  • At most AUTOPILOT_MAX_CONCURRENT_RUNS runs (default 4) can be queued or running at once. Creating or resuming one past the limit returns 429. Waiting and paused runs don't count.
  • The kill switch cancels every active run and refuses new ones. Add "jobs": true to also cancel running managed jobs. The halt is stored in the runs database, so it survives restarts.
curl -u "kimiflare:$KIMIFLARE_SERVER_PASSWORD" -H 'Content-Type: application/json' \
  -d '{"reason": "incident", "jobs": true}' -X POST http://127.0.0.1:4096/runs/stop-all

# Lift it
curl -u "kimiflare:$KIMIFLARE_SERVER_PASSWORD" -X DELETE http://127.0.0.1:4096/runs/stop-all

For a hard stop, run sudo systemctl stop autopilot. Managed jobs keep running until you cancel them.

Upgrades

sudo npm install -g autopilot-ai@latest
sudo systemctl restart autopilot

Runs waiting on timers or jobs resume on their own. A run in the middle of a turn resumes from its last checkpoint, or becomes interrupted_unknown.

State and backups

What Path
Runs, events, timers, notifications ~/.local/state/autopilot/runs.db
Managed jobs ~/.local/state/autopilot/jobs.db (job logs next to it)
Sessions ~/.local/share/kimiflare/sessions/
Config ~/.config/kimiflare/config.json

Copy the SQLite files with sqlite3 <db> ".backup '<dest>'", not cp, to get a consistent snapshot while the server writes. The source guide has a full nightly restic backup script and a restore test.

Send tasks to your server

From your laptop, you can send a task to an Autopilot server you run. It works in its own sandbox and opens a pull request, and you can close your laptop while it runs.

autopilot remote add https://acme.example.com      # approve the code in your browser; no keys copied
autopilot --on acme "fix the flaky checkout test"  # runs on the server
autopilot runs                                     # what's running or done, with PR links
autopilot runs watch <run>                         # follow a run (Ctrl-C detaches)
autopilot pull <run>                               # check out the run's branch here
  • The run starts from the last commit your branch shares with origin. Unpushed commits, staged and unstaged edits, and untracked files that aren't ignored are sent as a patch, up to 3 MiB.
  • --detach returns right away. -m <model> picks one of the server's models. --repo <owner/name> overrides the repository.
  • Each task gets its own sandbox and branch, so several can run at once.
  • The server keeps the model key and GitHub credentials. Your laptop holds only a revocable 90-day pairing credential in ~/.config/autopilot/remotes.json.
Command Description
autopilot remote add <url> [-n <name>] [--no-open] Pair with a server
autopilot remote list List paired servers
autopilot remote remove <name> Forget a server
autopilot runs [--on <server>] [--json] List tasks
autopilot runs watch <run> Follow a run until it ends
autopilot runs cancel <run> Stop a run
autopilot pull <run> Check out a run's branch

Setting up the server side for paired tasks is covered in docs/aster-api.md.