> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.cessy.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.cessy.ai/_mcp/server.

# Frontend SDK Reference

> Use the host-provided @ces/app-sdk contract for authenticated, runtime-source-aware Cessy Custom Pages.

`@ces/app-sdk` is the supported frontend boundary for Cessy Custom Pages. The
host provides it through the page import map, so page bundles import it directly
and do not install or bundle an npm package.

For machine-readable discovery in Design MCP v1, call `get_frontend_sdk` or
read `ces://design/capabilities/frontend-sdk`. The tool response includes an
`authoringCatalog` with every supported SDK runtime and type export plus every
default `@app/ui/*` component module and its exported names. Inspect the Page
Design System when app- or Workspace-owned aliases can override or extend that
default UI catalog. In Design MCP vNext, read the exact `page-source`
Artifact-kind Resource and follow its app-scoped link to
`ces://design/vnext/apps/{appId}/capabilities/page-runtime`. Both SDK surfaces
serve the same platform-owned export contract summarized here; vNext does not
require another SDK tool or a second schema.

The vNext `page-source` contract also includes compact examples for a modeled
Projection list, `useCesNavigation()` into a bound detail Page,
`useParams()`/`useDocument()`, and an awaited `useCommand()` action. Interactive
Evaluation currently requires a static route root: use
`returns/[returnId]`, not `[returnId]`.

## Choose the correct boundary

| Need                                                        | Supported boundary                                                          |
| ----------------------------------------------------------- | --------------------------------------------------------------------------- |
| Read projections or execute commands in a Custom Page       | `useProjection`, `useDocument`, and `useCommand`                            |
| Sign out from an authenticated Operate or standalone page   | `useSession`                                                                |
| List app agents and build a page-owned assistant experience | `useAgents` followed by `useAgentChat`                                      |
| Chat in an existing channel or entity thread                | `useChannelChat`                                                            |
| Upload or resolve a durable modeled file                    | `useUploadFile` and `useFileUrl`                                            |
| Build an external frontend or partner BFF                   | The app-specific Runtime REST OpenAPI, `GET /agents`, and channel endpoints |

Do not hand-wire `/api/apps/...`, page-chat, attachment, workspace, or other
platform-internal routes. They are host implementation details, not public
integration contracts. Custom Pages use the SDK; external clients fetch the
app-specific OpenAPI document and use Runtime REST.

## Host context, authentication, and runtime source

Cessy mounts each hosted page inside `CesPageProvider`. The host supplies the
tenant and app identity, current user, page parameters and binding, navigation,
an authenticated fetch function, the selected runtime source, and a typed
session capability.

The host renders the default export as `<PageComponent />` and does not pass
Next.js `params`, `searchParams`, a bound document, or other component props.
The default component must therefore be callable without required props. Read
route and query values with `useParams()`, bound records with `useDocument()`,
and the remaining hosted context through the SDK hooks below.

Page code never receives or stores a bearer token. Data, command, file,
`useAgents`, and channel-chat helpers preserve the provider's Production or
Workspace source. The shell selects that source; page code should not add
`workspaceId` itself.

`useAgentChat` currently resolves its chat target from the published app
configuration. `useAgents` can show a Workspace-only agent while previewing,
but that agent becomes a valid `useAgentChat` target only after promotion. This
limitation is also declared on the machine-readable SDK contract. Do not bypass
it by calling an internal page-chat route.

`useCesContext().fetch` is an authenticated low-level fetch function, not a
general public API client and not an automatic runtime-source URL rewriter.
Prefer the typed SDK hooks. If a supported public HTTP operation is needed from
an external client, use the generated Runtime REST contract instead.

User-delegated chat requires an authenticated operator. Do not assume that
`useAgentChat` or `useChannelChat` is available to an anonymous public page
share merely because projection or command access was granted to its constrained
public principal.

## Host-owned sign-out

Standalone pages have no Operate account menu, so render an accessible control
with `useSession()` when users need to end their Cessy session:

