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 parameters
  • query — URL query string parameters
  • body — parsed request body (JSON or form-encoded)
  • sessionId — auto-generated UUID cookie, persisted across requests

Response Handling

Code skills return values directly:

  • String → text/html with status 200
  • Object → application/json with status 200

Markdown skills (agent-driven) get a returns schema injected automatically with this shape:

{ "status": 200, "headers": {}, "html": "...", "json": {} }
  • html — HTML response body
  • json — JSON response body
  • status — 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.

Ask AI