Sending Events
Events are the way your app sends work to AI agents. Once connected to Relay via WebSocket, you send structured event messages to request AI involvement.
Event Message Format
Every event sent to Relay must follow this structure:
{
"type": "event",
"agent_id": "athena",
"thread_id": "task-123",
"payload": {
"event": "comment.mention",
"message": "@athena summarize this task",
"sender": { "id": "user-456", "name": "Christian Garcia" },
"task_id": "task-123",
"task_title": "Q2 Roadmap Review",
"project": "Smiling Group Platform"
}
}
Required Fields
| Field | Type | Example | Notes |
|---|---|---|---|
type | string | "event" | Always "event" |
agent_id | string | "athena" | Which agent to involve. Lowercase, unique within organization. |
thread_id | string | "task-123" | Your unique identifier for this conversation thread. Used for session continuity. |
payload | object | { ... } | Your app-specific data. Sent untouched to the agent. Max 64KB. |
The Payload Field
The payload object is sacred — Relay never parses, validates, or modifies it. Whatever you send is forwarded directly to the agent in the event message.
Design your payload for the agent, not for Relay. Include all context the agent needs:
{
"payload": {
"event": "comment.mention",
"message": "@athena summarize this task",
"sender": {
"id": "user-456",
"name": "Christian Garcia"
},
"task_id": "task-123",
"task_title": "Q2 Roadmap Review",
"description": "Review Q2 timeline and deliverables",
"priority": "high",
"status": "in_progress",
"comments": [
{
"author": "John Doe",
"text": "We need to finalize the timeline",
"created_at": "2026-04-01T10:30:00Z"
}
]
}
}
Payload Size Limit
The maximum payload size is 64KB. If you exceed this, Relay returns an error with code PAYLOAD_TOO_LARGE. Check your payload size before sending:
import json
payload = { ... }
payload_size = len(json.dumps(payload).encode('utf-8'))
if payload_size > 64 * 1024:
raise ValueError(f"Payload too large: {payload_size} bytes")
Designing Thread IDs
The thread_id is critical for session continuity. It determines how the agent's working memory is organized. Choose something meaningful for your use case:
Portal (task-based):
thread_id = f"task-{task_id}" # "task-123"
All comments on the same task share one agent session. The agent builds context across all comments.
Academy (lesson-based):
thread_id = f"lesson-{lesson_id}" # "lesson-101"
All student questions about the same lesson stay in one session.
Chat app (channel-based):
thread_id = f"channel-{channel_id}-{user_id}" # "channel-general-user-456"
Each user's questions in a channel are one session (or channel-{channel_id} if shared).
One-off event:
import uuid
thread_id = f"event-{uuid.uuid4()}" # Each request is a new session
Choose a thread_id format that reflects your domain. Once set, keep it consistent — the agent will recognize when you send another event for the same thread_id and resume context.
Sending Events
Python Example
import asyncio
import websockets
import json
async def send_event(websocket, agent_id, thread_id, payload):
event = {
"type": "event",
"agent_id": agent_id,
"thread_id": thread_id,
"payload": payload
}
await websocket.send(json.dumps(event))
print(f"Sent event to {agent_id} for thread {thread_id}")
# Usage
async def main():
token = "rlk_yourapp_x8k2m9p..."
uri = "wss://api.relay.ckgworks.com/v1/ws/app"
async with websockets.connect(uri, extra_headers={"Authorization": f"Bearer {token}"}) as ws:
payload = {
"event": "comment.mention",
"message": "@athena summarize this task",
"task_id": "task-123",
"task_title": "Q2 Roadmap Review"
}
await send_event(ws, "athena", "task-123", payload)
asyncio.run(main())
Node.js Example
function sendEvent(ws, agentId, threadId, payload) {
const event = {
type: 'event',
agent_id: agentId,
thread_id: threadId,
payload: payload
};
ws.send(JSON.stringify(event));
console.log(`Sent event to ${agentId} for thread ${threadId}`);
}
// Usage
const ws = new WebSocket(uri, {
headers: { Authorization: `Bearer ${token}` }
});
ws.on('open', () => {
const payload = {
event: 'comment.mention',
message: '@athena summarize this task',
task_id: 'task-123',
task_title: 'Q2 Roadmap Review'
};
sendEvent(ws, 'athena', 'task-123', payload);
});
Response Format
When Relay receives your event, it immediately responds with an acceptance message:
{
"type": "accepted",
"event_id": "evt_k9p2m",
"agent_id": "athena",
"session_key": "relay:athena:portal:task-123",
"status": "accepted"
}
The event_id is Relay's unique identifier for this event. Store it to correlate subsequent token streams and the final reply.
After the accepted response, you will receive:
- Zero or more
tokenmessages (streaming reply) - One
replymessage (final response) - Or one
errormessage (if something went wrong)
All three message types will include the same event_id so you know which event they belong to.
Best Practices
Do
- Include all relevant context in payload
- Use consistent, domain-specific thread_ids
- Wait for "accepted" before showing feedback to user
- Store event_id to correlate replies
- Validate payload size before sending
Don't
- Send the same event twice in rapid succession
- Use random or non-meaningful thread_ids
- Rely on Relay to validate your payload
- Send sensitive information Relay doesn't need
- Exceed 64KB payload size
Error Cases
If Relay cannot accept your event, you will receive an error message instead of "accepted":
{
"type": "error",
"event_id": null,
"agent_id": "athena",
"error": "Payload too large: 70000 bytes exceeds 65536 limit",
"code": "PAYLOAD_TOO_LARGE"
}
Common errors:
AGENT_NOT_ALLOWED: Your app isn't allowlisted for this agentAGENT_OFFLINE: Agent is not currently connectedPAYLOAD_TOO_LARGE: Event payload exceeds 64KBRATE_LIMITED: You've exceeded your rate limitINVALID_EVENT: Required fields missing or malformed
See Error Handling for details on each error code.