```tsx
import { useSession } from '@ces/app-sdk';
import { Button } from '@app/ui/Button';

export function SignOutButton() {
  const session = useSession();

  if (!session.supported) return null;

  return (
    <>
      <Button disabled={session.state === 'pending'} onClick={() => void session.signOut()}>
        {session.state === 'pending' ? 'Signing out…' : 'Sign out'}
      </Button>
      {session.error ? <p role="alert">{session.error}</p> : null}
    </>
  );
}
```

The result exposes `surface`, `supported`, `state`, `error`, and `signOut()`.
Supported physical Operate and standalone sessions transition through `idle`,
`pending`, and `signed-out` or `failed`. The host validates the app and calling
surface, clears the physical identity session together with selected-tenant,
impersonation, and stale Agent Page Session state, then redirects to the fixed
`/auth/login` path. Page code never supplies an identity, tenant, redirect, or
cookie name.

Agent Page Session, public-share, preview, Evaluation, and unauthenticated
surfaces return `supported: false`; `signOut()` returns a typed `unsupported`
result without a network request or navigation. Do not hand-wire the private
host endpoint or treat an unsupported surface as a physical login session.

## Durable file access

`useUploadFile()` stores a browser `File` or `Blob` and returns a `CesFileRef`.
When `access` is omitted, the host binds the file to the authenticated
principal; page code cannot name a different principal. Choose the broader
`access: 'app'` compatibility scope only when every authenticated operator with
app access should be able to download the object. Use
`{ kind: 'projection-document', projection, documentId }` when the file should
reuse a modeled document's projection authorization.

Policy-side `ctx.files.put()` and `ctx.files.importUrl()` do not have an
implicit scope: they must supply `access`, or both `projection` and
`documentId`. Browser and policy metadata, roles, tags, filenames, and MIME
types never grant download authority.

`useFileUrl()` resolves the selected Production or Workspace source, but does
not weaken the stored access rule. Both browser `GET` and `HEAD` fail with the
same 404 contract for an unknown file and a known file that the current
principal cannot read. This prevents a copied file id or URL from acting as a
capability. Authorized inline-safe files can also be rendered from the returned
URL in a same-origin iframe, including browser PDF previews. The response blocks
cross-origin framing.

## Agent selection and page chat

`useAgents` returns only the safe app agent catalog. Each item has this shape:

```ts
interface PublicAgentSummary {
  id: string;
  name: string;
  label: string;
  kind: 'standard';
}

interface UseAgentsResult {
  agents: PublicAgentSummary[];
  isLoading: boolean;
  error: Error | null;
  refresh: () => Promise<PublicAgentSummary[]>;
}
```

The catalog does not expose prompts, models, MCP configuration, credentials,
container settings, provider secrets, scope policy, or other private runtime
configuration. In Production, or for an agent already present in the published
configuration, a selected `id` can be passed directly to `useAgentChat`.

```tsx
import { useState } from 'react';
import { useAgents, useAgentChat } from '@ces/app-sdk';
import { Button } from '@app/ui/Button';
import { Card } from '@app/ui/Card';
import { Stack } from '@app/ui/Stack';

function AgentConversation({ agentId }: { agentId: string }) {
  const chat = useAgentChat({ agentId });

  return (
    <Card>
      <Stack gap={8}>
        {chat.messages.map((message) => (
          <div key={message.id} data-role={message.role}>
            {message.parts.map((part, index) =>
              part.type === 'text' ? <p key={index}>{part.text}</p> : null
            )}
          </div>
        ))}
        <Button onClick={() => void chat.sendMessage('How can you help here?')}>Ask</Button>
      </Stack>
    </Card>
  );
}

export default function AgentPicker() {
  const { agents, isLoading, error, refresh } = useAgents();
  const [agentId, setAgentId] = useState<string | null>(null);

  if (isLoading) return <p>Loading agents…</p>;
  if (error) {
    return <Button onClick={() => void refresh()}>Retry agent catalog</Button>;
  }

  return (
    <Stack gap={8}>
      {agents.map((agent) => (
        <Button key={agent.id} onClick={() => setAgentId(agent.id)}>
          {agent.label}
        </Button>
      ))}
      {agentId ? <AgentConversation agentId={agentId} /> : null}
    </Stack>
  );
}
```

