REST API Reference
Agentmd exposes an HTTP API over a Unix domain socket. The CLI uses this API internally — you can also use it directly for integrations.
Connection
Unix socket (default):
TCP (opt-in): Start with agentmd start --port 4100 --api-key YOUR_KEY, then:
Authentication
Auth is decided by the transport a request arrives on, not by the route:
| Transport | Requirement |
|---|---|
| Unix socket | None. The socket file is chmod 600, so the OS restricts it to your user, and the CLI sends no key. |
TCP (--port) |
X-API-Key: <key> on every route except /health. Missing or wrong key → 401 {"detail": "Invalid or missing API key"}. |
--port without --api-key is refused at startup — the API can execute
arbitrary agents, so there is no unauthenticated TCP mode. Both listeners serve
the same runtime and the same data; only the door differs.
Keep the port on 127.0.0.1 unless a firewall or a TLS-terminating proxy sits
in front of it: plain HTTP carries the key in cleartext. See
Server Mode for the startup flags.
Endpoints
Health & Info
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
No | Liveness check |
| GET | /info |
Yes | Backend status (version, pid, uptime, agents, scheduler) |
| POST | /shutdown |
Yes | Graceful shutdown |
Agents
| Method | Path | Description |
|---|---|---|
| GET | /agents |
List all agents (id, display_name, …) |
| GET | /agents/{id} |
Agent detail (config + last_run + next_run); {id} is the filename stem |
| POST | /agents/{id}/run |
Start execution, returns {execution_id} |
| GET | /agents/{id}/runs |
Execution history for agent |
| POST | /agents/reload |
Re-parse agent files from disk |
Path segments with spaces must be URL-encoded (e.g. /agents/Daily%20Processor). Lookup is by id only — a distinct display name does not resolve.
Agent list/detail fields:
| Field | Type | Description |
|---|---|---|
id |
string | Canonical filename stem |
display_name |
string | Frontmatter name: or id |
name |
string | Alias of display_name (compat; not a route key) |
Run request body:
{
"args": ["arg1"],
"message": "optional user message",
"context": "optional trigger context",
"session_id": "optional conversation id"
}
| Field | Type | Description |
|---|---|---|
args |
string[] | Positional arguments substituted into the prompt |
message |
string | Replaces the synthetic "Execute your task" |
context |
string | Appended to the user turn as trigger context |
session_id |
string | Conversation this run belongs to (max 200 chars; blank is treated as absent) |
When message is provided, it replaces the synthetic "Execute your task". This is how the CLI chat works — each chat turn is a /run with the user's message.
session_id is what makes a run a conversation turn: it is persisted on the
execution, recorded with trigger: "chat" instead of "manual", and it scopes
history seeding. With history enabled the runner seeds the next execution from
the most recent finished execution of the same session; a run without a
session_id only ever seeds from other sessionless runs of that agent. Two
clients chatting with the same agent under different session_ids therefore keep
independent conversations, and a scheduled run never inherits chat context (a
session_id sent with a schedule or watch run is ignored).
Generating and reusing the id is the client's job — the backend never invents one and there is no endpoint to list or resume sessions. Any opaque string works; a UUID per conversation is the intended shape.
Executions
| Method | Path | Description |
|---|---|---|
| GET | /executions |
List executions (filters: status, agent, limit, offset) |
| GET | /executions/{id} |
Execution detail |
| GET | /executions/{id}/messages |
Full message log |
| GET | /executions/{id}/stream |
SSE stream (catchup + live) |
| DELETE | /executions/{id} |
Cancel running execution |
Execution response fields:
| Field | Type | Description |
|---|---|---|
id |
int | Execution ID |
agent_id |
string | Canonical agent id (filename stem) |
status |
string | running, waiting, success, error, aborted, timeout, orphaned |
trigger |
string | manual, chat, schedule, watch, agent |
parent_execution_id |
int | null | Parent execution ID when triggered by run_agent |
session_id |
string | null | Conversation the execution belongs to (null outside a chat) |
started_at |
string | ISO 8601 timestamp |
finished_at |
string | null | ISO 8601 timestamp |
duration_ms |
int | null | Execution duration |
total_tokens |
int | null | Total token usage |
cost_usd |
float | null | Estimated cost |
error |
string | null | Error message if failed |
output_data |
string | null | Final answer text, stored whole (detail endpoint only) |
The agent trigger type indicates the execution was started by another agent via the run_agent tool. Use parent_execution_id to trace delegation chains.
SSE event types: message, meta, tool_call, tool_result, ai, final_answer, complete, waiting, shutdown
shutdown is terminal but not a result. Both streams emit it and close when the
backend is shutting down, so that a stream can never hold the process open (uvicorn will
not exit while a connection is live). It says nothing about the execution: reconnect after
the backend is back up and the stream replays from the log as usual.
Per-execution stream payloads include identity fields:
| Field | Meaning |
|---|---|
agent_id |
Canonical stem |
display_name |
Human label |
agent_name |
Same as agent_id (compat alias; not the display label) |
meta subtypes — the SSE event name is always meta. The payload field
event_type distinguishes three kinds of frame (clients must branch on it):
event_type |
When | Payload fields |
|---|---|---|
execution_info |
Once, at the start of every run (before other log frames) | agent_id, display_name, model (provider/name), trigger, args (list), session_id (null outside a chat), plus identity aliases |
stats |
After every AI message during the run | input_tokens, output_tokens, total_tokens, cost_usd (accumulated; may be null when pricing is unknown), plus identity |
meta |
Skill-context injection on the live bus | content plus identity (legacy; not the same as the two above) |
Both execution_info and stats are persisted in execution_logs and replayed on
reconnect the same way as every other log frame. A client that opens the stream after the
run has finished still receives them.
final_answer is never abridged. The answer is carried under content, identically
whether the frame comes from the live stream or from the catchup replay, and the same
string is stored in the execution's output_data. Replayed frames additionally carry the
raw log row under message. Read content. (An upper bound of 1,000,000 characters
exists as a backstop; it is far above any model's output limit.)
This matters because the stream deduplicates replay against live by event id: a replayed frame and its live twin share an id, so they must be the same string or a reconnecting client would be left with whichever arrived first.
Scheduler
| Method | Path | Description |
|---|---|---|
| GET | /scheduler |
Status + jobs with next_run |
| POST | /scheduler/pause |
Pause every trigger |
| POST | /scheduler/resume |
Resume every trigger |
Notes:
- Pause covers both trigger types: schedule agents stop firing and watch agents stop
reacting to file events. Manual runs (POST /agents/{id}/run) are unaffected — pausing
the scheduler does not disable the run button.
- Pause does not cancel an execution already running, and does not shut the backend down:
a paused workspace stays alive so it can be resumed.
Events
| Method | Path | Description |
|---|---|---|
| GET | /events/stream |
Global SSE event stream |
Event types:
| Event | When | Data fields |
|---|---|---|
heartbeat |
10s of inactivity | timestamp |
shutdown |
The backend is going down | reason |
execution_started |
Execution begins | execution_id, agent_id, display_name, agent_name (= id), trigger |
execution_completed |
Execution finishes | execution_id, agent_id, display_name, agent_name (= id), status, duration_ms |
agents_changed |
Agent file added/modified/deleted | event (loaded/updated/removed), agent_id, display_name, agent_name (= id) |
scheduler_changed |
Scheduler paused/resumed | status (paused/running) |
Connection:
Notes:
- No reconnection replay — on reconnect, use REST endpoints to sync state
- Heartbeat replaces /health polling — if the connection is alive, the backend is online
- The SSE connection keeps the backend alive (counts as active stream for idle timeout)
OpenAPI
Interactive docs available at /docs (Swagger) and /redoc when the backend is running.