Kandev Docs

WebSocket API

Reference for Kandev WebSocket messages and API behavior.

This page documents the WebSocket envelope, current domain model, and the request actions used by the main task and agent workflow. It is not a substitute for the Go request types: payloads evolve with the application.

Scope and source of truth

Kandev uses one persistent WebSocket connection for browser request/response traffic and real-time notifications. This applies to WebSocket-backed operations only; Kandev also has dedicated HTTP APIs. For example, workflow import and export use HTTP routes because they transfer portable documents.

Use these sources when checking an action:

An action constant by itself does not guarantee a request handler. Some constants are server-to-client notifications only.

Local endpoint: ws://localhost:38429/ws

Use wss:// when Kandev is served over HTTPS.

Message envelope

Request

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "request",
  "action": "workflow.list",
  "payload": {
    "workspace_id": "workspace-uuid"
  },
  "timestamp": "2026-07-15T09:00:00Z"
}

Response

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "response",
  "action": "workflow.list",
  "payload": {
    "workflows": [],
    "total": 0
  },
  "timestamp": "2026-07-15T09:00:00Z"
}

Notification

{
  "type": "notification",
  "action": "task.updated",
  "payload": {
    "id": "task-uuid",
    "title": "Update authentication"
  },
  "timestamp": "2026-07-15T09:00:01Z"
}

Error

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "error",
  "action": "task.get",
  "payload": {
    "code": "NOT_FOUND",
    "message": "Task not found"
  },
  "timestamp": "2026-07-15T09:00:01Z"
}
FieldTypeContract
idstringCorrelates a request with its response or error. Notifications omit it.
typestringrequest, response, notification, or error.
actionstringRegistered action name.
payloadobjectAction-specific request or response data.
timestampstringISO 8601 timestamp. Server responses, notifications, and errors always include it. Client requests may omit it; the backend does not use it for request correlation.
metadataobjectOptional string-to-string metadata used by selected internal flows.

Error codes currently include BAD_REQUEST, NOT_FOUND, INTERNAL_ERROR, UNAUTHORIZED, FORBIDDEN, VALIDATION_ERROR, CONFLICT, and UNKNOWN_ACTION.

Current domain model

flowchart LR
    Workspace --> Workflow
    Workflow --> WorkflowStep["Workflow step"]
    WorkflowStep --> Task
    Task --> Session
    Session --> Message
    Session --> Agent

The common request flow is:

  1. List or create a workspace.
  2. List or create a workflow in that workspace.
  3. List the workflow steps.
  4. Create a task with workspace_id, workflow_id, and workflow_step_id.
  5. Subscribe to the task and session for live updates.
  6. Start work with session.launch.
  7. Send user turns with message.add, or use message.queue.* while a turn is active.
  8. Stop a session with session.stop, or update the task with task.state.

Registered request actions

The tables below cover the core task workflow. Check actions.go and handler registration for integrations and less common operational actions.

Workspaces and workflows

ActionPurpose
workspace.listList workspaces.
workspace.createCreate a workspace.
workspace.getGet a workspace by ID.
workspace.updateUpdate a workspace.
workspace.deleteDelete a workspace.
workflow.listList workflows, optionally scoped by workspace_id.
workflow.createCreate a workflow in a workspace.
workflow.getGet a workflow by ID.
workflow.updateUpdate a workflow.
workflow.deleteDelete a workflow.
workflow.reorderReorder workflows in a workspace.
workflow.template.listList workflow templates.
workflow.template.getGet a workflow template.
workflow.step.listList steps for a workflow.
workflow.step.getGet a workflow step.
workflow.step.createCreate a workflow's steps from a template.
workflow.history.listList workflow history for a session.

Direct create, update, delete, and reorder operations for individual workflow steps are exposed through the HTTP workflow API and configuration-mode MCP tools. They are not registered as ordinary WebSocket requests.

Tasks

ActionPurpose
task.listList tasks for a workflow.
task.createCreate a task.
task.getGet a task.
task.updateUpdate task fields.
task.repository.updateUpdate a task repository's base branch.
task.deleteDelete a task.
task.moveMove a task to a workflow step.
task.stateSet the task state.
task.archiveArchive or unarchive a task.
task.session.listList sessions for a task.
task.session.statusGet task session status.
task.plan.createCreate a task plan.
task.plan.getGet a task plan.
task.plan.updateUpdate a task plan.
task.plan.deleteDelete a task plan.
task.plan.revisions.listList plan revisions.
task.plan.revision.getGet a plan revision.
task.plan.revertRevert a plan revision.