When `useAgentChat` has no active conversation, the first send creates a fresh
channel-backed DM conversation. Selecting a conversation from its history
continues that selected conversation. `startNewConversation()` clears the
active selection so the next send creates another fresh conversation. The chat
target remains the published agent configuration even while the surrounding
page previews Workspace data.

Use `useChannelChat` instead when the channel itself is the domain conversation.
It can bind by `channelId` or by an entity reference and will not create a
parallel page-agent DM.

### Chat attachments and agent access

Both chat hooks upload through the same private Platform attachment boundary.
Upload first, then pass the returned attachment metadata with the message:

```tsx
const attachments = await chat.uploadAttachments([file]);
await chat.sendMessage({ text: 'Please review this', attachments });
```

A message accepts at most four attachments and each upload is capped at 8 MiB.
The host preserves the sanitized filename, MIME type, size, attachment id, and
authenticated UI URL in channel history. The selected runtime agent receives
only a compact `channelId`/`attachmentId` pointer in its current turn and later
session history. When it needs the contents, Runtime MCP authorizes and serves
them with `runtime_channel_attachment_read`; page code does not need to copy
bytes into text, page context, or `agentTurnContext`.

The read capability is channel-scoped. The agent must be a participant, and the
attachment must have been committed by its uploader in that exact channel.
Knowing an id or URL does not grant cross-channel or cross-tenant access.
`deleteAttachment(id)` removes the underlying upload and future agent reads
then fail closed, although the historical message can retain its display
metadata.

Browser downloads follow the same boundary. The uploader can read a pending or
standalone attachment. Another human can read it only after the uploader's
exact metadata is durably committed in a message in a channel where that human
is a current participant. A known unauthorized attachment and an unknown
attachment produce the same `GET`/`HEAD` 404 behavior.

## Value exports

| Export                         | Signature or purpose                                                                                                                                                                     |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useProjection`                | `useProjection<T>(name, options?)` reads a projection list or document through SWR and returns an explicit no-argument `mutate()` revalidation path.                                     |
| `useDocument`                  | `useDocument<T>()` reads the server-prefetched document for a bound page and returns a `ready` or `error` state.                                                                         |
| `useCommand`                   | `useCommand<T>(name)` returns `execute`, `isLoading`, and an operator-safe typed `error`; `execute` resolves after related page caches revalidate.                                       |
| `useAdapterOperation`          | `useAdapterOperation<T>(adapterName, operation)` calls a configured adapter through the host boundary.                                                                                   |
| `useAgents`                    | `useAgents()` loads the safe, source-aware app agent catalog.                                                                                                                            |
| `useAuth`                      | `useAuth()` returns the current `CesUser` or `null`.                                                                                                                                     |
| `useSession`                   | Returns the host-owned sign-out capability and explicit action state for the current Page surface.                                                                                       |
| `useParams`                    | `useParams()` returns merged path and query parameters.                                                                                                                                  |
| `useCesNavigation`             | Returns the app id plus `currentSlug`, `navigateTo`, and `goBack`; Agent Views use a host-owned, same-app page-session continuation across navigation and browser history.               |
| `useAssetUrl`                  | Returns a resolver for page deployment assets.                                                                                                                                           |
| `useFileUrl`                   | Returns a resolver for authenticated Platform file URLs.                                                                                                                                 |
| `useUploadFile`                | Returns the authenticated Platform file upload function.                                                                                                                                 |
| `useBeforeRuntimeSourceSwitch` | Registers a guard for dirty or in-flight page state before a source switch.                                                                                                              |
| `useAgentChat`                 | Creates or selects page-agent DM conversations against the published agent target and exposes history, AI SDK-compatible messages, streaming state, attachments, and page refresh state. |
| `useChannelChat`               | Binds to an existing channel or entity channel and exposes messages, binding/send state, attachments, and streaming state.                                                               |
| `useCesContext`                | Reads the host context. Prefer the higher-level hooks for platform operations.                                                                                                           |
| `CesPageProvider`              | Host integration component. Normal Custom Page code consumes it indirectly and should not mount a second provider.                                                                       |

`ProjectionOptions` supports `id`, `filter`, `limit`, `offset`, and
`fallbackData`. `useChannelChat` accepts either `channelId` or `entityRef`; set
`createIfMissing` only when explicitly creating an entity channel is intended.
When a Custom Page runs against a Workspace source, `useProjection` list and
document requests evaluate that exact Workspace candidate's projection
authorization rules. Published projection rules are not used as a fallback.
The first read lazily activates the exact Workspace runtime scope before either
an inherited or newly added projection collection is opened. If activation is
unavailable, the HTTP error includes structured `reason`, `workspaceId`, and
`recoveryAction` details instead of an opaque internal-server response; follow
the returned Workspace recovery action and retry.
Pass `agentId` to route each user send to that existing app-agent participant
without changing the visible message. Set `mentionAgent: true` only when the
technical agent mention should also appear in the channel message.

## Projection freshness and explicit retry

`useCommand.execute()` does not resolve immediately after the command response.
It first awaits every mounted projection query for the selected runtime source,
including filtered lists that did not contain a document before the command
created it. Keep the action disabled from `isLoading` while awaiting `execute()`;
when the promise resolves, the new projection state is ready for the same render
cycle.

Build derived values from the documented response fields rather than the hook
response wrapper. For example, use `projection.data` as the dependency of a
memoized list:

```tsx
const workDays = useProjection<WorkDay[]>('WorkDayView', {
  filter: { employeeId },
  limit: 120,
});
const days = useMemo(() => normalizeDays(workDays.data ?? []), [workDays.data]);

