Noddocs

WebSocket protocol

Real-time bidirectional communication between your agent and Nod

Connection

wss://api.asknod.ai/ws/agent/{agent_id}

The WebSocket provides real-time communication between your agent and the Nod backend. All messages are JSON-encoded. The first message after connecting must be an auth handshake.

The WebSocket is the preferred communication channel. It enables instant message delivery, live processing indicators, and real-time webhook triggers. REST endpoints are available as an alternative for simpler integrations.

Client → server messages

TypeDescriptionFields
pingKeep-alive ping
messageSend a chat messagetext, conversation_id?, task_run_id?, report_title?, report_description?
decisionCreate a decision requesttitle, description, kind, conversation_id?, options?, allows_always?, always_allow_label?, always_allow_options?
alertLog an activity eventtitle, text?, task_run_id?
agent_heartbeatSignal active processing (send every 1s)conversation_id?, status_text?
agent_idleSignal processing completeconversation_id?
statusLifecycle signalaction, details?

message

Send a chat message to the user. The message appears in the conversation view on the Nod app.

{
  "type": "message",
  "text": "Deployment complete! All tests passed.",
  "conversation_id": "conv_abc123"
}

To send a report (rendered as a compact card that opens full-screen), include report_title and report_description:

{
  "type": "message",
  "text": "# Full Report Content\n\n## Section 1\n...",
  "report_title": "Weekly Summary",
  "report_description": "Key metrics and highlights",
  "conversation_id": "conv_abc123"
}

To send a task result (rendered as a task bubble), include task_run_id:

{
  "type": "message",
  "text": "Found 3 open PRs that need review.",
  "task_run_id": "run_abc123",
  "conversation_id": "conv_abc123"
}

decision

Create a decision request that the user must respond to.

Approval (yes/no)

{
  "type": "decision",
  "title": "Deploy to production?",
  "description": "This will deploy build #142 to the production cluster.",
  "kind": "approval",
  "conversation_id": "conv_abc123"
}

Question (free text)

{
  "type": "decision",
  "title": "Project name?",
  "description": "What should I name the new project?",
  "kind": "question",
  "conversation_id": "conv_abc123"
}

Choice (multiple options)

{
  "type": "decision",
  "title": "Database choice",
  "description": "Which database should we use for this project?",
  "kind": "choice",
  "options": ["PostgreSQL", "SQLite", "MongoDB"],
  "conversation_id": "conv_abc123"
}

Permission (tool approval)

