Squads¶
A squad is a group of LLM agents that share one workspace directory, a memory bank, and a task list, and that run a planner-produced plan together. The Management API exposes the whole surface: 95 endpoints under /squads, /squad-discussions, /squad-templates, and /squad-registry, including a cross-squad and a per-squad Server-Sent Events stream, plus a per-execution event ledger.
Every endpoint on this page is reachable at the Management API base URL, http://127.0.0.1:8001/api/v1 by default, and appears in Swagger UI at /api/docs under the Squads, Squad Sessions, Squad Templates, and Squad Discussions tags. The aigo squad command group wraps the same endpoints; the flags are in the CLI reference.
How this surface behaves headless¶
Two things are worth knowing before you build against this surface.
Running a plan works the same headless. POST /squads/{id}/execute on a headless aigo-server validates the request, resolves the planner agent, runs the planner decomposition, stores the plan it produced, and, when autoApprove is set, starts the executor. POST /squads/{id}/executions/{eid}/approve starts the executor for a plan that was waiting. The desktop Tauri commands and these endpoints are thin wrappers over the same two shared services, so a run reaches the same states in the same order on both, and the intervention endpoints below act on a run that is actually moving: pause and resume hold and release it between task dispatches, steer is read by the executor before the next dispatch, and skip and retry address real tasks. Before issue #4954 this endpoint recorded an execution with an empty plan and started nothing, so on a headless server those controls acted on a run that never progressed. One difference remains, and it is a policy one rather than a capability one: a task's tool calls go through the same gates as POST /tools/execute, so a tool that needs the desktop app is refused with the reason the tool catalog gives instead of running.
Squads are available as MCP tools. Issue #4581 moved the squad tools' state access behind squad::runtime_handles, which both the desktop app and aigo-server install, so the MCP endpoint advertises them and POST /tools/execute accepts them in either mode. Thirteen tools are exposed: list_squads, list_squad_templates, create_squad, get_squad, list_squad_tasks, get_squad_execution, list_squad_executions, submit_squad_request, approve_squad_plan, reject_squad_plan, cancel_squad_execution, steer_squad_execution, and send_squad_agent_message. submit_squad_request and approve_squad_plan reach the same shared services the endpoints above do, so a request submitted through a tool is planned and an approved plan runs, on either runtime. Both report what happened rather than leaving the caller to assume it: submit_squad_request returns plannerStarted, and approve_squad_plan returns executorStarted. plannerStarted answers whether a planner actually decomposed the request, not whether the runtime called the submission path: run_planner_decomposition degrades to a one-task-per-agent split when the router is down, the planner agent has no usable model, or the planner reply creates no task, and a submission that landed on that split reports plannerStarted: false with plannerDegradedReason naming the cause (issue #4966). The reason stays on the execution, so get_squad_execution carries it on every later poll as well.
Authentication and scopes¶
Requests authenticate the same way as the rest of the Management API: an X-API-Key header, an Authorization: Bearer token, or the aigo_session cookie obtained from POST /api/v1/auth/login.
Access keys carry scopes, and the squad surface uses two:
agent_readfor everyGET.agent_writefor everyPOST,PUT,PATCH, andDELETE.
Two mutating routes are deliberately agent_read, because neither changes stored state: POST /squads/workspace/validate (it inspects a candidate directory path) and POST /squad-discussions/{id}/export (it renders a transcript). The authoritative table is ROUTE_MANIFEST in src-tauri/crates/aigo-rest/src/route_scope.rs; the scope column in the tables below is copied from it.
In a managed install, an administrator can hide the Squad page with the /squad page id in features.hiddenPages. That gate covers the /squads, /squad-discussions, /squad-templates, and /squad-registry route prefixes and the matching Tauri commands, so hiding the page also refuses the API, not just the UI. POST /squad-registry/install is in that list because it writes into the squad template store, and because leaving it out would have refused the operation over IPC while allowing it over HTTP.
Resource model¶
Squad¶
A squad is the top-level record: a name, a description, an agent lineup, an optional planner agent, a workspace path, and a status. Its id is a UUID v4. Squads are stored as <app_data_dir>/squads/<squad-uuid>.json with an index.json beside them for fast listing, and every write is an atomic write-to-temp-then-rename.
Agent¶
An agent is one member of the lineup: a name, a role (planner, developer, reviewer, writer, or a custom label), a model, a system prompt, instructions, a tool configuration, and an execution mode (in-process or container). An agent id is a UUID v4 the server generates when the request does not supply one, so an agent created from a template gets its id at creation time. plannerAgentId on the squad names the agent that plans and aggregates; the API refuses to remove that agent while it holds the role.
Workspace¶
The workspace is a directory on disk that the squad's agents share. POST /squads/{id}/workspace/init creates it with a fixed layout:
{workspace_path}/
|-- .squad.json # squad metadata and full config, so the directory is portable
|-- plans/ # planner-generated task plans
|-- tasks/ # per-task tracking files
|-- memory/ # per-agent memory bank files
|-- artifacts/ # output files and deliverables
|-- logs/ # execution logs and event ledgers
|-- sessions/ # persisted agent conversation history
Path traversal is rejected, symlinks are not followed when archiving or walking, and cleanup verifies that .squad.json is present before it deletes anything.
Task¶
A managed task has a title, a description, a priority, a status, an optional assignee, dependencies on other tasks, and a retry budget. Statuses are pending, ready, assigned, in_progress, review, done, failed, and cancelled. Tasks form a dependency graph that GET /squads/{id}/tasks/graph returns and that the planner uses to build execution waves.
Plan and execution¶
An execution is one run of one request. Submitting a request produces an execution id; the planner turns the request into a plan of tasks grouped into waves; the plan waits for approval unless the request asked for auto-approval; and the executor then runs one wave at a time. An execution carries a status, the current wave, per-task attempt counts, the tasks an operator skipped, and the operator instructions the run has received.
Agent session¶
Separate from plan execution, each agent can hold a 1:1 chat session. A session has its own conversation history persisted under sessions/, and a squad agent can have several stored sessions with one of them current.
Memory bank¶
Each agent has a Markdown memory file under memory/, divided into named sections. Writes append to a section by default and replace it when asked. GET /squads/{id}/memory/search searches across the squad's agents, with optional agent and section filters.
Discussion room¶
A discussion room is a moderated multi-agent conversation attached to a squad: agents take turns speaking under a strategy and a turn budget, users can enqueue messages between turns, and the room can be concluded into a structured summary and handed off to a squad execution.
Template¶
A squad template is a reusable lineup: agent definitions with roles, models, and prompts, without a workspace. Creating a squad from a template copies the lineup and selects the template's planner-role agent as the planner. Templates can be imported from a file, exported, saved from an existing squad, or installed from a shared registry catalog.
Budget¶
A budget caps a squad's token and cost consumption, with a warning threshold below the hard limit. The configuration is stored and readable, and GET /squads/{id}/budget/usage reports the active run's consumption against it. The two budget events below are declared but nothing emits them today, so do not build an alert on them; poll the usage endpoint instead.
Endpoints¶
Paths below omit the /api/v1 prefix. {id} is the squad id except under /squad-discussions, where it is the discussion room id.
Squad lifecycle¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
GET | /squads | agent_read | - | SquadIndexEntry[] |
POST | /squads | agent_write | CreateSquadRequest | Squad (201) |
GET | /squads/{id} | agent_read | - | Squad |
PUT | /squads/{id} | agent_write | UpdateSquadRequest | Squad |
DELETE | /squads/{id} | agent_write | ?keepWorkspace= (default true) | 204 |
POST | /squads/restore | agent_write | RestoreSquadRequest | RestoreSquadPreview |
DELETE /squads/{id} keeps the workspace directory by default; pass ?keepWorkspace=false to remove it as well. POST /squads/restore reads a workspace's .squad.json and returns a preview of the squad it would restore, so a caller can confirm before committing.
Workspace¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
POST | /squads/{id}/workspace/init | agent_write | InitWorkspaceRequest | WorkspaceInfo |
GET | /squads/{id}/workspace/status | agent_read | - | WorkspaceStatus |
GET | /squads/{id}/readiness | agent_read | - | SquadReadiness |
DELETE | /squads/{id}/workspace | agent_write | ?archive= (default false) | CleanupResult |
POST | /squads/workspace/validate | agent_read | ValidatePathRequest | ValidationResult |
GET /squads/{id}/readiness is the pre-flight model check, not a workspace check. available is true when the squad has at least one agent and every agent resolves a model; agents carries the same verdict per agent in squad order, and cause plus message say what is blocking. routerHealthy reports whether the inference router answered its health probe and is advisory: a false there does not lower available. Workspace existence and structure come from GET /squads/{id}/workspace/status instead.
Tasks¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
GET | /squads/tasks/summary | agent_read | - | SquadTaskSummary[] |
POST | /squads/{id}/tasks | agent_write | CreateTaskRequest | ManagedTask (201) |
GET | /squads/{id}/tasks | agent_read | ?status= | ManagedTask[] |
GET | /squads/{id}/tasks/graph | agent_read | - | TaskGraph |
GET | /squads/{id}/tasks/{task_id} | agent_read | - | ManagedTask |
PATCH | /squads/{id}/tasks/{task_id} | agent_write | UpdateTaskRequest | ManagedTask |
DELETE | /squads/{id}/tasks/{task_id} | agent_write | ?force= (default false) | 204 |
POST | /squads/{id}/tasks/{task_id}/retry | agent_write | RetryTaskRequest (optional) | ManagedTask |
POST | /squads/{id}/tasks/{task_id}/reassign | agent_write | ReassignTaskRequest (optional) | ManagedTask |
PATCH | /squads/{id}/tasks/{task_id}/status | agent_write | UpdateTaskStatusRequest | ManagedTask |
An edit is refused when the task is too far along: a running task accepts only description, priority, and maxRetries, and a finished one accepts nothing. Both refusals answer 409 with SQUAD_TASK_ACTIVE and SQUAD_TASK_TERMINAL respectively. DELETE on a running task needs ?force=true, which cancels it first; tasks that depended on the deleted one lose the dependency rather than becoming unsatisfiable. retry re-queues a failed or cancelled task and grants one extra attempt when force is set and the retry budget is spent.
Memory bank¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
POST | /squads/{id}/memory/init | agent_write | - | 204 |
GET | /squads/{id}/memory/search | agent_read | ?q=, ?agentFilter=, ?sectionFilter=, ?caseSensitive=, ?limit= | MemorySearchResult[] |
GET | /squads/{id}/memory/{agent_id} | agent_read | - | MemoryContent |
POST | /squads/{id}/memory/{agent_id} | agent_write | WriteMemoryRequest | 204 |
GET | /squads/{id}/memory/{agent_id}/sections | agent_read | - | string[] |
Agents¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
GET | /squads/{id}/agents | agent_read | - | AgentConfig[] |
POST | /squads/{id}/agents | agent_write | AddSquadAgentRequest | AgentConfig (201) |
GET | /squads/{id}/agents/{agent_id} | agent_read | - | AgentConfig |
PATCH | /squads/{id}/agents/{agent_id} | agent_write | UpdateSquadAgentRequest | UpdateSquadAgentResponse |
DELETE | /squads/{id}/agents/{agent_id} | agent_write | - | 204 |
POST | /squads/{id}/agents/bulk-update-model | agent_write | BulkUpdateAgentModelRequest | BulkUpdateAgentModelResult |
Removing an agent is refused with 409 in two cases: SQUAD_AGENT_IS_PLANNER when the agent is the squad's planner (clear plannerAgentId first), and SQUAD_AGENT_BUSY when the agent is mid-flight in a chat session or a running execution. Every add, update, and remove raises squad:agents-changed, so a Squad page left open picks up an edit made from the CLI or the API without a reload.
PATCH replaces whole objects rather than merging them: sending modelPreferences replaces the whole preference record, and sending toolConfig replaces the whole tool configuration. aigo squad agent set reads the agent first and merges for those two fields; a direct API caller has to do the same. settingsOverrides and containerConfig accept an explicit null to clear them, while omitting them leaves them alone.
Agent sessions and 1:1 chat¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
POST | /squads/{id}/agents/{agent_id}/session | agent_write | - | SessionInfo (201) |
DELETE | /squads/{id}/agents/{agent_id}/session | agent_write | - | 204 |
GET | /squads/{id}/agents/{agent_id}/status | agent_read | - | AgentSessionStatus |
POST | /squads/{id}/agents/{agent_id}/message | agent_write | SendMessageRequest | SendAgentMessageResponse |
PUT | /squads/{id}/agents/{agent_id}/response | agent_write | RecordResponseRequest | 200 |
GET | /squads/{id}/agents/{agent_id}/conversation | agent_read | - | PersistedSession \| null |
GET | /squads/{id}/agents/{agent_id}/sessions | agent_read | - | SessionIndexEntry[] |
POST | /squads/{id}/agents/{agent_id}/sessions | agent_write | - | new session id (201) |
GET | /squads/{id}/agents/{agent_id}/sessions/{session_id} | agent_read | - | PersistedSession \| null |
DELETE | /squads/{id}/agents/{agent_id}/sessions/{session_id} | agent_write | - | 204 |
GET | /squads/{id}/agents/{agent_id}/chat-system-prompt | agent_read | ?maxTokens= | AgentChatSystemPrompt |
Execution and its controls¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
POST | /squads/{id}/execute | agent_write | SubmitExecutionRequest | { executionId } (201) |
GET | /squads/{id}/executions/{eid} | agent_read | - | SquadExecution |
POST | /squads/{id}/executions/{eid}/approve | agent_write | ApprovePlanRequest (optional) | 200 |
POST | /squads/{id}/executions/{eid}/reject | agent_write | RejectPlanRequest | 200 |
DELETE | /squads/{id}/executions/{eid} | agent_write | - | 200 |
POST | /squads/{id}/executions/{eid}/pause | agent_write | - | SquadExecution |
POST | /squads/{id}/executions/{eid}/resume | agent_write | - | SquadExecution |
POST | /squads/{id}/executions/{eid}/steer | agent_write | SteerExecutionRequest | SteerMessage |
POST | /squads/{id}/executions/{eid}/tasks/{task_id}/skip | agent_write | - | SquadExecution |
POST | /squads/{id}/executions/{eid}/tasks/{task_id}/retry | agent_write | - | SquadExecution |
Every control acts between tasks. A task already running is never interrupted: pausing lets it finish and holds the run before the next dispatch, and skipping applies only to a task that has not started. The refusals are 409 with SQUAD_EXECUTION_NOT_PAUSABLE, SQUAD_EXECUTION_NOT_RUNNING, SQUAD_EXECUTION_NOT_STEERABLE, SQUAD_EXECUTION_TASK_NOT_SKIPPABLE, and SQUAD_EXECUTION_TASK_NOT_RETRIABLE.
approve optionally carries a plan override in planOverride. An override is a replacement, not a patch: a stored task the override does not name is removed, a task carrying an id the stored plan holds keeps that identity and its recorded status, a task carrying an id the stored plan does not hold is added under that id, and one with no id is added under a generated one.
Choosing the id is what lets one override introduce several tasks and order them, because dependsOn is resolved against the ids of the resulting plan rather than against the stored one. A chosen id may hold only ASCII letters, digits, and hyphens, and at most 128 bytes: it travels back as a URL path segment on the task skip and retry endpoints, and when the run starts the squad's board writes the task as <id>.json and <id>.md in the workspace's tasks directory. A few names are therefore refused outright: index, because the board keeps its own index.json in that directory, and the Windows device names (CON, PRN, AUX, NUL, COM1 to COM9, LPT1 to LPT9). No two tasks of the resulting plan may share an id, or differ only by case, because macOS and Windows would make those one file; naming a stored id is not a collision, it is the instruction to keep that task. Assignees must be agents of the squad, every dependsOn entry must name a task the override itself leaves in the plan, and a cycle, including a task naming itself, is rejected before the run starts.
A chosen id must also be free on the squad's board. The board is per squad rather than per run: it lives in the workspace and outlives every execution, so an id an earlier run of the same squad already put there is refused with a message naming the id and the plan holding it, plus that plan's run when the server still has it in memory. Reusing a memorable id such as draft across two runs of one squad is therefore an error you see at approve time, rather than a silent overwrite of the earlier run's recorded status, result and error. An id the approving run's own plan already holds on the board is not a collision: that is the same plan re-syncing, which is what a run recovered after a restart does. A row created directly on the board with POST /squads/{id}/tasks and no planId belongs to no run and cannot be claimed either; delete it first if you want the id.
A steer instruction is standing by default and is appended to every later task turn. The body is { "message": "...", "scope": {...} }, and scope is internally tagged: omit it for the whole run, or send {"type": "task", "taskId": "..."} or {"type": "agent", "agentId": "..."} to narrow it. The request rejects unknown fields, so a bare top-level taskId answers 400.
Read How this surface behaves headless before building an operator flow on these.
Workspace files¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
GET | /squads/{id}/workspace/files | agent_read | ?path= | FileEntry[] |
GET | /squads/{id}/workspace/files/content | agent_read | ?path= | FileContent |
GET | /squads/{id}/workspace/search | agent_read | ?q= | WorkspaceSearchResult[] |
?path= is workspace-relative. A .. component is rejected, and the resolved path is then canonicalized and required to stay inside the workspace root, so a symlink pointing outside it is refused.
Activity log¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
GET | /squads/{id}/activity-log | agent_read | ?limit=, ?offset= | ActivityLogResponse |
GET | /squads/{id}/activity-log/load | agent_read | ?limit=, ?offset= | ActivityLogResponse |
GET /squads/{id}/activity-log reads the in-memory ring buffer, which is empty after a server restart. GET /squads/{id}/activity-log/load first reloads logs/events.jsonl from the workspace, so it is the one to use for a squad the server has not been running.
History and the event ledger¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
GET | /squads/{id}/history | agent_read | ?limit=, ?offset= | ExecutionRecord[] |
GET | /squads/{id}/history/{eid} | agent_read | - | ExecutionRecord |
GET | /squads/{id}/history/{eid}/logs | agent_read | ?agentId=, ?minLevel=, ?limit= | LogEntry[] |
GET | /squads/{id}/history/{eid}/events | agent_read | ?eventTypes=, ?afterSeq=, ?limit= | ExecutionEventPage |
POST | /squads/{id}/history/{eid}/report | agent_write | - | Report file path |
GET /squads/{id}/history/{eid}/events reads the per-execution typed ledger, logs/{execution-id}.events.jsonl. One line per squad:* event that named the execution, in emission order, each carrying a 1-based seq, so a reader can resume with ?afterSeq=. ?eventTypes= takes a comma-separated list of event names. A run is capped at a maximum number of recorded events and a maximum byte size, whichever comes first; on reaching either cap the writer appends one truncation record naming which cap it hit, so a truncated ledger says so instead of ending mid-run.
The squad-wide logs/events.jsonl behind the activity log is a different file: it holds only the most recent events across all runs and is not keyed by execution.
Analytics, budget, emergency stop, event stream¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
GET | /squads/{id}/analytics | agent_read | ?period= | SquadAnalytics |
GET | /squads/{id}/budget | agent_read | - | BudgetConfig |
PUT | /squads/{id}/budget | agent_write | BudgetConfig | 200 |
GET | /squads/{id}/budget/usage | agent_read | - | BudgetUsage |
POST | /squads/{id}/emergency-stop | agent_write | - | 200 |
GET | /squads/{id}/events | agent_read | ?types= | text/event-stream |
GET /squads/{id}/events is a Server-Sent Events stream of the squad's live events. ?types= filters it to a comma-separated list of event names, for example ?types=squad:task-completed,squad:execution-failed. A name outside the squad: family is rejected with 400 naming it, rather than opening a connection that never delivers anything. POST /squads/{id}/emergency-stop cancels every non-terminal execution for the squad and raises squad:emergency-stopped.
Templates¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
GET | /squad-templates | agent_read | - | SquadTemplate[] |
GET | /squad-templates/{id} | agent_read | - | SquadTemplate |
POST | /squad-templates/import | agent_write | ImportTemplateRequest | SquadTemplate (201) |
GET | /squad-templates/{id}/export | agent_read | - | ExportTemplateResponse |
DELETE | /squad-templates/{id} | agent_write | - | 204 |
POST | /squads/{id}/save-as-template | agent_write | SaveAsTemplateRequest | SquadTemplate (201) |
POST | /squad-registry/install | agent_write | InstallSquadTemplateRequest | InstallSquadTemplateResult |
Discussion rooms¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
POST | /squad-discussions | agent_write | CreateDiscussionRequest | DiscussionRoom (201) |
GET | /squad-discussions/{id} | agent_read | - | DiscussionRoom |
DELETE | /squad-discussions/{id} | agent_write | - | 204 |
GET | /squads/{id}/discussions | agent_read | ?limit=, ?offset= | DiscussionSummary[] |
GET | /squads/{id}/discussions/completed | agent_read | ?limit=, ?offset= | DiscussionSummary[] |
POST | /squad-discussions/{id}/start | agent_write | - | 200 |
POST | /squad-discussions/{id}/pause | agent_write | - | 200 |
POST | /squad-discussions/{id}/resume | agent_write | - | 200 |
POST | /squad-discussions/{id}/stop | agent_write | - | 200 |
POST | /squad-discussions/{id}/messages | agent_write | PostDiscussionMessageRequest | PostDiscussionMessageResponse |
DELETE | /squad-discussions/{id}/messages/{message_id} | agent_write | - | { queueLength } |
PUT | /squad-discussions/{id}/mode | agent_write | SetDiscussionModeRequest | 204 |
PUT | /squad-discussions/{id}/turn-budget | agent_write | SetTurnBudgetRequest | 204 |
PUT | /squad-discussions/{id}/strategy | agent_write | SetDiscussionStrategyRequest | 204 |
POST | /squad-discussions/{id}/conclusion | agent_write | SynthesizeDiscussionConclusionRequest (optional) | DiscussionConclusion |
POST | /squad-discussions/{id}/handoff | agent_write | - | DiscussionHandoffRequest |
POST | /squad-discussions/{id}/export | agent_read | ExportDiscussionTranscriptRequest (optional) | ExportDiscussionTranscriptResponse |
GET | /squad-discussions/{id}/analytics | agent_read | - | DiscussionAnalytics |
GET /squads/{id}/discussions returns lightweight summaries without transcripts; fetch the full room with GET /squad-discussions/{id} when a user opens one. Both listings page with ?limit= (default 50, capped at 100) and ?offset=.
Event stream¶
| Method | Path | Scope | Body or query | Returns |
|---|---|---|---|---|
GET | /squads/events | agent_read | ?types= | text/event-stream |
GET | /squads/ws | agent_read | ?types=, ?since= | WebSocket (101) |
GET | /squads/{id}/ws | agent_read | ?types=, ?since= | WebSocket (101) |
/squads/events and /squads/ws are registered before /squads/{id}, so their last segments are read as literals rather than squad ids. /squads/ws is the WebSocket twin of the cross-squad SSE stream. /squads/{id}/ws is the WebSocket twin of the per-squad SSE stream, and unlike it supports replay. See Events and Event Streams.
Events¶
Squad events reach a client through the cross-squad SSE or WebSocket stream, the per-squad SSE or WebSocket stream, and, for events that named an execution, the durable ledger at GET /squads/{id}/history/{eid}/events. The same names are emitted as Tauri events in the desktop app, so a payload is identical on both transports. Payload fields are camelCase.
GET /squads/events and GET /squads/ws carry every squad:* name from every squad, including the squad:discussion_* family, and require agent_read, the same scope as GET /squads. Use them when the client does not know the squad ids yet, or watches several at once; use a per-squad stream when it does. None carries anything outside squad:*, and GET /events remains the cross-domain stream requiring admin. ?types= narrows to a comma-separated subset, and a name outside the squad: family is refused with 400 rather than accepted into a connection that silently delivers nothing. Both WebSocket routes refuse it before the socket exists. The cross-squad pair and per-squad WebSocket honor a resume cursor and report a truncated replay with stream:gap and a lagging consumer with stream:lagged; replayed events pass the same domain filter the live stream applies. See Event Streams.
This stream works on the desktop app's own embedded Management API too. The squad emitter is repointed at that server when it starts (issue #4889), so a turn started from the desktop UI is visible on SSE. /schedules/events, /memory/events, and /data/events do the same since issue #5040, which replaced the webview-only emitter each of those subsystems built for itself with one shared sink the embedded server attaches to.
Execution-scoped payloads carry executionId and task-scoped ones carry taskId, so a client can correlate a stream event with a run without tracking state of its own.
Execution lifecycle¶
| Event | Payload | Emitted when |
|---|---|---|
squad:planning-started | PlanningStartedPayload | The planner begins analyzing a request. |
squad:plan-ready | PlanReadyPayload | A plan is ready for approval. |
squad:execution-started | ExecutionStartedPayload | Execution starts, after approval or auto-approval. |
squad:task-wave-started | TaskWaveStartedPayload | A new wave of parallel tasks begins. |
squad:task-completed | TaskCompletedPayload | A task finishes, successfully or not. |
squad:aggregation-started | AggregationStartedPayload | The planner starts aggregating results. |
squad:execution-completed | ExecutionCompletedPayload | The run finishes successfully. |
squad:execution-failed | ExecutionFailedPayload | The run fails. |
Execution controls¶
| Event | Payload | Emitted when |
|---|---|---|
squad:execution-paused | ExecutionPausedPayload | An operator pauses a running execution. |
squad:execution-resumed | ExecutionResumedPayload | An operator resumes a paused execution. |
squad:execution-steered | ExecutionSteeredPayload | An operator instruction is accepted. |
squad:task-skipped | TaskSkippedPayload | An operator skips a task that has not started. |
Agent sessions¶
| Event | Payload | Emitted when |
|---|---|---|
squad:agent-session-started | AgentSessionStartedPayload | An agent session starts. |
squad:agent-state-changed | AgentStateChangedPayload | An agent's state changes. |
squad:agent-stream-chunk | AgentStreamChunkPayload | A streaming token chunk arrives from an agent. |
squad:agent-stream-completed | AgentStreamCompletedPayload | Streaming from an agent completes. |
squad:agent-error | AgentErrorPayload | An agent hits an error. |
squad:agent-tool-call | AgentToolCallPayload | An agent is about to run a tool, in a 1:1 chat turn or in a plan execution task. Execution-path payloads carry executionId and taskId; chat-turn payloads omit both. On the execution path each string inside toolCall.arguments is bounded (4,000 characters, with a truncation marker) and the serialized whole is capped at 8,000; the tool received the arguments unmodified. |
squad:agent-tool-result | AgentToolResultPayload | A tool an agent called returns. On the execution path toolResult.output is a bounded copy (4,000 characters, with a truncation marker); the agent still received the full output. |
Squad configuration¶
| Event | Payload | Emitted when |
|---|---|---|
squad:agents-changed | SquadAgentsChangedPayload | An agent is added, updated, or removed. change is added, updated, or removed. |
Task lifecycle¶
| Event | Payload | Emitted when |
|---|---|---|
squad:task-created | TaskCreatedPayload | A task is created. |
squad:task-status-changed | TaskStatusChangedPayload | A task's status changes. |
squad:task-assigned | TaskAssignedPayload | A task is assigned to an agent. |
squad:task-failed | TaskFailedPayload | A task fails. |
squad:task-updated | TaskUpdatedPayload | A task's editable fields change. Carries the whole task, because one edit can move several fields. |
squad:task-deleted | TaskDeletedPayload | A task is deleted. |
Resources and state¶
| Event | Payload | Emitted when |
|---|---|---|
squad:token-usage-update | TokenUsageUpdatePayload | Token usage changes (debounced). |
squad:execution-token-usage | ExecutionTokenUsagePayload | After each LLM round trip during plan execution, carrying the run's cumulative usage. |
squad:memory-updated | MemoryUpdatedPayload | An agent's memory is written. |
squad:workspace-file-changed | WorkspaceFileChangedPayload | A workspace file changes (debounced). |
Budget and safety¶
| Event | Payload | Emitted when |
|---|---|---|
squad:budget-warning | BudgetWarningPayload | Declared for the warning threshold, and not emitted by anything today. |
squad:budget-exceeded | BudgetExceededPayload | Declared for a budget limit being exceeded, and not emitted by anything today. |
squad:emergency-stopped | EmergencyStoppedPayload | The emergency stop is triggered. |
Container output¶
| Event | Payload | Emitted when |
|---|---|---|
squad:container-output | ContainerOutputPayload | A structured output block is parsed from container stdout. |
squad:container-log | ContainerLogPayload | A non-marker debug line arrives on container stdout. |
squad:container-status | ContainerStatusPayload | A container's execution status changes. |
Discussion rooms¶
Discussion events use underscores after the prefix, unlike the hyphenated names above.
| Event | Payload | Emitted when |
|---|---|---|
squad:discussion_message | DiscussionMessagePayload | A message is appended to the shared log. |
squad:discussion_turn_started | DiscussionTurnStartedPayload | An agent turn begins. |
squad:discussion_turn_ended | DiscussionTurnEndedPayload | An agent turn finishes, successfully or with an error. |
squad:discussion_status_changed | DiscussionStatusChangedPayload | The orchestrator status transitions. |
squad:discussion_queue_changed | DiscussionQueueChangedPayload | The pending user-message queue changes (push, drain, cancel). |
squad:discussion_conclusion_synthesized | DiscussionConclusionSynthesizedPayload | A conclusion is synthesized. |
squad:discussion_turn_delta | DiscussionTurnDeltaPayload | A streaming content chunk arrives during a turn. A live preview only; the committed squad:discussion_message is the source of truth for the transcript. |
squad:discussion_turn_tool_call | DiscussionTurnToolCallPayload | The current speaker is about to run a tool. |
squad:discussion_turn_tool_result | DiscussionTurnToolResultPayload | A tool the current speaker called returns. |
Walkthroughs¶
The examples use an access key in X-API-Key. Drop the header if the server runs on loopback with Require API key off.
Create a squad from a template¶
BASE=http://127.0.0.1:8001/api/v1
KEY=<your-access-key>
# Pick a template
curl -s -H "X-API-Key: $KEY" "$BASE/squad-templates" | jq -r '.[] | "\(.id)\t\(.name)"'
# Create the squad from it
SQUAD=$(curl -s -X POST "$BASE/squads" \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"name":"docs-team","workspacePath":"/tmp/docs-team","templateId":"builtin-fullstack-dev-team"}' \
| jq -r .id)
# Create the workspace directory
curl -s -X POST "$BASE/squads/$SQUAD/workspace/init" \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"path":"/tmp/docs-team"}' | jq .
Add an agent¶
AGENT=$(curl -s -X POST "$BASE/squads/$SQUAD/agents" \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"name":"editor","role":{"type":"reviewer"},"modelPreferences":{"preferredModelId":"qwen3-8b"}}' \
| jq -r .id)
curl -s -H "X-API-Key: $KEY" "$BASE/squads/$SQUAD/agents" \
| jq -r '.[] | "\(.id)\t\(.name)\t\(.role.type)"'
Submit a request and approve the plan¶
EXEC=$(curl -s -X POST "$BASE/squads/$SQUAD/execute" \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"request":"Draft the release notes for 1.13"}' \
| jq -r .executionId)
# Read the plan once the planner has produced it
curl -s -H "X-API-Key: $KEY" "$BASE/squads/$SQUAD/executions/$EXEC" | jq '.plan.tasks'
# Approve it as planned
curl -s -X POST "$BASE/squads/$SQUAD/executions/$EXEC/approve" -H "X-API-Key: $KEY"
# Or approve with a replacement task list
curl -s -X POST "$BASE/squads/$SQUAD/executions/$EXEC/approve" \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d @plan-override.json
The same call does the same work on a headless aigo-server: the planner runs, the plan is stored, and autoApprove starts the executor. See How this surface behaves headless.
Watch events¶
# Everything for this squad
curl -N -H "X-API-Key: $KEY" "$BASE/squads/$SQUAD/events"
# Only the two that matter for a progress bar
curl -N -H "X-API-Key: $KEY" \
"$BASE/squads/$SQUAD/events?types=squad:task-completed,squad:execution-completed"
Steer, then read the report¶
curl -s -X POST "$BASE/squads/$SQUAD/executions/$EXEC/pause" -H "X-API-Key: $KEY" | jq .status
curl -s -X POST "$BASE/squads/$SQUAD/executions/$EXEC/steer" \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"message":"Keep the public API unchanged"}' | jq .
curl -s -X POST "$BASE/squads/$SQUAD/executions/$EXEC/resume" -H "X-API-Key: $KEY" | jq .status
# Replay what the run actually did
curl -s -H "X-API-Key: $KEY" \
"$BASE/squads/$SQUAD/history/$EXEC/events?limit=200" | jq -r '.events[] | "\(.seq)\t\(.event)"'
# Generate the Markdown report. The response is the report's path on the
# server's filesystem, not the report body, so a remote caller gets a path it
# cannot open; read the run through the events endpoint above instead.
curl -s -X POST "$BASE/squads/$SQUAD/history/$EXEC/report" -H "X-API-Key: $KEY" | jq -r .
See also¶
- CLI reference for the
aigo squadcommand group. - MCP endpoint for exposing Backend.AI GO's tools to external MCP clients.
- External access for binding the Management API to a non-loopback address safely.