When to Use WebSocket vs REST
Use WebSocket When:
- You need live agent thoughts and reasoning as tasks execute
- Building real-time dashboards or interactive UIs
- You want sub-second event notifications
- You need immediate guardrail detection
- Building applications where users watch tasks execute live
Use REST API When:
- Simple integrations where polling is acceptable
- Serverless environments (Lambda, Vercel)
- Stateless workflows
- You don’t need real-time updates
Rule of thumb: If you’re building a UI where users watch tasks execute, use WebSocket. Otherwise, REST is simpler.
Socket.IO Setup and Connection
Installation
npm install socket.io-client
Basic Connection
import { io } from "socket.io-client";
// Step 1: Create a session via REST API
const session = await fetch("https://connect.enigma.click/start/start-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_KEY"
},
body: JSON.stringify({
taskDetails: "Go to google.com and search for Anthropic"
})
}).then(r => r.json());
// Step 2: Connect to WebSocket with sessionId
const socket = io("https://connect.enigma.click", {
auth: { sessionId: session.sessionId },
transports: ["websocket"]
});
// Step 3: Listen for events
socket.on("connect", () => {
console.log("✓ Connected to session");
});
socket.on("message", (data) => {
console.log("Event:", data.type, data);
});
socket.on("disconnect", () => {
console.log("✗ Disconnected");
});
Event Types
agent
Live agent thoughts and reasoning as the task executes.
Event:
{
"type": "agent",
"content": "I'll navigate to the search box and enter the query..."
}
Handler:
socket.on("message", (data) => {
if (data.type === "agent") {
console.log("💭 Agent:", data.content);
}
});
action
Browser action performed by the agent.
Event:
{
"type": "action",
"data": {
"name": "click_element",
"target": "search button"
}
}
Handler:
socket.on("message", (data) => {
if (data.type === "action") {
console.log("🎯 Action:", data.data.name);
}
});
task_completed
Task finished successfully.
Event:
{
"type": "task_completed",
"taskId": "x9y8z7w6v5u4",
"data": {
"message": "Successfully searched for Anthropic on Google",
"prompt_tokens": 15420,
"completion_tokens": 8230,
"total_tokens": 23650,
"completion_time": 45.3
},
"usage": {
"cost": 0.0234
}
}
Handler:
socket.on("message", (data) => {
if (data.type === "task_completed") {
console.log("✅ Done:", data.data.message);
console.log("💰 Cost:", data.usage.cost);
socket.disconnect();
}
});
guardrail_trigger
Agent needs human input to continue.
Event:
{
"type": "guardrail_trigger",
"data": {
"type": "human_input_needed",
"value": "I need login credentials to proceed"
}
}
Handler:
socket.on("message", (data) => {
if (data.type === "guardrail_trigger") {
console.log("⚠️ Guardrail:", data.data.value);
// Respond with required information
socket.emit("message", {
actionType: "guardrail",
taskDetails: "Username: demo@example.com, Password: demo123",
newState: "resume"
});
}
});
error
Task failed with an error.
Event:
{
"type": "error",
"error": "Navigation timeout after 30 seconds",
"code": "NAVIGATION_TIMEOUT"
}
Handler:
socket.on("message", (data) => {
if (data.type === "error") {
console.error("❌ Error:", data.error);
socket.disconnect();
}
});
end_session
Session terminated by server.
Event:
{
"type": "end_session",
"reason": "completed",
"message": "Session ended"
}
Reasons:
"completed" - Task completed successfully
"terminated" - User terminated session
"expired" - Session timeout (5 minutes)
"terminateOnCompletion" - Auto-terminated after task
"instance_lost" - Instance connection permanently lost
Handler:
socket.on("end_session", (data) => {
console.log("🔚 Session ended:", data.reason);
socket.disconnect();
});
instance:disconnected
Instance connection lost, entering 5-minute grace period.
Event:
{
"type": "instance:disconnected",
"message": "Instance connection lost. Attempting reconnection...",
"gracePeriod": 300000
}
Handler:
socket.on("instance:disconnected", (data) => {
console.warn("⚠️ Instance disconnected");
console.log("Grace period:", data.gracePeriod, "ms");
// Show "Reconnecting..." UI to user
});
instance:reconnected
Instance connection restored.
Event:
{
"type": "instance:reconnected",
"message": "Instance connection restored"
}
Handler:
socket.on("instance:reconnected", (data) => {
console.log("✅ Instance reconnected");
// Hide "Reconnecting..." UI
});
Complete Working Example
import { io } from "socket.io-client";
async function runTaskWithWebSocket(taskDetails, apiKey) {
return new Promise(async (resolve, reject) => {
// Step 1: Create session
const session = await fetch("https://connect.enigma.click/start/start-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
},
body: JSON.stringify({ taskDetails })
}).then(r => r.json());
console.log("Session created:", session.sessionId);
// Step 2: Connect to WebSocket
const socket = io("https://connect.enigma.click", {
auth: { sessionId: session.sessionId },
transports: ["websocket"]
});
// Step 3: Handle events
socket.on("connect", () => {
console.log("✓ Connected");
});
socket.on("message", (data) => {
switch (data.type) {
case "agent":
console.log("💭", data.content);
break;
case "action":
console.log("🎯", data.data.name);
break;
case "task_completed":
console.log("✅ Done:", data.data.message);
console.log("💰 Cost: $" + data.usage.cost);
socket.disconnect();
resolve(data);
break;
case "guardrail_trigger":
console.error("⚠️ Guardrail:", data.data.value);
socket.disconnect();
reject(new Error(`Guardrail: ${data.data.value}`));
break;
case "error":
console.error("❌ Error:", data.error);
socket.disconnect();
reject(new Error(data.error));
break;
}
});
socket.on("end_session", (data) => {
console.log("🔚 Session ended:", data.reason);
socket.disconnect();
});
socket.on("instance:disconnected", (data) => {
console.warn("⚠️ Instance disconnected, reconnecting...");
});
socket.on("instance:reconnected", () => {
console.log("✅ Instance reconnected");
});
socket.on("error", (err) => {
console.error("Socket error:", err);
reject(err);
});
socket.on("disconnect", () => {
console.log("✗ Disconnected");
});
});
}
// Usage
try {
const result = await runTaskWithWebSocket(
"Go to google.com and search for Anthropic",
"enig_xxx"
);
console.log("Result:", result.data.message);
} catch (error) {
console.error("Task failed:", error.message);
}
Reconnection Handling
Automatic Reconnection
Socket.IO automatically reconnects on connection loss. The Enigma server maintains session state during brief disconnections.
const socket = io("https://connect.enigma.click", {
auth: { sessionId: session.sessionId },
transports: ["websocket"],
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: 5
});
socket.on("reconnect", (attemptNumber) => {
console.log(`Reconnected after ${attemptNumber} attempts`);
});
socket.on("reconnect_attempt", (attemptNumber) => {
console.log(`Reconnection attempt ${attemptNumber}...`);
});
socket.on("reconnect_error", (error) => {
console.error("Reconnection error:", error);
});
socket.on("reconnect_failed", () => {
console.error("Reconnection failed after all attempts");
// Session may be lost
});
Instance Disconnection vs Socket Disconnection
Two types of disconnections:
1. Socket Disconnection (client-server connection)
- Handled automatically by Socket.IO
- Session state preserved
- Events resume after reconnection
2. Instance Disconnection (browser instance connection)
- Server loses connection to browser instance
- 5-minute grace period to recover
- You receive
instance:disconnected event
- If recovered:
instance:reconnected event
- If not recovered within 5 min: session terminates
let instanceDisconnected = false;
socket.on("instance:disconnected", (data) => {
instanceDisconnected = true;
console.warn("⚠️ Browser instance disconnected");
console.log(`Grace period: ${data.gracePeriod / 1000}s`);
// Show UI warning
showReconnectingUI();
});
socket.on("instance:reconnected", () => {
instanceDisconnected = false;
console.log("✅ Browser instance restored");
// Hide UI warning
hideReconnectingUI();
});
socket.on("end_session", (data) => {
if (data.reason === "instance_lost") {
console.error("❌ Instance could not be recovered");
// Notify user, restart task if needed
}
});
Error Handling
Comprehensive Error Handler
function setupErrorHandling(socket) {
// Socket.IO errors
socket.on("connect_error", (error) => {
console.error("Connection error:", error.message);
// Invalid sessionId, network issue, or server down
});
socket.on("error", (error) => {
console.error("Socket error:", error);
});
// Task errors
socket.on("message", (data) => {
if (data.type === "error") {
console.error("Task error:", data.error);
console.error("Code:", data.code);
// Handle specific error codes
switch (data.code) {
case "NAVIGATION_TIMEOUT":
console.log("Page took too long to load");
break;
case "ELEMENT_NOT_FOUND":
console.log("Agent couldn't find required element");
break;
case "SESSION_EXPIRED":
console.log("Session timed out");
break;
default:
console.log("Unknown error:", data.code);
}
socket.disconnect();
}
});
// Guardrail errors (unhandled)
socket.on("message", (data) => {
if (data.type === "guardrail_trigger") {
console.warn("Guardrail triggered but no handler provided");
console.warn("Agent says:", data.data.value);
// Optionally auto-stop
socket.emit("message", {
actionType: "guardrail",
taskDetails: "Cannot proceed",
newState: "stop"
});
}
});
}
Sending Commands
You can send commands to control the session via WebSocket:
Start New Task
socket.emit("message", {
actionType: "newTask",
newState: "start",
taskDetails: "Search for wireless keyboards",
maxDuration: 60000
});
Pause Task
socket.emit("message", {
actionType: "state",
newState: "pause"
});
Resume Task
socket.emit("message", {
actionType: "state",
newState: "resume"
});
Stop Task
socket.emit("message", {
actionType: "state",
newState: "stop"
});
Terminate Session
socket.emit("message", {
actionType: "state",
newState: "terminate"
});
Respond to Guardrail
socket.emit("message", {
actionType: "guardrail",
taskDetails: "Username: user@example.com, Password: pass123",
newState: "resume"
});
Manual Browser Control
// Take control
socket.emit("message", {
actionType: "interaction",
action: { type: "takeOverControl" }
});
// Click at coordinates
socket.emit("message", {
actionType: "interaction",
action: { type: "CLICK", x: 500, y: 300 }
});
// Type text
socket.emit("message", {
actionType: "interaction",
action: { type: "TYPE", text: "Hello world", humanLike: true }
});
// Press key
socket.emit("message", {
actionType: "interaction",
action: { type: "KEY_PRESS", key: "Enter" }
});
// Release control
socket.emit("message", {
actionType: "interaction",
action: { type: "releaseControl" }
});
Advanced Pattern: Multi-Task with Live Updates
import { io } from "socket.io-client";
class EnigmaWebSocketClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.socket = null;
this.sessionId = null;
}
async createSession(taskDetails) {
const response = await fetch("https://connect.enigma.click/start/start-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.apiKey}`
},
body: JSON.stringify({ taskDetails })
});
const session = await response.json();
this.sessionId = session.sessionId;
this.socket = io("https://connect.enigma.click", {
auth: { sessionId: this.sessionId },
transports: ["websocket"]
});
this.setupEventHandlers();
return session;
}
setupEventHandlers() {
this.socket.on("connect", () => {
console.log("✓ Connected");
});
this.socket.on("message", (data) => {
this.handleEvent(data);
});
this.socket.on("end_session", (data) => {
console.log("Session ended:", data.reason);
this.disconnect();
});
this.socket.on("instance:disconnected", () => {
console.warn("Instance disconnected, reconnecting...");
});
this.socket.on("instance:reconnected", () => {
console.log("Instance reconnected");
});
}
handleEvent(data) {
switch (data.type) {
case "agent":
this.onAgent(data.content);
break;
case "action":
this.onAction(data.data);
break;
case "task_completed":
this.onTaskCompleted(data);
break;
case "guardrail_trigger":
this.onGuardrail(data.data);
break;
case "error":
this.onError(data);
break;
}
}
onAgent(content) {
console.log("💭", content);
}
onAction(action) {
console.log("🎯", action.name);
}
onTaskCompleted(data) {
console.log("✅", data.data.message);
console.log("💰 Cost:", data.usage.cost);
}
onGuardrail(data) {
console.warn("⚠️ Guardrail:", data.value);
// Override this method to handle guardrails
}
onError(data) {
console.error("❌", data.error);
this.disconnect();
}
sendTask(taskDetails, terminateOnCompletion = false) {
return new Promise((resolve, reject) => {
this.socket.emit("message", {
actionType: "newTask",
newState: "start",
taskDetails,
terminateOnCompletion
});
const originalHandler = this.onTaskCompleted.bind(this);
this.onTaskCompleted = (data) => {
originalHandler(data);
resolve(data);
};
const originalErrorHandler = this.onError.bind(this);
this.onError = (data) => {
originalErrorHandler(data);
reject(new Error(data.error));
};
});
}
pause() {
this.socket.emit("message", {
actionType: "state",
newState: "pause"
});
}
resume() {
this.socket.emit("message", {
actionType: "state",
newState: "resume"
});
}
terminate() {
this.socket.emit("message", {
actionType: "state",
newState: "terminate"
});
}
disconnect() {
if (this.socket) {
this.socket.disconnect();
}
}
}
// Usage
const client = new EnigmaWebSocketClient("enig_xxx");
await client.createSession("Go to amazon.com");
await client.sendTask("Search for wireless keyboards");
await client.sendTask("Add first result to cart", true); // terminates after
Next Steps