Sessions, messages, and agents

ActionPurpose
session.launchUnified prepare, start, resume, workflow-step, or workspace-restore operation.
session.ensureReturn an existing task session or create one.
session.recoverResume, fresh-start, or cancel a transient retry.
session.reset_contextReset an agent session's context.
session.stopStop a session.
session.deleteDelete a session.
session.set_primarySet the primary session for a task.
session.set_plan_modeEnable or disable plan mode.
message.addPersist a user message and forward it to the session when appropriate.
message.listList session messages.
message.searchSearch messages.
message.queue.addQueue a message for a busy session.
message.queue.getRead queued messages and status.
message.queue.updateReplace queued message content.
message.queue.appendAppend content to a queued message.
message.queue.drainDispatch one queued entry when the session can accept it.
message.queue.removeRemove one queued entry.
message.queue.cancelClear the session queue.
agent.listList agent executions.
agent.launchLow-level agent launch. Prefer session.launch for task sessions.
agent.statusGet agent status.
agent.logsGet agent logs.
agent.stopStop an agent execution.
agent.cancelCancel the active agent turn.
agent.typesList available agent types.

Agent and task-session lifecycle states are uppercase on the wire. For example, agent states include PENDING, STARTING, RUNNING, READY, COMPLETED, FAILED, and STOPPED.

Orchestrator and subscriptions

Only these orchestrator request actions are registered:

ActionPurpose
orchestrator.statusGet orchestrator status.
orchestrator.queueGet queued tasks.
orchestrator.stopStop execution for a task.

Lifecycle starts and user turns use session.launch and message.add; task completion uses task.state or workflow automation.

Subscription requests are handled by the WebSocket gateway:

ActionPayload key
task.subscribe / task.unsubscribetask_id
session.subscribe / session.unsubscribesession_id
session.focus / session.unfocussession_id
user.subscribe / user.unsubscribeuser-scoped connection state
run.subscribe / run.unsubscriberun_id
system.metrics.subscribe / system.metrics.unsubscribeno resource ID

Core payload examples

List workflow steps

{
  "id": "request-steps",
  "type": "request",
  "action": "workflow.step.list",
  "payload": {
    "workflow_id": "workflow-uuid"
  },
  "timestamp": "2026-07-15T09:01:00Z"
}

Create a task

workspace_id, workflow_id, and title are required. workflow_step_id selects the initial step and should be supplied by normal clients.

{
  "id": "request-create-task",
  "type": "request",
  "action": "task.create",
  "payload": {
    "workspace_id": "workspace-uuid",
    "workflow_id": "workflow-uuid",
    "workflow_step_id": "step-uuid",
    "title": "Implement feature X",
    "description": "Detailed requirements",
    "priority": "high",
    "repositories": [
      {
        "repository_id": "repository-uuid",
        "base_branch": "main"
      }
    ]
  },
  "timestamp": "2026-07-15T09:02:00Z"
}

To create and immediately start a task, also send start_agent: true and a valid agent_profile_id. The response then includes session_id and agent_execution_id when launch succeeds.

Move a task

{
  "id": "request-move-task",
  "type": "request",
  "action": "task.move",
  "payload": {
    "id": "task-uuid",
    "workflow_id": "workflow-uuid",
    "workflow_step_id": "target-step-uuid",
    "position": 0
  },
  "timestamp": "2026-07-15T09:03:00Z"
}

Launch a session

session.launch accepts these intents: prepare, start, start_created, resume, workflow_step, and restore_workspace. If intent is omitted, the backend infers it from the other fields.

{
  "id": "request-launch-session",
  "type": "request",
  "action": "session.launch",
  "payload": {
    "task_id": "task-uuid",
    "intent": "start",
    "agent_profile_id": "profile-uuid",
    "prompt": "Implement the task and run focused tests"
  },
  "timestamp": "2026-07-15T09:04:00Z"
}
{
  "id": "request-launch-session",
  "type": "response",
  "action": "session.launch",
  "payload": {
    "success": true,
    "task_id": "task-uuid",
    "session_id": "session-uuid",
    "agent_execution_id": "execution-uuid",
    "state": "RUNNING"
  },
  "timestamp": "2026-07-15T09:04:01Z"
}

Send a user turn

