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.
Client → server messages
| Type | Description | Fields |
|---|---|---|
| ping | Keep-alive ping | — |
| message | Send a chat message | text, conversation_id?, task_run_id?, report_title?, report_description? |
| decision | Create a decision request | title, description, kind, conversation_id?, options?, allows_always?, always_allow_label?, always_allow_options? |
| alert | Log an activity event | title, text?, task_run_id? |
| agent_heartbeat | Signal active processing (send every 1s) | conversation_id?, status_text? |
| agent_idle | Signal processing complete | conversation_id? |
| status | Lifecycle signal | action, 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
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
| Type | When | Key fields |
|---|---|---|
| auth_ok | Auth handshake succeeded | session_id, agent_name |
| pong | Response to ping | — |
| decision_ack | Decision was created | decision_id |
| message_ack | Message was stored | message_id |
| alert_ack | Alert was logged | — |
| error | Something went wrong | message |
| user_message | User sent a chat message | text, sender_name, conversation_id, message_id, image_url?, image_urls?, audio_url? |
| decision_resolved | User resolved a decision (approved, rejected, answered, or dismissed) | decision_id, status, note?, title, description, conversation_id?, always_allow?, always_allow_pattern? |
| task_due | Scheduled task is due to run | task_id, run_id, task_name, task_prompt, schedule |
| task_trigger | Webhook triggered a task | task_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.
{
"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
}{
"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
| Concern | Recommendation |
|---|---|
| Keep-alive | Send ping every 30 seconds |
| Processing indicator | Send agent_heartbeat every 1 second while working; send agent_idle when done |
| Reconnection | Exponential backoff starting at 1s, max 30s, up to 10 attempts |
| Auth timeout | Expect 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
decision_ack response has no correlation ID, so concurrent sends may cause ack handlers to cross-fire. Our CLI serializes decisions through a promise queue.