> ## Documentation Index
> Fetch the complete documentation index at: https://docs.standardagents.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Users

> Run endpoints for one user and push live events to that user

## 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

Use the user scope for data that more than one thread reads: preferences, quotas, counters, and registries.

## User endpoints

### Quick start

Create a file in `agents/users/` and export a `defineUserEndpoint` call as the default export:

```typescript theme={null}
// agents/users/preferences.ts
import { defineUserEndpoint } from '@standardagents/spec';

export default defineUserEndpoint(async (_req, user) => {
  const theme = (await user.getValue<string>('preferences.theme')) ?? 'light';
  return Response.json({ userId: user.userId, theme });
});
```

The endpoint is available at `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](https://docs.standardagents.ai/core-concepts/api.md#file-based-routing). The path sets the route, a suffix sets the method, and `[name]` and `[*]` capture path segments.

| File | HTTP method | Route |
| - | - | - |
| `preferences.ts` | GET | `/api/users/me/preferences` |
| `preferences.put.ts` | PUT | `/api/users/me/preferences` |
| `quota/spend.post.ts` | POST | `/api/users/me/quota/spend` |
| `items/[id].ts` | GET | `/api/users/me/items/:id` |

`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

```typescript theme={null}
declare function defineUserEndpoint(
  handler: (
    req: Request,
    user: UserState,
    params: Record<string, string>
  ) => Response | Promise<Response>
): MarkedUserEndpoint;
```

* `req`: the Web API `Request`
* `user`: the `UserState` of the addressed user
* `params`: the values captured from `[name]` and `[*]` path segments

A user endpoint receives no `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:

```typescript theme={null}
// agents/users/quota/spend.post.ts
import { defineUserEndpoint } from '@standardagents/spec';
import { z } from 'zod';

const SpendInput = z.object({
  amount: z.number().int().positive(),
});

const DAILY_LIMIT = 100;

export default defineUserEndpoint(async (req, user) => {
  const input = SpendInput.safeParse(await req.json().catch(() => null));
  if (!input.success) {
    return Response.json({ error: 'Send a positive integer "amount".' }, { status: 400 });
  }

  const key = `quota.${new Date().toISOString().slice(0, 10)}`;
  const used = (await user.getValue<number>(key)) ?? 0;
  const next = used + input.data.amount;
  if (next > DAILY_LIMIT) {
    return Response.json({ ok: false, used, limit: DAILY_LIMIT }, { status: 429 });
  }

  await user.setValue(key, next);
  user.emit('quota_changed', { used: next, limit: DAILY_LIMIT });
  return Response.json({ ok: true, used: next, limit: DAILY_LIMIT });
});
```

This guarantee covers user endpoints and `state.user().call()`. It does not cover direct value writes from thread code, such as `(await state.user())?.setValue(...)`.

### UserState

| Member | Description |
| - | - |
| `userId` | The user's ID |
| `identity()` | The user's profile: `id`, `username`, `email`, `display_name`, `avatar_url`, `role`, `roles`, and `permissions` |
| `getValue(key)` | Reads one durable JSON value. Returns `null` when the key has no value |
| `setValue(key, value)` | Writes one value. A `null` value deletes the key |
| `listValues({ prefix, limit, offset })` | Lists the user's values |
| `env(name)` | Reads a variable from the user-account environment and then from the instance environment. Rejects when neither defines the name |
| `instanceEnv(name)` | Reads a variable from the instance environment only. Returns `undefined` when the name is not set |
| `emit(event, data)` | Sends a live event to the user's connected clients |
| `listThreads({ agentId, limit, offset })` | Lists the user's threads |
| `thread(threadId)` | Returns the `ThreadState` of one of the user's threads. Rejects for a thread that another user owns |
| `fetch(grant, input, init)` | Sends a request with the credential of a [credential grant](https://docs.standardagents.ai/core-concepts/credential-grants.md) |
| `clients()` | Lists the clients connected to the [user room](#user-rooms) |
| `send(clientId, frame)` | Sends one frame to one client of the user room. Returns `false` when that client is not connected |

User values are plain JSON. Do not store credentials in them. Store a credential for an external API as a [credential grant](https://docs.standardagents.ai/core-concepts/credential-grants.md).

### Calling a user endpoint from a thread

Tools, hooks, handlers, and thread endpoints reach the thread's user through `state.user()`. `call()` runs a user endpoint by its path under `agents/users/`, with the same one-at-a-time guarantee.

```typescript theme={null}
// agents/tools/spend_quota.ts
import { defineTool } from '@standardagents/spec';
import { z } from 'zod';

export default defineTool({
  description: 'Spend units of the daily quota before metered work.',
  args: z.object({
    amount: z.number().int().positive().describe('Units to spend'),
  }),
  execute: async (state, args) => {
    const user = await state.user();
    if (!user) {
      return { status: 'error', error: 'This thread has no user.' };
    }

    const response = await user.call('quota/spend', { amount: args.amount });
    if (!response.ok) {
      return { status: 'error', error: 'The daily quota is used up.' };
    }
    return { status: 'success', result: await response.text() };
  },
});
```

* `call()` sends a `POST` request with the payload as a JSON body. It reaches endpoints that handle `POST`.
* `call()` rejects a path that contains a `.` or `..` segment, in plain, percent-encoded, or backslash form.
* `state.user()` returns `null` for a thread without a user.
* A `ThreadState` that came from `user.thread()` cannot use `call()`. The call throws, because the user endpoint that raised the thread is still running.

The value returned by `state.user()` also has `getValue()`, `setValue()`, `emit()`, and `fetch()` for the same user.

### Caller channel

Standard Agents Builder sets the `X-Standard-Agents-Caller` request header on every user endpoint request. The public routes overwrite any value that the client sent.

| Value | Caller |
| - | - |
| `user` | The user's own HTTP request to `/api/users/me/...` |
| `admin` | An administrator's request to `/api/users/:id/...` |
| `thread` | `state.user().call()` from thread code |

Check for `thread` in an endpoint that only your agents may change:

```typescript theme={null}
// agents/users/credits/grant.post.ts
import { defineUserEndpoint } from '@standardagents/spec';

