Cortex is a Kubernetes-native AI agent orchestrator. It manages Claude Code agents running in Docker containers, coordinates work through NATS JetStream, and exposes real-time updates via WebSocket.
The hierarchy is simple: Projects contain Agents. Each agent runs in its own K8s pod with a git workspace. You interact with agents through a REST API and receive real-time events over WebSocket.
A project maps to a git repository. Agents are scoped to a project.
{
"id": "uuid",
"name": "my-project",
"description": "...",
"repository_url": "https://github.com/org/repo.git",
"repository_branch": "main",
"default_model": "anthropic/claude-opus-4-6",
"max_agents": 10,
"agent_image": "...",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
An agent is a Claude Code instance running in a K8s pod. Each agent works on a git branch.
{
"id": "uuid",
"project_id": "uuid",
"agent_config_id": "uuid | null",
"spawned_by": "uuid | null",
"status": "pending | starting | ready | busy | terminating | terminated | failed | timeout",
"current_branch": "main",
"pod_name": "agent-xxx",
"pod_ip": "10.0.0.1",
"started_at": "timestamp | null",
"last_seen_at": "timestamp | null",
"terminated_at": "timestamp | null",
"created_at": "timestamp",
"updated_at": "timestamp"
}
Status values:
| Status | Active? | Meaning |
|---|---|---|
pending | yes | Pod requested, waiting for K8s |
starting | yes | Pod running, containers initializing |
ready | yes | Agent is idle, accepts commands |
busy | yes | Processing a prompt or task |
terminating | no | Shutdown in progress, saving work |
terminated | no | Gone. Terminal state. |
failed | no | Pod crashed or errored |
timeout | no | Heartbeat lost (90s threshold) |
A reusable configuration template for agents. Defines the system prompt, model, and tools.
{
"id": "uuid",
"name": "code-reviewer",
"description": "Reviews PRs",
"prompt": "You are a code reviewer...",
"model": "anthropic/claude-opus-4-6",
"tools": [{}],
"chrome_enabled": false,
"created_at": "timestamp",
"updated_at": "timestamp"
}
A named, reusable task with a Go text/template prompt and optional JSON result schema.
{
"id": "uuid",
"slug": "analyze-pr",
"name": "Analyze Pull Request",
"description": "...",
"prompt_template": "Analyze PR #{{.number}} in {{.repo}}...",
"result_schema": { "type": "object", "properties": { ... } },
"created_at": "timestamp",
"updated_at": "timestamp"
}
The output of executing a task on an agent.
{
"id": "uuid",
"task_definition_id": "uuid",
"project_id": "uuid",
"agent_id": "uuid",
"params": { "number": 42, "repo": "org/repo" },
"data": { "summary": "...", "score": 8 },
"status": "pending | running | completed | failed",
"error": "",
"created_at": "timestamp",
"completed_at": "timestamp | null"
}
Auto-executes a task when a matching event fires. Can be global or project-scoped.
{
"id": "uuid",
"scope": "global | project",
"project_id": "uuid | null",
"task_definition_id": "uuid",
"task_definition_slug": "analyze-pr",
"pattern": "ai.git.push.completed",
"filter": { "data.branch": "main" },
"debounce_seconds": 5,
"enabled": true,
"created_at": "timestamp"
}
All events use CloudEvents. This is the wire format for WebSocket messages and persisted history.
{
"specversion": "1.0",
"type": "ai.opencode.message.updated",
"source": "/projects/{projectID}/agents/{agentID}",
"id": "uuid",
"time": "2026-01-01T00:00:00Z",
"subject": "",
"datacontenttype": "application/json",
"data": { ... }
}
+---------+
(create) ---->| pending |-----> failed
+----+----+ |
| |
v |
+---------+ |
|starting |-----> failed
+----+----+
|
v
+---------+ +---------+
| ready |<----->| busy |
+----+----+ +----+----+
| |
any active state |
| | |
v v v
+------------+
|terminating | (heartbeat lost)
+-----+------+ |
| v
v +---------+
+------------+ | timeout |
| terminated | +---------+
+------------+
Key rules:
- ready <-> busy is the normal work cycle
- Any active state can go to: terminating, failed, timeout
- terminating can ONLY go to terminated (protected)
- terminated is final, no way back
Full endpoint documentation with request/response schemas: Swagger UI (raw JSON spec)
const ws = new WebSocket("ws://HOST/ws?clientId=my-frontend-123");
clientId is required. If a new connection uses the same ID, the old one is closed.
ws.send(JSON.stringify({
specversion: "1.0",
type: "cortex.subscribe",
source: "/clients/my-frontend-123",
id: crypto.randomUUID(),
data: {
projects: ["project-uuid-1"],
filter: {
agents: ["agent-uuid-1"], // optional: only these agents
event_types: ["ai.opencode.*"] // optional: wildcard supported
},
since: "last-known-event-id" // optional: replay missed events
}
}));
Server responds with cortex.subscribe.ack, then (if since was set) replays up to 1000 events followed by cortex.replay.complete.
ws.send(JSON.stringify({
specversion: "1.0",
type: "cortex.unsubscribe",
source: "/clients/my-frontend-123",
id: crypto.randomUUID(),
data: { projects: ["project-uuid-1"] }
}));
Set subject to {projectID}/{agentID}:
ws.send(JSON.stringify({
specversion: "1.0",
type: "ai.agent.command.prompt",
source: "/clients/my-frontend-123",
id: crypto.randomUUID(),
subject: "project-uuid/agent-uuid",
data: {
prompt: "Fix the failing tests",
session_id: "optional-session-id"
}
}));
Server responds with cortex.command.ack. Then you receive agent events as they happen.
| Type | Data |
|---|---|
ai.agent.command.prompt | { prompt, session_id?, model? } |
ai.agent.command.abort | { session_id } |
ai.agent.command.shutdown | { reason? } |
ai.agent.command.session.create | { title? } |
ai.agent.command.git.clone | { url, branch?, path? } |
ai.agent.command.git.checkout | { branch, create? } |
ai.agent.command.git.pull | { remote?, branch? } |
ai.agent.command.git.push | { remote?, branch?, force? } |
ai.agent.command.git.commit | { message, paths? } |
ai.agent.command.git.merge | { branch, strategy? } |
| Type | Description |
|---|---|
ai.agent.started | Pod started |
ai.agent.ready | Agent ready to accept commands |
ai.agent.busy | Agent is working |
ai.agent.idle | Agent finished, back to idle |
ai.agent.error | Something went wrong |
ai.agent.heartbeat | Periodic heartbeat with status + branch |
ai.agent.work.saved | Work committed and pushed to git |
ai.agent.timeout | Heartbeat lost, agent timed out |
| Type | Description |
|---|---|
ai.opencode.session.created | New session started |
ai.opencode.session.updated | Session metadata changed |
ai.opencode.session.deleted | Session removed |
ai.opencode.session.status | Status change (busy/idle/retry) |
ai.opencode.session.idle | Session done processing — primary completion signal |
ai.opencode.session.error | Session error |
ai.opencode.message.updated | Message content updated |
ai.opencode.message.part.updated | Streaming message chunk |
ai.opencode.permission.updated | Permission request created/resolved |
| Type | Description |
|---|---|
ai.git.clone.started | Clone in progress |
ai.git.clone.completed | Clone finished |
ai.git.clone.error | Clone failed |
ai.git.push.started | Push in progress |
ai.git.push.completed | Push finished |
ai.git.push.error | Push failed |
ai.git.checkout | Branch checkout |
ai.git.commit | New commit created |
ai.git.conflict | Merge conflict detected |
| Type | Description |
|---|---|
ai.agent.file.created | File created in workspace |
ai.agent.file.modified | File modified |
ai.agent.file.deleted | File deleted |
ai.agent.file.renamed | File renamed (has old_path) |
| Type | Description |
|---|---|
ai.agent.message.sent | Agent sent a message to another agent |
ai.agent.spawned | Agent spawned a new agent |
| Type | Description |
|---|---|
ai.agent.wakeup.scheduled | Periodic schedule created |
ai.agent.wakeup.triggered | Wakeup fired, prompt delivered |
ai.agent.wakeup.skipped | Wakeup skipped (agent was busy) |
ai.agent.wakeup.cancelled | Schedule cancelled |
ai.agent.wakeup.expired | Schedule expired naturally |
| Type | Description |
|---|---|
cortex.task.result.completed | Task result ready |
cortex.task.result.failed | Task failed |
| Type | Description |
|---|---|
ai.email.received | Email received for an agent |
| Type | Direction | Description |
|---|---|---|
cortex.subscribe | client → server | Subscribe to project events |
cortex.subscribe.ack | server → client | Subscription confirmed |
cortex.unsubscribe | client → server | Unsubscribe from project events |
cortex.command.ack | server → client | Command forwarded to agent |
cortex.replay.complete | server → client | Historical event replay finished |
// 1. Create project
POST /api/projects
{ "name": "my-app" }
// 2. Create agent
POST /api/projects/{projectID}/agents
{ "agent_config_id": "optional-config-id", "branch": "main" }
// Returns agent with status "pending"
// 3. Connect WebSocket and subscribe
ws = new WebSocket("ws://HOST/ws?clientId=frontend-1")
ws.send({ type: "cortex.subscribe", data: { projects: [projectID] } })
// 4. Wait for ai.agent.ready event
// 5. Send prompt
ws.send({
type: "ai.agent.command.prompt",
subject: "{projectID}/{agentID}",
data: { prompt: "Add input validation to the signup form" }
})
// 6. Watch for events:
// ai.opencode.message.part.updated (streaming response)
// ai.opencode.message.updated (complete message)
// ai.opencode.session.idle (agent done)
// 1. Create a task definition (once)
POST /api/tasks
{
"slug": "analyze-pr",
"name": "Analyze PR",
"prompt_template": "Review PR #{{.number}} and return a summary.",
"result_schema": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"score": { "type": "integer" }
},
"required": ["summary", "score"]
}
}
// 2. Execute on an agent
POST /api/projects/{projectID}/agents/{agentID}/tasks/analyze-pr/execute
{ "params": { "number": 42 } }
// Returns: { "result_id": "uuid" }
// 3. Poll for result (or listen for cortex.task.result.completed via WebSocket)
GET /api/projects/{projectID}/tasks/analyze-pr/results/{resultID}
// When status is "completed", data contains the structured result
// Keep track of the last event ID you received
let lastEventId = null;
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
lastEventId = event.id;
// handle event...
};
// On reconnect, pass "since" to replay missed events
ws.send({
type: "cortex.subscribe",
data: {
projects: [projectID],
since: lastEventId // replays up to 1000 events
}
});
// Wait for cortex.replay.complete, then you're caught up
Authorization: Bearer <token>. See GET /api/me to bootstrap user state.CORS_ALLOWED_ORIGINS env var (comma-separated origins). Allowed headers: Authorization, Content-Type, X-Org-Id.{ "error": "message" } with an appropriate HTTP status code.