What is the user scope?
A thread belongs to one conversation. The user scope belongs to one user and spans every thread that user owns. It has five parts:- User endpoints: HTTP routes that run for one user
- User values: durable JSON values for one user
- User event stream: a WebSocket that delivers live events to the user’s connected clients
- User room: an optional WebSocket hub that receives frames from the user’s clients
- Credential grants: credentials that the runtime adds to requests for the user
User endpoints
Quick start
Create a file inagents/users/ and export a defineUserEndpoint call as the default export:
GET /api/users/me/preferences. Standard Agents Builder discovers it from the agents/users/ directory.
File-based routing
Routing follows the rules of thread endpoints. The path sets the route, a suffix sets the method, and[name] and [*] capture path segments.
me is the authenticated caller. Every route under /api/users/me/ requires authentication.
A super admin session has no user account, so a me route returns HTTP 400 for it.
The paths env, kv, hub, and stream directly under /api/users/me/ belong to built-in routes. Do not name a user endpoint file after one of them.
A request that matches no user endpoint returns HTTP 404.
Handler function
req: the Web APIRequestuser: theUserStateof the addressed userparams: the values captured from[name]and[*]path segments
ThreadState. An exception that the handler does not catch returns HTTP 500.
One run at a time for each user
For one user, Standard Agents Builder runs one user endpoint at a time. Each run sees every write that earlier runs completed. A user endpoint can therefore read a value, check a limit, and write the new value without a race:state.user().call(). It does not cover direct value writes from thread code, such as (await state.user())?.setValue(...).
UserState
User values are plain JSON. Do not store credentials in them. Store a credential for an external API as a credential grant.
Calling a user endpoint from a thread
Tools, hooks, handlers, and thread endpoints reach the thread’s user throughstate.user(). call() runs a user endpoint by its path under agents/users/, with the same one-at-a-time guarantee.
call()sends aPOSTrequest with the payload as a JSON body. It reaches endpoints that handlePOST.call()rejects a path that contains a.or..segment, in plain, percent-encoded, or backslash form.state.user()returnsnullfor a thread without a user.- A
ThreadStatethat came fromuser.thread()cannot usecall(). The call throws, because the user endpoint that raised the thread is still running.
state.user() also has getValue(), setValue(), emit(), and fetch() for the same user.
Caller channel
Standard Agents Builder sets theX-Standard-Agents-Caller request header on every user endpoint request. The public routes overwrite any value that the client sent.
Check for
thread in an endpoint that only your agents may change:
Administrative access
An administrator can call the same endpoints for another user at/api/users/:id/.... The endpoint runs in the addressed user’s scope. A caller that is not an administrator receives HTTP 403.
To guard an endpoint with application roles, see application role checks.
User event stream
The user event stream delivers everyuser.emit() and state.user().emit() event to the user’s connected clients.
Connecting
Authorization header on a WebSocket. Request a single-use ticket for the exact path and pass it as ?ticket=, as the thread stream does:
Frames
Each frame is a JSON object:- Standard Agents Builder sends
{ "event": "stream_ready", "data": { "client_id": "..." } }as the first frame of each connection. Pass?client_id=to set that ID, and?client_kind=to label the client. datais absent when the event was emitted without data.- Frames arrive in the order the user’s code emitted them.
- Choose event names that include your package or feature name, such as
quota_changed.
Delivery
The stream is live only. It stores no history and has no acknowledgements. A client that was not connected when an event was emitted does not receive that event. After each connect and reconnect, read the current state from your user endpoints. Use the stream for changes that occur after that read.Keepalive
Send the stringstream_ping at intervals. The runtime answers stream_pong.
User rooms
What is a room?
A user room is a WebSocket hub for one user’s connected clients. A project has at most one room. The room adds three things to the user event stream:- Client frames: a client sends a frame on the socket, and your code handles it in the user scope
- Targeted delivery: your code sends a frame to one client by its ID
- Presence callbacks: your code runs when a client attaches or detaches
When a project needs a room
Define a room when one of these is true:- Clients must send messages to your user-scope code over an open socket.
- Your code must address one device or tab of the user.
- Your code must know which clients are connected.
- Your code must verify a client before that client receives frames.
Defining a room
Createagents/users/<package>/room.ts and export a defineUserRoom call as the default export:
onFrameruns for each room frame that a client sends.onPresenceis optional. It runs when a client attaches and when a client detaches. In the default admission mode, a client that reconnects within 30 seconds causes no presence change.- A room file is not an HTTP route. A request to its path returns HTTP 404.
- When two files define a room, Standard Agents Builder refuses room connections with HTTP 503. User endpoints and the user event stream continue to work.
Hub route
- Standard Agents Builder sends
{ "type": "hub_ready" }as the first frame. client_ididentifies the client. A connection withoutclient_idis not a room connection. The optionalclient_kindlabels the client. Each value is trimmed and cut to 256 characters.- A room frame is a JSON object with
"type": "room"and a stringidof 1 to 256 characters, such as{ "type": "room", "id": "1", "op": "ping" }. The other fields belong to your protocol. The runtime sends no acknowledgement, so reply withuser.send()and repeat theid. - A room frame in a project without a room receives
{ "type": "room", "id": "...", "error": "no_room" }. user.emit()events also reach room connections, as{ "event": "...", "data": ... }frames.- In the default admission mode, a new connection with the same
client_idreplaces the earlier one. The earlier socket receives{ "type": "superseded" }and closes with code 4001. A client that receives this frame stops its reconnect attempts.
Admission rules
A room has its own admission mode. The default isautomatic: each connection with a client_id joins the room at once.
With admission: 'explicit', a new connection starts as pending. A pending connection receives no user.send() frames and no user.emit() events, and user.clients() does not list it. onFrame still receives its frames, so your code can verify a proof from the client. Reply to a pending connection with user.roomAttachments.send(), admit it with user.roomAttachments.admit(), or reject it with user.roomAttachments.close(). Each call takes connection.attachmentId. An admitted connection replaces the earlier admitted connection with the same client_id. In this mode a connection without client_id returns HTTP 400.
Room connections do not run the user_client_attached and user_client_detached hooks. Those hooks belong to the user event stream.
User lifecycle hooks
Three hook types run in the user scope. Each receives aUserState in place of a ThreadState.
client is { id: string; kind?: string }.
Next steps
API
Create endpoints that operate on one thread
Credential grants
Call an external API with a user credential that your code cannot read
Thread
Read the thread owner’s identity with
state.user()WebSocket stream
Receive live frames for one thread
Hooks
Run code at thread lifecycle points