Permission decisions are typically created by hook systems (like our CLI's PreToolUse hook), not manually. They include allows_always and always_allow_options to let users persist permission patterns.

{
  "type": "decision",
  "title": "Allow: Bash(npm install stripe)",
  "description": "The agent wants to run: npm install stripe",
  "kind": "permission",
  "allows_always": true,
  "always_allow_label": "Always allow npm install",
  "always_allow_options": [
    { "pattern": "Bash(npm install *)", "label": "All npm installs" },
    { "pattern": "Bash(npm install stripe)", "label": "This exact command" }
  ],
  "conversation_id": "conv_abc123"
}

conversation_id

If conversation_id is null, the decision appears only in the Requests page — no chat bubble is created. This is used for task-originated decisions that don't belong to any conversation.

alert

Log an action to the user's activity feed.

{
  "type": "alert",
  "title": "Editing src/index.ts",
  "text": "/Users/alice/project/src/index.ts",
  "task_run_id": "run_abc123"
}

agent_heartbeat / agent_idle

Send agent_heartbeat every ~1 second while actively processing to show an indicator in the app. Send agent_idle when done.

Use status_text to customize what users see. If omitted, the app shows a default "Thinking" indicator.

// Default indicator ("Thinking")
{ "type": "agent_heartbeat", "conversation_id": "conv_abc123" }

// Custom status text
{ "type": "agent_heartbeat", "conversation_id": "conv_abc123", "status_text": "Searching the web..." }
{ "type": "agent_heartbeat", "conversation_id": "conv_abc123", "status_text": "Reading files..." }
{ "type": "agent_heartbeat", "conversation_id": "conv_abc123", "status_text": "Running tests..." }

// When done — clears the indicator
{ "type": "agent_idle", "conversation_id": "conv_abc123" }

ping

Send periodically (e.g., every 30s) to keep the connection alive. The server responds with pong.

{ "type": "ping" }

Server → client messages

TypeWhenKey fields
auth_okAuth handshake succeededsession_id, agent_name
pongResponse to ping
decision_ackDecision was createddecision_id
message_ackMessage was storedmessage_id
alert_ackAlert was logged
errorSomething went wrongmessage
user_messageUser sent a chat messagetext, sender_name, conversation_id, message_id, image_url?, image_urls?, audio_url?
decision_resolvedUser resolved a decision (approved, rejected, answered, or dismissed)decision_id, status, note?, title, description, conversation_id?, always_allow?, always_allow_pattern?
task_dueScheduled task is due to runtask_id, run_id, task_name, task_prompt, schedule
task_triggerWebhook triggered a tasktask_id, run_id, task_name, task_prompt, payload

user_message

Delivered when a user sends a chat message from the Nod app.

{
  "type": "user_message",
  "text": "Can you check the logs?",
  "sender_name": "Alice",
  "conversation_id": "conv_abc123",
  "message_id": "msg_xyz789",
  "image_url": null,
  "image_urls": null,
  "audio_url": null
}

decision_resolved

Delivered when the user acts on a pending decision. Check the status field:approved, rejected, responded (question/choice answered — read the answer from the note field), or dismissed.

Approved
{
  "type": "decision_resolved",
  "decision_id": "dec_xyz",
  "status": "approved",
  "note": "Go ahead",
  "title": "Deploy to production?",
  "description": "Build #42 passed all tests.",
  "conversation_id": "conv_abc123",
  "always_allow": false,
  "always_allow_pattern": null
}
Question answered
{
  "type": "decision_resolved",
  "decision_id": "dec_xyz",
  "status": "responded",
  "note": "nod-payments-api",
  "title": "Project name?",
  "description": "What should I name the new project?",
  "conversation_id": "conv_abc123"
}

task_due

Delivered when a scheduled task is due to run. The server has already created the run — your agent just needs to execute the prompt and call POST /api/agent/tasks/run/complete when done.

{
  "type": "task_due",
  "task_id": "tsk_xyz",
  "run_id": "run_abc123",
  "task_name": "Daily standup summary",
  "task_prompt": "Summarize yesterday's git commits and open PRs",
  "schedule": "0 9 * * 1-5"
}

task_trigger

Delivered when an external webhook triggers a task. See inbound webhooks.

{
  "type": "task_trigger",
  "task_id": "tsk_xyz",
  "run_id": "run_abc123",
  "task_name": "Process GitHub push",
  "task_prompt": "Handle the incoming push event",
  "payload": {
    "action": "push",
    "ref": "refs/heads/main",
    "commits": [...]
  }
}

error

{
  "type": "error",
  "message": "Decision creation failed: missing title"
}

Connection lifecycle

Recommended patterns

ConcernRecommendation
Keep-aliveSend ping every 30 seconds
Processing indicatorSend agent_heartbeat every 1 second while working; send agent_idle when done
ReconnectionExponential backoff starting at 1s, max 30s, up to 10 attempts
Auth timeoutExpect auth_ok within 10 seconds or close and retry

Session management

When your agent connects, a runtime session is created on the backend. This tracks whether the agent is online and processing. The session is automatically cleaned up when the WebSocket disconnects. Only one active session per agent is allowed — a new connection replaces any existing one.

Message ordering

If you send multiple decisions concurrently, use a serialization queue. Thedecision_ack response has no correlation ID, so concurrent sends may cause ack handlers to cross-fire. Our CLI serializes decisions through a promise queue.