Skip to navigation

Frontend SDK Reference

@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

NeedSupported boundary
Read projections or execute commands in a Custom PageuseProjection, useDocument, and useCommand
Sign out from an authenticated Operate or standalone pageuseSession
List app agents and build a page-owned assistant experienceuseAgents followed by useAgentChat
Chat in an existing channel or entity threaduseChannelChat
Upload or resolve a durable modeled fileuseUploadFile and useFileUrl
Build an external frontend or partner BFFThe 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:

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:

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.

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:

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

ExportSignature or purpose
useProjectionuseProjection<T>(name, options?) reads a projection list or document through SWR and returns an explicit no-argument mutate() revalidation path.
useDocumentuseDocument<T>() reads the server-prefetched document for a bound page and returns a ready or error state.
useCommanduseCommand<T>(name) returns execute, isLoading, and an operator-safe typed error; execute resolves after related page caches revalidate.
useAdapterOperationuseAdapterOperation<T>(adapterName, operation) calls a configured adapter through the host boundary.
useAgentsuseAgents() loads the safe, source-aware app agent catalog.
useAuthuseAuth() returns the current CesUser or null.
useSessionReturns the host-owned sign-out capability and explicit action state for the current Page surface.
useParamsuseParams() returns merged path and query parameters.
useCesNavigationReturns the app id plus currentSlug, navigateTo, and goBack; Agent Views use a host-owned, same-app page-session continuation across navigation and browser history.
useAssetUrlReturns a resolver for page deployment assets.
useFileUrlReturns a resolver for authenticated Platform file URLs.
useUploadFileReturns the authenticated Platform file upload function.
useBeforeRuntimeSourceSwitchRegisters a guard for dirty or in-flight page state before a source switch.
useAgentChatCreates 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.
useChannelChatBinds to an existing channel or entity channel and exposes messages, binding/send state, attachments, and streaming state.
useCesContextReads the host context. Prefer the higher-level hooks for platform operations.
CesPageProviderHost 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:

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:

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:

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:

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 for the complete OAuth flow and Runtime API for the generated HTTP contract.