export default defineUserEndpoint(async (req, user) => {
  if (req.headers.get('X-Standard-Agents-Caller') !== 'thread') {
    return Response.json({ error: 'Forbidden' }, { status: 403 });
  }
  const credits = (await user.getValue<number>('credits')) ?? 0;
  await user.setValue('credits', credits + 1);
  return Response.json({ credits: credits + 1 });
});
```

### 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](https://docs.standardagents.ai/core-concepts/api.md#guarding-user-endpoints-with-application-roles).

## User event stream

The user event stream delivers every `user.emit()` and `state.user().emit()` event to the user's connected clients.

### Connecting

```
GET /api/users/me/stream     (WebSocket upgrade)
GET /api/users/:id/stream    (WebSocket upgrade, administrators)
```

A browser cannot set an `Authorization` header on a WebSocket. Request a single-use ticket for the exact path and pass it as `?ticket=`, as the [thread stream](https://docs.standardagents.ai/api-reference/websocket/stream.md#connecting) does:

```typescript theme={null}
async function connectUserStream(origin: string, token: string): Promise<WebSocket> {
  const pathname = '/api/users/me/stream';
  const response = await fetch(`${origin}/api/auth/websocket-ticket`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ pathname }),
  });
  const { ticket } = (await response.json()) as { ticket: string };

  const ws = new WebSocket(`${origin.replace(/^http/, 'ws')}${pathname}?ticket=${ticket}`);
  ws.onmessage = (message) => {
    if (message.data === 'stream_pong') return;
    const frame = JSON.parse(String(message.data)) as { event: string; data?: unknown };
    console.log(frame.event, frame.data);
  };
  return ws;
}
```

A request without a WebSocket upgrade returns HTTP 426. A user can hold several connections at the same time, one for each device or tab.

### Frames

Each frame is a JSON object:

```typescript theme={null}
interface UserStreamFrame {
  event: string;
  data?: unknown;
}
```

```json theme={null}
{ "event": "quota_changed", "data": { "used": 12, "limit": 100 } }
```

* 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.
* `data` is 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 string `stream_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

Room frames and presence callbacks run one at a time for each user, in the same queue as [user endpoints](#one-run-at-a-time-for-each-user).

### 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.

The user event stream is enough when your code only pushes events to all of the user's clients and the clients send their changes through user endpoints.

### Defining a room

Create `agents/users/<package>/room.ts` and export a `defineUserRoom` call as the default export:

```typescript theme={null}
// agents/users/my_package/room.ts
import { defineUserRoom } from '@standardagents/spec';

export default defineUserRoom({
  async onFrame(user, connection, frame) {
    if (frame.op === 'ping') {
      await user.send(connection.clientId, { type: 'room', id: frame.id, ok: true });
    }
  },
  async onPresence(user, event) {
    user.emit('my_package.presence', {
      client: event.connection.clientId,
      online: event.kind === 'attached',
    });
  },
});
```

* `onFrame` runs for each room frame that a client sends.
* `onPresence` is 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

```
GET /api/users/me/hub?client_id=...     (WebSocket upgrade)
GET /api/users/:id/hub?client_id=...    (WebSocket upgrade, administrators)
```

Authenticate with a single-use ticket for the exact path, as the [user event stream](#connecting) does. A request without a WebSocket upgrade returns HTTP 426.

* Standard Agents Builder sends `{ "type": "hub_ready" }` as the first frame.
* `client_id` identifies the client. A connection without `client_id` is not a room connection. The optional `client_kind` labels the client. Each value is trimmed and cut to 256 characters.
* A room frame is a JSON object with `"type": "room"` and a string `id` of 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 with `user.send()` and repeat the `id`.
* 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_id` replaces 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 is `automatic`: 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 a `UserState` in place of a `ThreadState`.

| Hook | Arguments | Runs when |
| - | - | - |
| `user_client_attached` | `(user, client)` | A client connects to the user event stream |
| `user_client_detached` | `(user, client)` | A client disconnects from the user event stream |
| `user_value_changed` | `(user, key)` | A user value changes |

`client` is `{ id: string; kind?: string }`.

```typescript theme={null}
// agents/hooks/announce_client.ts
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'user_client_attached',
  id: 'announce_client',
  execute: async (user, client) => {
    user.emit('client_attached', { id: client.id, kind: client.kind ?? null });
  },
});
```

A user hook needs no reference from a prompt or an agent. It runs for every user of the project while its file exists. A user hook observes the operation and cannot block it. Standard Agents Builder discards an error that a user hook throws.

## Next steps

> [card] [API](https://docs.standardagents.ai/core-concepts/api.md)
>
> Create endpoints that operate on one thread

> [card] [Credential grants](https://docs.standardagents.ai/core-concepts/credential-grants.md)
>
> Call an external API with a user credential that your code cannot read

> [card] [Thread](https://docs.standardagents.ai/core-concepts/thread.md)
>
> Read the thread owner's identity with `state.user()`

> [card] [WebSocket stream](https://docs.standardagents.ai/api-reference/websocket/stream.md)
>
> Receive live frames for one thread

> [card] [Hooks](https://docs.standardagents.ai/core-concepts/hooks.md)
>
> Run code at thread lifecycle points


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.