Web & WebSocket Skills
Web
Start an HTTP server that dispatches requests to route skills.
Setup
Add web to your main skill's metadata with a routes: array mapping HTTP methods and paths to skills:
metadata:
paths: [./skills]
web:
port: 4000
routes:
- method: GET
path: /
ref: home
- method: GET
path: /users/:id
ref: user-detail
Individual skills can also declare themselves as web endpoints using route: (singular) — the ref is implicit (the skill's own name):
# In a route skill's own metadata
metadata:
web:
route:
method: GET
path: /users/:id
URL patterns support :param segments that are extracted into args.route.
Request Args
Route skills receive structured args:
{
"route": { "id": "42" },
"query": { "page": "2" },
"body": { "title": "Hello" },
"headers": { "content-type": "application/json" },
"method": "GET",
"path": "/users/42",
"sessionId": "uuid-..."
}
route— extracted URL pattern parametersquery— URL query string parametersbody— parsed request body (JSON or form-encoded)sessionId— auto-generated UUID cookie, persisted across requests
Response Handling
Code skills return values directly:
- String →
text/htmlwith status 200 - Object →
application/jsonwith status 200
Markdown skills (agent-driven) get a returns schema injected automatically with this shape:
{ "status": 200, "headers": {}, "html": "...", "json": {} }
html— HTML response bodyjson— JSON response bodystatus— HTTP status code (optional, default 200)headers— additional response headers (optional)
Static Files
If your project has a public/ directory, static files are served automatically when no route matches.
Session Cookie
Every request gets a _sid cookie (UUID, HttpOnly, SameSite=Lax). The session ID is included in args as sessionId. Use it as a key for per-user state.
Web Send
Tool for direct HTTP response control — set status codes, custom headers, stream body chunks. Available to any skill running inside a route handler.
web-send { status: 200, headers: { "content-type": "text/event-stream" } }
web-send { write: "data: hello\n\n" }
web-send { write: "data: world\n\n" }
web-send { end: true }
Arguments (all optional, combine as needed):
status— HTTP status code (must be set before first write)headers— response headers (must be set before first write)body— response body (sends and ends in one call)redirect— redirect URL (sends 302 with Location header)write— write a body chunk (can be called multiple times for streaming)end— end the response
If a skill uses web-send, the web handler skips its default response behavior (it checks res.writableEnded).
WebSocket
WebSocket server with socket-based skill dispatch.
Setup
Add websocket to your main skill's metadata with a routes: array:
metadata:
websocket:
routes:
- socket: chat
listen: true
ref: echo
- socket: prices
connect: "wss://api.example.com/v1"
Individual skills can also declare themselves as websocket endpoints using route: (singular) — the ref is implicit:
# In a handler skill's own metadata
metadata:
websocket:
route:
socket: chat
listen: true
Each route defines a named socket. listen: true accepts inbound WebSocket connections. connect: "url" establishes an outbound connection. ref names the skill to invoke for each message.
If web is also configured, websocket reuses the same HTTP server. Otherwise it creates its own (configure port via port key).
Each incoming message invokes the referenced skill with { message, socket } as args. The return value is sent back to the client.
Tools
websocket-send— broadcast a message to all clients on a socket:ctx.manager.invoke("websocket-send", { socket: "chat", data: "hello" })websocket-receive— await the next message on a socket:ctx.manager.invoke("websocket-receive", { socket: "prices", timeout: 5000 })
Both tools support on-demand connections via an optional url argument. If the named socket doesn't exist yet, the tool auto-connects to the URL before sending or receiving:
websocket-send { socket: "alerts", url: "wss://alerts.example.com", data: "subscribe" }
websocket-receive { socket: "alerts", timeout: 5000 }
Once a socket is open (by either declarative config or auto-connect), it stays open for the lifetime of the process. Subsequent calls reuse the existing connection — the url argument is ignored if the socket already exists.