await clockIn.execute({ employeeId, workDate });
```

To force or retry the current query explicitly, call the bound no-argument SWR
mutator and await it:

```ts
await workDays.mutate();
```

This is the supported explicit revalidation path. Do not add a `refreshKey` or
other ad hoc option to `useProjection`; Workspace page validation diagnoses
unknown object-literal options instead of silently ignoring them. Validation
also checks named imports and re-exports against the actual SDK inventory, so an
invented `useNavigation` import fails immediately; the supported hook is
`useCesNavigation`. During ChangeSet validation and publication, these SDK
diagnostics block changed or page-resource-affected sources. Existing debt on an
unchanged, unaffected page is reported as a warning and does not block an
unrelated ChangeSet; syntax, imports, resources, and Design System component
contracts remain globally enforced.

## Command errors

`UseCommandResult.error` is a `CesCommandError`. Its ordinary `message` is
bounded and safe to render in an operator-facing banner. Guard expressions,
command payloads, state snapshots, modeled values, and internal troubleshooting
text stay in protected execution traces and are not attached to the page error.

Both `useCommand.execute()` and Operate Extension
`ctx.executeCommand(name, payload, options?)` validate the server confirmation
before resolving: it must confirm success for the requested command and intent,
with consistent event evidence. A valid command may emit zero events. A `2xx`
status alone is not a command confirmation. Missing, malformed, mismatched or
contradictory confirmations reject with `COMMAND_CONFIRMATION_INVALID` and
`effectState: outcome_unknown`; no success continuation or cache refresh runs.
Keep the form input and show the error until the action is confirmed.

Every command call sends a client-owned UUID as `Idempotency-Key`. If a
connection fails or an unclassified `5xx` leaves the outcome unknown, repeating
the same payload through the mounted hook or extension reuses that intent. The runtime then
returns the original canonical receipt if the command committed, without a
second handler dispatch or event append. To continue the same intent after the
component or extension remounts, pass the error's intent explicitly:

```tsx
try {
  await clockIn.execute(payload);
} catch (error) {
  if (error instanceof Error && error.name === 'CesCommandError') {
    const commandError = error as CesCommandError;
    if (commandError.effectState === 'outcome_unknown') {
      await clockIn.execute(payload, { intentId: commandError.intentId });
    }
  }
}
```

Do not change the payload while reusing an intent. The runtime rejects that as
`COMMAND_IDEMPOTENCY_CONFLICT` instead of risking a different effect under the
same identity.

The error exposes a stable `code`, an `intentId`, and an `effectState`:

* `rejected` — validation, authorization, or a domain rule rejected the command;
  no command effect was appended.
* `rolled_back` — the request boundary closed before append and the runtime
  authoritatively confirmed no command effect. A later operator retry is a new
  intent.
* `committed` — the command committed, but a later client step such as
  projection revalidation failed. Do not resend the command.
* `outcome_unknown` — the response did not prove whether the command committed.
  Retry the identical payload with the same `intentId`.

When execution started, `executionId` and `correlationId` are also available
for support lookup. Those identifiers are separate from `message`:

```tsx
const clockIn = useCommand('ClockIn');