{
  "id": "request-add-message",
  "type": "request",
  "action": "message.add",
  "payload": {
    "task_id": "task-uuid",
    "session_id": "session-uuid",
    "content": "Also add regression coverage"
  },
  "timestamp": "2026-07-15T09:05:00Z"
}

Mark a task complete

{
  "id": "request-complete-task",
  "type": "request",
  "action": "task.state",
  "payload": {
    "id": "task-uuid",
    "state": "COMPLETED"
  },
  "timestamp": "2026-07-15T09:06:00Z"
}

Notifications

Common server notifications include:

  • Task lifecycle: task.created, task.updated, task.deleted, task.state_changed.
  • Workflow lifecycle: workflow.created, workflow.updated, workflow.deleted, workflow.step.created, workflow.step.updated, workflow.step.deleted.
  • Session messages and state: session.message.added, session.message.updated, session.message.deleted, session.state_changed, session.waiting_for_input, session.turn.started, session.turn.completed.
  • Agent stream compatibility events: acp.progress, acp.log, acp.result, acp.error, acp.status, acp.heartbeat.
  • Permission flow: permission.requested; clients answer with permission.respond. Its payload requires session_id and pending_id, plus option_id unless cancelled is true. The optional rejected flag distinguishes an explicit denial from dismissing the request.
  • Repository and executor lifecycle events matching the *.created, *.updated, and *.deleted action constants.

Subscribe to the relevant task and session before depending on resource-scoped notifications.

Browser request helper

type PendingRequest = {
  resolve: (message: unknown) => void;
  reject: (error: Error) => void;
};

const pending = new Map<string, PendingRequest>();
const socket = new WebSocket('ws://localhost:38429/ws');

socket.addEventListener('message', (event) => {
  const message = JSON.parse(event.data);
  const request = message.id ? pending.get(message.id) : undefined;
  if (request) {
    pending.delete(message.id);
    request.resolve(message);
  }
});

function rejectPending(reason: string) {
  const error = new Error(reason);
  for (const request of pending.values()) request.reject(error);
  pending.clear();
}

socket.addEventListener('close', () => rejectPending('WebSocket closed'));
socket.addEventListener('error', () => rejectPending('WebSocket failed'));

async function waitForSocketOpen() {
  if (socket.readyState === WebSocket.OPEN) return;
  if (socket.readyState !== WebSocket.CONNECTING) {
    throw new Error('WebSocket is not open');
  }

  await new Promise<void>((resolve, reject) => {
    const cleanup = () => {
      socket.removeEventListener('open', handleOpen);
      socket.removeEventListener('error', handleError);
      socket.removeEventListener('close', handleClose);
    };
    const handleOpen = () => {
      cleanup();
      resolve();
    };
    const handleError = () => {
      cleanup();
      reject(new Error('WebSocket failed to open'));
    };
    const handleClose = () => {
      cleanup();
      reject(new Error('WebSocket closed before opening'));
    };

    socket.addEventListener('open', handleOpen);
    socket.addEventListener('error', handleError);
    socket.addEventListener('close', handleClose);
  });
}

async function request(action: string, payload: Record<string, unknown>) {
  await waitForSocketOpen();

  const id = crypto.randomUUID();
  return new Promise((resolve, reject) => {
    pending.set(id, { resolve, reject });

    try {
      socket.send(JSON.stringify({
        id,
        type: 'request',
        action,
        payload,
        timestamp: new Date().toISOString(),
      }));
    } catch (error) {
      pending.delete(id);
      reject(error instanceof Error ? error : new Error('WebSocket send failed'));
    }
  });
}

await request('task.subscribe', { task_id: 'task-uuid' });
const launch = await request('session.launch', {
  task_id: 'task-uuid',
  intent: 'start',
  agent_profile_id: 'profile-uuid',
  prompt: 'Implement the task',
});

Production clients should also enforce request timeouts and route type: "error" responses into typed errors.

HTTP operations that remain separate

Workflow document transfer is intentionally HTTP-based:

MethodRoutePurpose
GET/api/v1/workflows/:id/exportExport one workflow.
GET/api/v1/workspaces/:id/workflows/exportExport workflows from a workspace.
POST/api/v1/workspaces/:id/workflows/importImport a portable workflow document.

Other focused HTTP routes also exist for snapshots, file transfer, health checks, and agentctl-local control APIs. Do not infer that a WebSocket action replaces every HTTP endpoint.

On this page