Streaming & Realtime with Surf
Build commands that stream progressive output via SSE and push real-time events to AI agents via WebSocket with session-scoped delivery.
When to Use Streaming
Not every command needs streaming. Use it for operations that take more than a few hundred milliseconds and where the agent benefits from progressive results: AI text generation, large data exports, complex computations, or real-time monitoring.
Request Flow
Streaming Commands
Mark a command as streaming with stream: true and use ctx.emit() to send progressive chunks:
import { createSurf } from '@surfjs/core'ย const surf = await createSurf({ name: 'AI Writer', commands: { // Streaming text generation generate: { description: 'Generate text with streaming output', stream: true, params: { prompt: { type: 'string', required: true }, maxTokens: { type: 'number', default: 500 }, model: { type: 'string', default: 'gpt-4' }, }, run: async ({ prompt, maxTokens, model }, ctx) => { const stream = await openai.chat.completions.create({ model, messages: [{ role: 'user', content: prompt }], max_tokens: maxTokens, stream: true, })ย for await (const chunk of stream) { const text = chunk.choices[0]?.delta?.content if (text) { // ctx.emit() sends an SSE chunk to the client ctx.emit!({ text, done: false }) } }ย // Return value is sent as the final "done" event return { done: true, model } }, },ย // Streaming data export 'export': { description: 'Export large datasets with progress', stream: true, auth: 'required', params: { table: { type: 'string', required: true }, format: { type: 'string', enum: ['json', 'csv'], default: 'json' }, }, run: async ({ table, format }, ctx) => { const total = await db.count(table) let processed = 0ย for await (const batch of db.streamBatches(table, 100)) { processed += batch.length ctx.emit!({ rows: format === 'csv' ? toCsv(batch) : batch, progress: Math.round((processed / total) * 100), }) }ย return { total: processed, format } }, }, },})๐ก Tip: Both the command definition must have stream: true and the client request must include stream: true for SSE to activate.
Client-Side Streaming
The client SDK handles SSE streams automatically:
import { SurfClient } from '@surfjs/client'ย const client = await SurfClient.discover('https://ai-writer.com')ย // Execute returns the final result after streaming completesconst result = await client.execute('generate', { prompt: 'Explain quantum computing in 3 sentences',})// result => { done: true, model: 'gpt-4' }ย // For raw HTTP SSE access:const response = await fetch('https://ai-writer.com/surf/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'generate', params: { prompt: 'Hello world' }, stream: true, }),})ย // Parse SSE eventsconst reader = response.body.getReader()const decoder = new TextDecoder()while (true) { const { done, value } = await reader.read() if (done) break const text = decoder.decode(value) // data: { "type": "chunk", "data": { "text": "Hello" } } // data: { "type": "done", "result": { "done": true } }}WebSocket & Events
For real-time bidirectional communication, Surf supports WebSocket connections. Define events in your config and emit them from anywhere in your code:
import { createSurf } from '@surfjs/core'import http from 'node:http'import express from 'express'ย const surf = await createSurf({ name: 'Ops Dashboard', // Define events with scoping events: { 'metrics.update': { description: 'Real-time metrics push', scope: 'session', // Only the subscribing session receives this data: { cpu: { type: 'number' }, memory: { type: 'number' }, timestamp: { type: 'number' }, }, }, 'alert.triggered': { description: 'System alert', scope: 'broadcast', // All connected clients receive this data: { severity: { type: 'string' }, message: { type: 'string' }, }, }, }, commands: { status: { description: 'Get current system status', hints: { idempotent: true, sideEffects: false }, run: async () => ({ cpu: await metrics.cpu(), memory: await metrics.memory(), requests: await metrics.requestsPerMinute(), uptime: process.uptime(), }), }, 'metrics.stream': { description: 'Stream live metrics via SSE', stream: true, params: { interval: { type: 'number', default: 1000, description: 'Interval in ms' }, }, run: async ({ interval }, ctx) => { for (let i = 0; i < 60; i++) { ctx.emit!({ cpu: await metrics.cpu(), memory: await metrics.memory(), timestamp: Date.now(), }) await sleep(interval) } return { streamed: 60 } }, }, },})ย const app = express()app.use(express.json())app.use(surf.middleware())ย const server = http.createServer(app)surf.wsHandler(server) // Attach WebSocket transport (requires 'ws' package)ย server.listen(3000)ย // Emit events from your application logicsetInterval(async () => { surf.emit('metrics.update', { cpu: await metrics.cpu(), memory: await metrics.memory(), timestamp: Date.now(), })}, 5000)ย // Broadcast alerts to all connected clientsalerting.on('alert', (alert) => { surf.emit('alert.triggered', { severity: alert.severity, message: alert.message, })})Event Scoping โ Security Feature
Event scoping is a key security feature that controls who receives each event. Surf supports three scopes:
| Scope | Delivery | Example |
|---|---|---|
| session | Only the triggering session | Cart updates, user metrics |
| global | All subscribers (inc. server-side) | System announcements |
| broadcast | All connected sessions | Stock updates, live alerts |
โ ๏ธ Security: Events default to session scope, preventing one user from receiving another user's updates. Only use broadcast for non-sensitive data.
Agent WebSocket Client
Agents connect via WebSocket to execute commands in real-time and receive event push notifications:
import { SurfClient } from '@surfjs/client'ย const client = await SurfClient.discover('https://ops-dashboard.com')ย // Connect via WebSocketconst ws = await client.connect()ย // Execute commands over WebSocketconst status = await ws.execute('status', {})console.log('CPU:', status.cpu)ย // Listen for eventsws.on('alert.triggered', (data) => { console.log(`[ALERT] ${data.severity}: ${data.message}`)})ย ws.on('metrics.update', (data) => { console.log(`CPU: ${data.cpu}%, Memory: ${data.memory}%`)})ย // Clean upclient.disconnect()Browser Local Commands
Surf Live handles real-time state sync from the server. For instant UI reactions that don't need a network round-trip, pair it with useSurfCommands() from @surfjs/react:
useSurfCommandsuseSurfState() โ syncs state pushed from the server in real time (Surf Live)useSurfCommands() โ registers local handlers that execute instantly in the browserimport { useSurfCommands, useSurfState } from '@surfjs/react'ย function Dashboard() { // Real-time state from server via Surf Live const [metrics] = useSurfState('metrics', { users: 0 })ย // Local commands โ no server call, instant execution useSurfCommands({ 'ui.highlight': { mode: 'local', run: (params) => { // Instant UI update, no server round-trip highlightElement(params.elementId) return { ok: true } }, }, })ย return <div>Online: {metrics.users}</div>}When an agent calls window.surf.execute('ui.highlight', { elementId: 'row-42' }), the @surfjs/web runtime intercepts it locally โ the WebSocket connection is used only for server-pushed state updates.
Summary
Surf's streaming and event system lets you build real-time AI agent integrations with minimal code. Use SSE streaming with ctx.emit() for progressive output, WebSocket for bidirectional communication, and event scoping for secure, session-aware delivery. Pair useSurfState() with useSurfCommands() to keep real-time sync separate from instant local execution.
Ask your agent
Copy these prompts into Claude, OpenClaw, or any AI agent
Add Surf streaming to my Node.js app. Install @surfjs/core and use await createSurf() to define a "generate" command with stream: true that uses ctx.emit() to send chunks. Also define events with session scoping for real-time updates. Mount on Express with surf.middleware() and attach WebSocket with surf.wsHandler(server).
My Next.js app has a live feed. Add Surf with await createSurf() including: a streaming "generate" command using ctx.emit(), event definitions with session/broadcast scopes, and WebSocket support via surf.wsHandler(). Use @surfjs/core. Also install @surfjs/react and use useSurfCommands() + useSurfState() in a client component for instant browser-side execution alongside real-time state sync.
Using @surfjs/client, connect to http://localhost:3000 and call the "generate" command with { prompt: "explain Surf in 3 sentences" }. Then connect via WebSocket and listen for events.
๐ก Works with OpenClaw, Claude Code, Cursor, Codex, and any agent that can make HTTP requests.