if (clockIn.error) {
  const supportId = clockIn.error.correlationId ?? clockIn.error.executionId;
  return <ErrorBanner message={clockIn.error.message} supportId={supportId} />;
}
```

`COMMAND_REVALIDATION_FAILED` means the command completed but page data could
not be refreshed. Call the relevant projection's `await mutate()` retry path;
do not immediately send the command a second time.

## Type exports

The module also exports these public TypeScript contracts:

* data and actions: `ProjectionOptions`, `UseProjectionResult`,
  `UseDocumentResult`, `CommandPayload`, `CommandExecuteOptions`,
  `CesCommandError`, `CesCommandErrorCode`, `CesCommandEffectState`,
  `UseCommandResult`, `UseAdapterOperationResult`
* agent catalog: `PublicAgentSummary`, `UseAgentsResult`
* page chat: `AgentChatActiveConversation`, `AgentChatAgent`,
  `AgentChatConversation`, `AgentChatMessage`, `AgentChatMessageMetadata`,
  `AgentChatPushStatus`, `AgentChatSendMessageInput`, `AgentChatSendResult`,
  `AgentChatUploadOptions`, `UseAgentChatOptions`, `UseAgentChatResult`
* channel chat: `ChannelChatAgentOptions`, `ChannelChatBindingStatus`,
  `ChannelChatChannel`, `ChannelChatEntityRef`, `ChannelChatMessage`,
  `ChannelChatMessageMetadata`, `ChannelChatPushStatus`,
  `ChannelChatSendMessageInput`, `ChannelChatSendResult`,
  `ChannelChatSendStatus`, `ChannelChatUploadOptions`, `UseChannelChatOptions`,
  `UseChannelChatResult`
* page context and files: `CesAdapterCallResponse`,
  `CesAdapterOperationInput`, `CesFileAccess`, `CesUploadFileAccess`,
  `CesFileRef`, `CesFileRefInput`,
  `CesPageContext`, `CesPageSessionControl`, `CesPageSessionSurface`,
  `CesPageSessionUnsupportedReason`, `CesSessionActionState`,
  `CesSessionSignOutResult`, `CesUploadFileOptions`, `CesUser`,
  `ResolvedBinding`, `UseSessionResult`

The hosted module is the runtime boundary. The public docs and Design MCP
contract describe its types, but this work does not publish a standalone npm
package or promise a local SDK development server.

## External frontend and BFF equivalent

An external frontend cannot use `@ces/app-sdk`. Its server-side BFF should:

1. fetch `/api/rest/<tenant-id>/<app-id>/openapi`
2. list safe agent metadata with `GET /api/rest/<tenant-id>/<app-id>/agents`
3. obtain a delegated OAuth token for the signed-in user
4. create a fresh conversation with `POST /channels/dm`
5. continue it through the returned channel id

The agent catalog uses normal Runtime REST authentication. Channel and user
channel-view operations are user-delegated and reject API keys and agent service
tokens. See [BFF Pattern](/build-a-frontend/bff-pattern) for the complete OAuth flow and
[Runtime API](/extend-the-app) for the generated HTTP contract.