---
title: "SotaAgents — User Manual"
description: "Everything you need to chat with AI, use capabilities, build and install apps, and manage your organization."
url: "https://sotaagents.ai/manual"
generated_by: "sotaagents-ldp"
docs_index: "https://sotaagents.ai/manual/llms.txt"
locale: "en"
---

# SotaAgents — User Manual

## Get started

Understand what SotaAgents is, learn the core concepts, and sign in for the first time.

### Overview

SotaAgents is a chat-first AI workspace. Sign in, pick a workspace, and ask a question — the assistant can draw on your team's documents, generate files, search the web, and call connected tools. The home screen shows a centered input box.

![The SotaAgents home screen with a centred input box and the workspace switcher and recent conversations in the sidebar](/manual/assets/product/chat-home-20260813.webp)
_The home screen: one input box, with your workspace and recent conversations to the left._

Org owners and admins manage the platform from the **Admin Console** — a separate area for creating workspaces, installing apps, managing credits, reviewing audit logs, and more.

### Core concepts

### Organization

Your company. Holds billing, members, and credit. You belong to one or more organizations.

### Workspace

A team space inside an organization. Holds conversations, files, integrations, MCP servers, and installed apps. Switch workspaces from the selector at the top of the sidebar.

### Capability

A shortcut you add to a message — Generate Image, Create Slides, Write Doc, Search Web, and more. Type `/` in the chat input to trigger the list.

### Conversation

A chat thread. All conversations live in the sidebar, grouped by date. You can organize them into Projects.

### Project

A group of related conversations with optional shared instructions, context files, and members. Created from the Projects page in the sidebar.

### Artifact

A file the assistant produces or cites — a generated .docx, an image, a PDF source. Opens in the artifacts panel beside the chat.

### App

An installable extension (Knowledge Base, Office App, Web Search, CAD, Remagine) that adds specialized capabilities to your org. Installed by an admin from the App Store in the Admin Console.

### API Key

A credential generated by an org admin to authenticate external systems or embedded chat widgets to SotaAgents programmatically. Managed in the Admin Console under _API Keys_.

### Integration

A connection to a cloud drive (Drive, OneDrive, SharePoint) set up by an admin. Lets the assistant read your team's documents.

### MCP server

A tool plug-in set up by an admin. Once connected, the assistant can use those tools mid-conversation to act on your behalf.

### Signing in

![The SotaAgents sign-in page with email-first login and social sign-in options](/manual/assets/product/auth-login-20260813.webp)
_Start with your email; the page then presents the sign-in methods allowed for that account._

SotaAgents supports four authentication methods: **email + password**, **Google SSO**, **Microsoft SSO**, and **Apple SSO**.

1. #### Open the sign-in page

   Visit [https://app.sotaagents.ai/](https://app.sotaagents.ai/).

2. #### Choose a method

   - **Email & password** — enter your registered email and password, then click _Sign in_.
   - **Google SSO** — click _Continue with Google_ and authenticate with a Google account.
   - **Microsoft SSO** — click _Continue with Microsoft_ and authenticate with a Microsoft account.
   - **Apple SSO** — click _Continue with Apple_ and authenticate with an Apple account.

3. #### Create an account

   If you do not have an account, click _Register_, submit your name, email, and password, then verify the email address. Registration creates your user account; an organization or workspace invitation is still required to join a tenant you do not already belong to.

4. #### Forgot your password?

   Click _Forgot password?_ on the sign-in page. Enter your email address and follow the reset link. No admin action is required.

5. #### Stay signed in

   SotaAgents keeps your session alive automatically. Use _Sign out_ in the account menu at the bottom of the sidebar when you are done on a shared device.

> [!NOTE]
> Native mobile/desktop sign-in
>
> Supported native builds open OAuth in the system browser and return through an app callback using a one-time PKCE exchange. If the callback fails, expires, or is consumed, restart sign-in from the app; do not reuse the old link. Availability can differ by deployment.

> [!WARNING]
> Can't sign in?
>
> Verify your email, use the same sign-in method you registered with, and follow any organization SSO policy. If you can sign in but cannot see a tenant, ask its admin for an organization or workspace invitation.

After authentication, the workspace screen appears. Use the workspace selector at the top of the sidebar to switch between workspaces.

### Your first conversation

1. #### Choose AI model, Speed & Reasoning Effort

   Select the AI model, Speed, and Reasoning Effort appropriate for the task:

   - **Reasoning Effort** — controls the depth of reasoning before a response is generated. Higher effort produces more detailed responses but increases processing time.
   - **Speed** — changes provider routing: `Standard` (default) uses quality-first ordering; `Fast` prioritizes throughput. Fast routing has no added 1.5x surcharge.

   > [!NOTE]
   > **Recommendation**If no specific requirement exists, the default configuration is recommended.

![The model selector open in the composer, listing available Claude and Gemini models with their credit multipliers](/manual/assets/product/chat-model-settings-20260813.webp)
_Each available model shows its credit multiplier, so the cost of a choice is visible before you make it._

2. #### Enter your request

   Enter the request as a question or a structured description specifying the **goal**, **context**, and **desired output format**.

![The home screen with the request box in the centre](/manual/assets/product/chat-home-20260813.webp)
_Type the request here. The controls beneath it stay with the conversation._

3. #### Review the result & continue

   Review the response, request revisions as needed, then download the output file or continue the conversation to refine the result.

![A conversation showing the question, the formatted answer, the credits used and the reply composer](/manual/assets/product/chat-conversation-20260813.webp)
_The answer arrives with its own actions and the credits it cost. Keep replying in the same thread to refine it._

4. #### Attach files (if needed)

   To attach a file, click the **(+)** icon on the chat input bar. Chat attachments are uploaded as Core conversation files and sent with that conversation; they are not automatically imported into the Knowledge Base and are not promised an OCR/indexing pipeline.

   - **Supported formats:** PDF, DOC, DOCX, TXT, MD, XLS, XLSX, CSV, PPT, PPTX, PNG, JPG, JPEG
   - **Limits:** up to 20 files per message, 100 files per conversation, and 100 MB per file

### Available Capabilities

The platform can create images, slides, documents, spreadsheets and PDFs, draw diagrams, build websites, search the web, and call tools supplied by enabled apps. What is available depends on the apps and policies admitted for the current workspace. A slash directive applies to the message you are about to send; it is not a sticky conversation switch.

For a plain question, just type. The assistant may select an available tool for that run. Knowledge Base and web search are not guaranteed defaults: use an explicit capability or clear request when a source is required.

| Capability | What the assistant produces |
| --- | --- |
| Search Knowledge Base | Queries documents already indexed by an enabled Knowledge Base app. Results and citations depend on that app's configuration and the current run. |
| Search Web | Looks the answer up on the live web and cites what it finds. |
| Create Slides | A full presentation deck from a topic or outline. Editable in the deck editor before export. |
| Write Doc | A `.docx` document — reports, memos, briefs — with headings and formatting. |
| Create Sheet | An `.xlsx` spreadsheet with formulas, charts, and formatting. |
| Create PDF | A polished, fixed-layout PDF — contracts, datasheets, one-pagers. |
| Generate Image | An image from your text prompt — illustrations, mockups, banners. |
| Draw Diagram | A flowchart or process diagram from your description. |
| Build Site | A small web page from your brief, exported as HTML. |

### What's available

Capabilities tell the assistant what kind of output to produce. Type `/` in the chat input to open the list and pick one; the capability is added to your message before you send it.

### What each capability does

### Search Knowledge Base

Use this when the answer must come from documents that have already been imported and processed by the workspace's enabled Knowledge Base app. Search behavior, ingestion formats, OCR and citation coverage are app/configuration dependent. Selecting this capability targets the current submitted message; select it again on a later message when the source requirement must be explicit. Web search may also be selected by the assistant when an admitted tool is relevant, so state your source boundary clearly.

Internal documents

Hybrid vector + full-text search

Citations included

> [!NOTE]
> **Sample prompt**"Find information about [topic] in our internal documents. Summarize the key points from the most relevant sections and include the source document names."

### Search Web

Search publicly available, up-to-date information on the Internet for sources beyond internal data. Supports standard web search, news search, image search, and full page extraction. All answers cite source links.

Internet search

News search supported

Answers cite full source links

> [!NOTE]
> **Sample prompt**"Search for [topic and timeframe] [search goal]. Summarize [what to include] from [scope of sources] [scope]. Return a concise bullet-point summary with key figures and official source citations [output format]."

### Document Generation (DOCX · Excel · PDF · PPTX)

Create documents, spreadsheets, PDFs, and presentations from a natural-language request. Files are generated by the Office app and converted to PDF via Gotenberg where needed.

Covers the **Write Doc**, **Create Sheet**, **Create PDF**, and **Create Slides** capabilities.

DOCX · EXCEL · PDF · PPTX

Download + Editable

> [!NOTE]
> **DOCX**"Create a DOCX file [format] for [document purpose] [content]. Use professional formatting with clear Heading 1/2 hierarchy, bullet points, and a summary table [presentation]."

> [!NOTE]
> **EXCEL**"Create an Excel file [format] for [calculation/tracking purpose] [content]. Include formulas to auto-calculate [metrics], a chart to visualize [data], and conditional formatting to highlight [condition] [presentation]."

> [!NOTE]
> **PDF**"Create a PDF file [format] for [document purpose] [content]. Clean, minimal design in a [color tone] suitable for [audience] [presentation]."

> [!NOTE]
> **PPTX (Slides)**"Create a [number]-slide report on [topic] [content] for [audience]. Cover [key points], in a [formal / modern / minimal] style using [brand color]. [style]"

### Generate Image

Create illustrative images from a natural-language description.

> [!NOTE]
> **Sample prompt**"Create an image of [subject and scene] [content], in a [art / photo / 3D render] style with [color palette and mood] [style], aspect ratio [1:1 / 16:9] [ratio]."

### Draw Diagram

Create flowcharts / process diagrams from a natural-language description.

> [!NOTE]
> **Sample prompt**"Draw a process diagram for [process name] [goal]. Include the steps from [start] through [end] [content]. Use a clear, professional flowchart suitable for [where it will be used] [presentation]."

### Build Site

Create a website from a natural-language description.

Professional website

Export HTML

> [!NOTE]
> **Sample prompt**"Build a [site type, e.g. microsite/landing page] for [purpose] [goal]. Include [sections, e.g. overview, schedule, FAQ, contact] [content]. Minimal, professional, easy-to-navigate design [presentation]."

### Account & session

![The account menu opened from the profile avatar, showing usage remaining, language, theme, the manuals, settings, console and sign out](/manual/assets/product/chat-account-menu-20260813.webp)
_Everything in this section lives behind the profile avatar at the bottom-left._

### Light & dark theme

Open _Settings → General_ and set Appearance to Light, Dark, or System. System follows your operating-system preference.

### Reduced motion

If your operating system is set to reduce motion, the interface honors it automatically — animations are minimized.

### Model & session settings

Choose the model, reasoning effort, and speed from the selector inside the composer on desktop and mobile. See [Model & session settings](/manual/using-sotaagents/model-and-session-settings).

### Account & sign out

Open the account menu at the bottom of the sidebar to see your name, email, and connection status. Click _Sign out_ when you're done — every time on a shared computer.

### Settings

Open _Settings_ from the account menu to manage General, AI Agent, Memory, Security, Usage, and — when supported by the deployed gateway — Login history.

### Personal Memory

The Memory tab lets admitted members control recall and learning separately, choose which categories may be learned, and review, correct, confirm, forget, import, or export private saved items. Incognito and guest/public sessions neither use nor write personal memory.

### General and AI Agent settings

Use the _General_ tab to manage profile details, Appearance, Chat font, and Motion.

![Account Settings on the General tab with the Appearance menu open for System, Light, and Dark](/manual/assets/product/chat-settings-general-20260903.webp)
_General combines profile details with visual and motion preferences._

Use _Settings → AI Agent_ to choose the send shortcut, Queue or Steer follow-up behavior, turn-completion notifications, custom instructions, Stats for nerds, and model-context visibility.

![The AI Agent settings tab showing send shortcut, follow-up behavior, notifications, and custom instructions](/manual/assets/product/chat-settings-ai-agent-20260903.webp)
_AI Agent keeps conversation behavior and diagnostics in one tab._

Open the _Voice_ section to choose Voice, Style, and Speed for read aloud. Playback streams sentence by sentence, so it can begin before the full answer is ready.

![The AI Agent Voice settings showing voice, style, and speed controls below technical details](/manual/assets/product/chat-settings-ai-agent-voice-20260903.webp)
_Voice settings apply to the Read aloud action on assistant replies._

### Manage Personal Memory

Open _Settings → Memory_. _Personal memory_ pauses or resumes the feature without deleting saved items. _Use memory in chats_ controls recall; _Learn from chats_ controls background capture after completed chats. Category controls cover communication style, working style, tool preferences, personal details, and long-term goals. Turning a category off stops new automatic saves but does not delete existing items.

![Personal Memory settings with separate recall and learning controls, category policies, and manage, activity, import, and export actions](/manual/assets/product/chat-memory-settings-20260813.webp)
_Recall, learning, and category permissions are independent; pausing memory keeps already saved items._

Use _View and manage memory_ to inspect each item, edit incorrect wording, confirm an item that needs review, or forget it. Workspace-specific memory remains private to your account—it is relevant only in that workspace, not shared with workspace members. _Memory activity_ records recent policy and item changes; export downloads JSON, while import previews content from another AI before adding it.

![The private Personal Memory manager listing saved communication, personal-detail, and working-style items with edit and forget controls](/manual/assets/product/chat-memory-items-20260813.webp)
_Saved items can span all workspaces or stay relevant to one workspace, while remaining private to the account owner._

### Two-factor authentication and login history

In _Settings → Security_, enroll a TOTP authenticator, save the one-time recovery codes, review trusted devices, or disable 2FA after re-authentication. _Login history_ appears only when the current gateway exposes that endpoint and shows recent sign-in activity; native OAuth availability can differ by desktop/mobile deployment.

★. #### View usage quota

   Select the **profile avatar** at the **bottom-left** of the screen to view the remaining usage quota and credit balance.

↩. #### Log out

   Select the **profile avatar** menu at the bottom-left, then select **Sign out** to end the session.

## Using SotaAgents

Chat is the core — type a message and the assistant replies, searches, generates files, and calls tools as needed. You can also organise conversations into projects, share them with teammates, and tune the model per conversation.

### Starting a chat

Chat always happens inside a workspace, so the first step is to launch one. Open the **workspace selector** at the top of the sidebar, pick the workspace you want to work in, and the home screen loads with a centered input box asking _How can I help you today?_ — type your question there to start a conversation.

### The chat layout

![The chat layout: workspace switcher, New Conversation, Search and Projects above the Recents list, with the composer centred](/manual/assets/product/chat-home-20260813.webp)
_The sidebar holds the workspace, search and your conversations; everything else is the thread._

| Area | What's there |
| --- | --- |
| Sidebar (left) | Workspace selector at the topNew Conversation buttonSearch (opens the command palette to find conversations)Projects (navigate to your project list)Recent conversationsAccount menu at the bottom |
| Chat UI (center) | Conversation areaChat input (holds model selector, text box, **(+)** attachments, file chips, and send/stop button)Type `/` to open the capabilities list |
| Artifacts panel (right) | Opens when you click a generated file or a citationLets you read, navigate, and download the file |

### Sending a message

1. #### Start a new conversation (optional)

   Hit _New Conversation_ in the sidebar to start fresh.

2. #### Type and send

   `Enter` sends, `Shift`+`Enter` adds a new line.

3. #### Watch the answer stream

   The assistant's reply appears live. If it needs to search, generate a file, or call a tool, you'll see a _tool step_ appear inline before it continues writing.

![An assistant answer streaming into the thread with headings and bullet points, above the reply composer](/manual/assets/product/chat-conversation-20260813.webp)
_The answer streams in as it is written; you can keep reading while it finishes._

4. #### Stop a runaway answer

   While the assistant is replying, the send button becomes a _Stop_ button. Click it to halt generation.

![The action row under an assistant message: retry, copy, read aloud and feedback, with the credits used](/manual/assets/product/chat-message-actions-20260813.webp)
_Every answer carries its own actions and the credits it cost._

### Reply, read aloud, and branch

Select a passage inside an assistant reply, then click _Reply_. The selected excerpt is attached above the composer so the next message keeps the exact context.

![Selected text in an assistant reply with the Reply action ready to attach the excerpt to the composer](/manual/assets/product/chat-quote-reply-20260903.webp)
_Select only the passage you want to discuss, then reply to that excerpt._

Open an assistant reply's _More_ menu for _Read aloud_ or _Fork from this message_. A message fork copies the conversation through that reply into a new thread and leaves the original untouched.

![An assistant message More menu showing Read aloud and Fork from this message](/manual/assets/product/chat-message-fork-menu-20260903.webp)
_Read the answer aloud or branch at exactly this message from the same menu._

### Edit and long conversations

Editing a historical user message opens the composer under a dark overlay and locks scrolling. Press `Esc` or click outside to leave edit mode; the in-progress edit is preserved if you switch conversations and return.

![A historical message being edited in the composer while a dark overlay covers the conversation](/manual/assets/product/chat-message-edit-overlay-20260903.webp)
_Edit mode takes over the screen so the branch point is unambiguous._

When a long conversation is summarized automatically to free context space, a _Context automatically compacted_ activity appears in the chat stream.

![A chat activity row saying Context automatically compacted](/manual/assets/product/chat-context-compaction-20260903.webp)
_The timeline records each automatic compaction._

### Inactive organizations

If the organization's subscription is inactive, the notice appears on the composer. Existing content remains readable, but sending messages, uploading files, and changing apps are disabled until the plan is restored.

![The composer showing an organization subscription cancelled notice and disabled sending](/manual/assets/product/chat-org-inactive-notice-20260903.webp)
_The inactive-organization notice appears with the controls it disables._

> [!NOTE]
> If something goes wrong
>
> An error banner appears below the messages with _Retry_ and _Dismiss_ buttons.

### Model & session settings

The **model selector** sits inside the composer on desktop and mobile. It shows the current model and opens a panel where you choose the model and tune how it responds. The choice applies to the messages you send next.

1. #### Pick a model

   Click the model name to open the selector. It shows only models currently available to the organization. Locked models are hidden; an eligible Owner or Admin requests access from _Console → Organization → Chat Models_.

![The current model selector showing available Claude and Gemini models with reasoning effort and speed controls](/manual/assets/product/chat-model-settings-latest-20260903.webp)
_The chat picker contains selectable models only; access requests live in the console._

2. #### Set reasoning effort

   The _Reasoning effort_ control sets how much the model thinks before replying — _None_, _Minimal_, _Low_, _Medium_, _High_, or _X-High_. Higher effort can give better answers on hard problems but takes longer and uses more credit. Only shown for models that support it.

3. #### Choose a speed

   _Standard_ uses quality-first provider ordering; _Fast_ prioritizes the highest-throughput route. Usage is charged at the provider-reported cost with no additional 1.5x fast-routing surcharge. Only shown for models that support it.

### Turn statistics and model context

Enable _Stats for nerds_ and _Show model context_ under _Settings → AI Agent_. The line below the composer shows turns, steps, LLM time, tool-call time, and average time to first token. The context readout shows total window usage and its system-prompt, message, attachment, tool-call, and skill breakdown.

![A chat composer showing per-turn timing statistics and an open model-context usage breakdown](/manual/assets/product/chat-turn-stats-model-context-20260903.webp)
_Turn timing and context usage are optional diagnostics controlled from AI Agent settings._

### Upload Attachments

Drop a file into a conversation when you want the assistant to read it for that turn — a contract, a spreadsheet, a screenshot.

1. #### Attach

   **Drag & drop** a file onto the input area, or click the plus button (**+**) to pick files from your computer. You can also paste an image or document directly into the input.

2. #### Wait for upload

   Each file shows a chip with its upload status. Remove a chip with its _x_ button before sending. Current limits are 20 files per message, 100 per conversation, and 100 MB per file.

3. #### Send your message

   Hit send. Core keeps the files with this conversation so later turns can refer to them, subject to the conversation limit.

> [!NOTE]
> Attachment vs. Knowledge Base import
>
> A chat attachment is not automatically indexed or OCR-processed by Knowledge Base. To create workspace-wide searchable knowledge, import and process the source through the enabled [Knowledge Base app](/manual/using-sotaagents/integrations).

### Rich answers

Assistant answers are more than plain text. SotaAgents renders them with full formatting and can embed generated content right in the reply.

### Formatting

- **Code blocks** — syntax-highlighted, with the language labelled and a _copy_ button.
- **Tables** — bordered and scrollable sideways when wide.
- **Lists** — bulleted and numbered.
- **Links** — open in a new tab.

### Generated content inside the answer

- **Generated images** — appear inline; click to open larger in a lightbox.
- **Generated files** — docs, sheets, slides, and PDFs appear as a block you can open in the [artifacts panel](/manual/using-sotaagents/artifacts-panel) and download.
- **Web search results** — when the assistant searches the web, the results show inline then feed into the answer.
- **Subagent work** — delegated work appears as a named chip. Open it to inspect the subagent's durable transcript without expanding its internal messages into the main timeline.

### Artifacts panel

When the assistant produces a file — or when you open a citation — it appears in the **artifacts panel** on the right of the chat.

### What opens here

| Type | What you see |
| --- | --- |
| Generated documents | Word docs, spreadsheets, slides, and PDFs the assistant created — previewed with a download button. |
| Files & images | PDFs, images, video, audio, text, markdown, HTML, and CSV render directly in the panel. |
| Citation previews | The source document behind a citation, opened to the cited page. |
| Subagent transcripts | A named, read-only transcript of delegated work, including its task, streamed result, and final status. It remains available after history reload. |

1. #### Open an artifact

   Click a generated file, citation badge, or named subagent chip in a message. The panel opens on the right.

![A chat with the Launch Readiness Analyst subagent chip opened into its durable transcript in the artifacts panel](/manual/assets/product/chat-subagent-artifact-20260813.webp)
_Named subagent work stays compact in the main thread and opens as a durable transcript in the artifacts panel._

2. #### Read & navigate

   Scroll a document or subagent transcript, page through a PDF, zoom an image, or play media — depending on the artifact type.

### Skill and Office previews

Use the breadcrumb at the top of the skill viewer to choose a file in the skill.

![The skill viewer open with its file breadcrumb menu at the top of the panel](/manual/assets/product/chat-skill-viewer-breadcrumb-20260903.webp)
_Use the breadcrumb to move between skill files without losing preview space._

Downloaded files keep their original filename. Excel previews render embedded images, charts, and shapes. DOCX form previews preserve the form layout; filling changes the document body only, keeps headers, footers, and page geometry intact, and does not turn footnote markers into fields.

3. #### Download

   Use the download button to save the file to your computer.

### Citations & sources

When the assistant answers using documents from your workspace or the web, it cites them. Citations appear as small labeled chips inline in the answer, showing the source file name.

1. #### Spot a citation

   Look for a small chip showing the source file name inside the answer. Hover it (or tap on mobile) for a quick card showing the source title, section, and a short snippet from the cited passage.

2. #### Open the source

   Click the chip to open the source document in the artifacts panel. The document renders as a full preview — Office files open in the document viewer, letting you read the original content in context.

### History

Ordinary, successfully created chats are saved in workspace history. Incognito chats are not retained in History: Core may persist them temporarily while the session is active, then purges them on close or expiry, with a 24-hour upper bound. A first turn blocked by guardrails or one that fails before the conversation is created may also leave no history entry. The sidebar lists retained conversations under _Recents_.

1. #### Find a conversation

   Click **Search** in the sidebar (or press `⌘``K`) to open the command palette: it lists your recent chats and lets you jump straight to one. You can also scroll the sidebar under _Recents_ and click any conversation; the active one is highlighted.

![The search command palette listing recent chats and suggested actions](/manual/assets/product/chat-search-palette-20260813.webp)
_Search opens a command palette over the page: recent chats first, then actions like New Conversation and Projects._

2. #### Change grouping

   Click the sliders icon next to _Recents_ to open the _Group by_ menu. Choose _None_ (flat list), _Date_ (Today, Yesterday, This Week…), or _Project_ (grouped by project).

3. #### Start fresh

   The _New Conversation_ button at the top of the sidebar opens an empty conversation.

### Fork a conversation

Open a conversation's options menu in the sidebar or history bar and choose _Fork_ to copy the complete thread into a new conversation. Forking from an assistant message branches at that exact reply. The original stays unchanged, and the new conversation is independent without an origin/provenance chip.

### Pin, edit, retry, and steer

Use a conversation's menu to pin it (up to 100 pinned conversations), rename it, move it to a project, share it, or delete it. During a run you can queue another message, then edit or remove queued items. _Steer_ stops the current reply and sends the queued message next; message actions let you retry an assistant response or edit a historical user message to branch from that point.

### Drafts and Incognito

Unsent drafts are stored locally for the current user/workspace route and restored when you return to that route. Incognito disables draft persistence as well as history and Personal Memory.

### Projects

Projects let you group related conversations under a shared name, with optional instructions and shared context files. Click _Projects_ in the sidebar to open the projects list.

### Creating a project

1. #### Open the list

   Click _Projects_ in the sidebar. Use the tabs to switch between _Created by you_ and _Shared with you_. Sort by last modified, name, or created date.

2. #### Create

   Click _New project_, enter a name (required) and an optional description, then confirm. The project appears in the list immediately.

![The Projects screen listing two projects, with tabs for projects created by you and shared with you](/manual/assets/product/chat-projects-list-20260813.webp)
_A new project appears in the list straight away._

### Adding and moving conversations

1. #### Add to a project

   Click the **…** menu on any conversation — in the sidebar or inside a project — and choose _Add to project_. A submenu lists your projects; you can also create a new project from there.

![The three-dot menu on a sidebar conversation, with the Add to project submenu open listing the available projects](/manual/assets/product/chat-add-to-project-20260813.webp)
_Any conversation can be filed into a project from its three-dot menu._

2. #### Move or remove

   From the same **…** menu, choose _Move to project_ to send it to a different project (a confirmation is shown first), or _Remove from project_ to unlink it without deleting it.

### Inside a project

Opening a project shows a composer to start a new chat within the project, and the list of conversations it contains. The right sidebar lets you:

![A project page showing its conversations, an Instructions panel, project context files and members](/manual/assets/product/chat-project-detail-20260813.webp)
_Inside a project: its conversations, the instructions every chat inherits, shared context files and who can see it._

- **Instructions** — set a shared instruction that applies to every chat started inside the project.
- **Project context** — attach files or text snippets (up to 50 items) available in every project chat.
- **Members** — invite workspace members to the project and manage their access.

### Renaming and deleting

Use the **…** menu on a project card or on the project detail page to _Edit_ (rename or update the description) or _Delete_ the project. Deleting removes the project and its conversations immediately. This cannot be undone.

### Sharing

You can share a conversation snapshot with teammates or the public via a link. A shared link captures the conversation at the moment you create it — new messages aren't included until you update the link.

### Share settings

| Audience | Who can view |
| --- | --- |
| Workspace only | Members of your current workspace. |
| Organization | Anyone signed in to your organization. |
| Public link | Anyone with the link, no sign-in required. |

### How to share

1. #### Open the share dialog

   Click the share icon in the conversation header (or three-dot menu → _Share_). The dialog shows the current audience and link.

![The share dialog with an include-tool-activity toggle and the audience choices: workspace members, organization, or anyone with the link](/manual/assets/product/chat-share-dialog-20260813.webp)
_The dialog is where the audience is chosen and the link is created._

2. #### Choose an audience

   Pick _Workspace only_, _Organization_, or _Public link_. Public links are visible to anyone — don't share sensitive conversations publicly.

3. #### Optional: include tool activity

   Toggle _Show tool activity_ to include tool step names in the shared view (tool outputs are never shown). The dialog warns if the conversation contains attachments, artifacts, knowledge sources, or possible secrets.

4. #### Copy and share the link

   Copy the link and share it however you like. Recipients can read the snapshot at `/share/c/:token` without joining your workspace.

5. #### Update or revoke

   _Update link_ refreshes the snapshot to include new messages. _Revoke link_ immediately stops access — anyone visiting the old URL will see an expired page.

> [!NOTE]
> Create a copy
>
> Recipients of a shared link can click _Create a copy_ to fork the snapshot into their own workspace and continue the conversation from there.

### Integrations

Provider connections for documents live inside the **Knowledge Base app**, not in a core workspace _Integrations_ route. The app must be installed for the organization and enabled for the workspace. Which providers appear depends on the deployed Knowledge Base configuration; some use optional Composio-backed connections.

App-owned and configuration-dependent

Setup: workspace admin

Use: admitted workspace members

1. #### Open Knowledge Base

   Open the workspace's enabled Knowledge Base app from the app navigation. If it is absent, an org admin must install it and enable the exact environment for this workspace.

2. #### Add a supported source

   Use the provider/source controls shown by that app. Authorize only the folders or sources intended for the workspace; availability and OAuth fields vary by provider configuration.

3. #### Sync and process

   Follow the app's status until the selected documents are imported and processed. A file attached directly to chat does not enter this pipeline.

4. #### Request grounded retrieval

   Ask the assistant to use Knowledge Base, or choose its capability for the current message, and check the returned citations. Do not assume every chat automatically searches connected sources.

> [!NOTE]
> Provider inventory is live
>
> Google Drive, OneDrive, SharePoint, or other connectors may appear when enabled, but the Manual does not guarantee a fixed list or automatic retrieval behavior.

### MCP servers

MCP servers give the assistant **tools**. Where Integrations let the assistant _read_ your documents, MCP servers let it _do_ things — open a Linear issue, post in Slack, push to a Notion database, run a query in your internal API. "MCP" stands for _Model Context Protocol_: an open standard for exposing tools to AI agents.

Typical providers: Linear · Notion · Slack · GitHub · Asana

Setup: workspace admin

Use: every workspace member

### Integration vs MCP — what's the difference

| Aspect | Integration | MCP server |
| --- | --- | --- |
| What it does | Brings the system's documents into the knowledge base. | Gives the assistant tools to act in the system. |
| The assistant can… | _Read_ the content and cite it in answers. | _Do_ things — create, update, query live data. |
| Data freshness | A snapshot from the last sync; re-sync to update. | Live — the assistant calls the system in the moment. |
| Use it when | You want answers grounded in your team's documents. | You want the assistant to perform tasks, not just answer. |

### For workspace admins — register an MCP server

1. #### Open MCP servers

   From the workspace console choose _MCP Servers_. The screen lists every server connected to this workspace.

![The MCP Servers screen: a list of connected servers on the left and, on the right, the selected server with its status, transport, auth type and tool inventory](/manual/assets/product/ws-mcp-20260813.webp)
_The list shows every registered server; selecting one reveals its transport, auth and the tools it exposes._

2. #### Add a server

   Click _Add server_. Fill in a **Name**, the server's HTTPS **URL**, and an **Auth method** (OAuth for SaaS apps, API key for internal tools, or None).

![The add-MCP-server dialog with a name, server URL, transport selector and, under Advanced, the authentication method and a prompt hint](/manual/assets/product/ws-mcp-add-20260813.webp)
_Adding a server needs its URL and transport; authentication and a prompt hint sit under Advanced._

3. #### Authenticate

   For OAuth servers, click _Connect_ and approve the scopes in the browser. For API-key servers, paste the key into the form — it's encrypted and never shown again.

4. #### Test and refresh tools

   Use the _Test_ button to confirm the connection. Then click _Refresh tools_ to pull the server's tool catalog into the workspace.

5. #### Allow tools per workspace

   The tools are available to the assistant automatically once the server is connected. You can restrict which tools appear by toggling them off here. Anything toggled off is invisible to the assistant.

### Common MCP servers

| Server | What the assistant can do | Auth |
| --- | --- | --- |
| Linear | Create / read / update issues, comments, and assignees | OAuth |
| Notion | Search pages, create pages, update database rows | OAuth |
| Slack | Post messages, read channels, look up users | OAuth |
| GitHub | Read PRs, leave comments, search code, trigger workflows | OAuth or PAT |
| Asana | List tasks, create tasks, update status | OAuth |
| Internal HTTP MCP | Whatever your engineers expose | API key |

> [!WARNING]
> Be deliberate about what you allow
>
> The assistant will use any tool you give it when it thinks it's helpful. Don't expose destructive tools (delete, drop, force-push) unless the assistant's role explicitly needs them.

## Admin Console

For organization owners and admins — manage roles, workspaces, apps, members, API keys, and credits.

### Roles & permissions

Access works on two levels. Your **organization role** sets what you can do across the whole account — billing, members, and workspaces. Your **workspace role** sets what you can do inside a specific workspace. A person can hold different roles in different workspaces, and org owners and admins automatically get admin rights in every workspace under the org.

![The organization members table listing each member with their organization role and status](/manual/assets/product/console-participants-20260813.webp)
_Members lists everyone in the organization and the role they hold._

### Organization roles

| Role | What they can do |
| --- | --- |
| Owner | Manage organization membership, workspaces, API keys, apps, security and the owner/admin credit-policy actions described below. Ownership does not grant platform subscription or money-movement authority. |
| Admin | Manage organization membership, workspaces, apps and many credit-policy settings. Exact screens and mutations are still enforced by the API; “Admin” is not shorthand for unrestricted billing. |
| Member | Use the workspaces they've been added to. |

### Subscription and credit authority

| Action | Authority |
| --- | --- |
| Read organization credit overview | Organization members |
| Credit package CRUD/assignment and rolling user limit | Organization Owner or Admin |
| Read Guest Credit add-on and manage workspace guest allocations | Organization Owner or Admin |
| Upgrade, downgrade, cancel plan; top-up or refund | SotaAgents operations team only |
| Pool/seat configuration and Guest Credit add-on lifecycle | SotaAgents operations team only |

### Workspace roles

![The workspace participants tab listing members with their workspace role](/manual/assets/product/ws-participants-20260813.webp)
_Workspace roles are set separately, on the workspace itself._

| Role | What they can do |
| --- | --- |
| Workspace Admin (`WS_ADMIN`) | Manage workspace settings, members, apps, integrations, guardrails, and MCP servers. |
| Workspace Member (`WS_MEMBER`) | Use the workspace and the capabilities available there. Cannot change membership, apps, or workspace settings. Legacy Editor/Chatter values are compatibility-mapped to this role. |

> [!NOTE]
> Org owners and admins are auto-promoted
>
> If you're an org owner or admin, you have workspace-admin power in every workspace under your org — no need to invite yourself separately.

### Console overview

The Admin Console (`/console`) is where org owners and admins manage the platform. It is separate from the workspace chat UI and requires org admin or owner role to access most features.

![The Admin Console organization directory, listing organizations with their plan and your role](/manual/assets/product/console-orgs-20260813.webp)
_The console opens on your organizations; each card shows the plan and your role in it._

![The organization settings page with identity and administrative configuration](/manual/assets/product/console-settings-20260813.webp)
_Organization settings hold identity and policies available to your current role._

The console is organized around your **organization**. From the org detail screen you can navigate to:

- **Dashboard** — usage overview and recent activity
- **Workspaces** — create, manage, and configure workspaces
- **Apps** — install and manage extensions from the App Store
- **Participants** — manage org members and roles
- **API Keys** — credentials for machine-to-machine access
- **Activity** — audit log for the whole org
- **Credits** — credit usage, allocation, alerts, and top-ups
- **Chat Models** — control which models are available and set an org default
- **Security** — org-level security settings
- **Settings** — org name and general configuration

### Dashboard

The org dashboard gives you a real-time snapshot of platform activity without leaving the console.

![The organization dashboard with member, workspace and AI response tiles, a credit usage chart, credit usage by user, and user and AI trends](/manual/assets/product/console-dashboard-20260813.webp)
_The dashboard answers what the organization has been doing: members, workspaces, responses and where the credits went._

### Three KPI cards

Total Users, Workspaces, and AI Responses summarize the current organization.

### Credit usage

Time-series usage and usage-by-user views show where organization credit is being consumed.

### Model usage

A ranked model breakdown shows which admitted chat models account for usage.

### Trends

User and AI-response trend cards show changes over the selected period. The current dashboard does not promise a recent-activity feed.

### Workspaces

The Workspaces tab lists every workspace in your org. From here you can create new workspaces or drill into an existing one to configure it.

![The Workspaces tab listing the organization workspaces with their slug and creation date](/manual/assets/product/console-workspaces-20260813.webp)
_Each workspace is an isolated context with its own apps, integrations and chat history._

Use the _Grid / List_ toggle above the workspace collection to switch between visual cards and compact rows. The choice changes presentation only.

![The organization Workspaces page in List view with the Grid and List toggle visible](/manual/assets/product/console-workspaces-list-20260903.webp)
_List view fits workspace identity, slug, description, and creation date into compact rows._

### Creating a workspace

1. #### Click Create workspace

   From the Workspaces tab, click the _Create workspace_ button. Enter a name and optional description.

2. #### Configure it

   Once created, click the workspace to open its detail screen. From there you can manage Apps, MCP Servers, Participants, Guardrails, Activity, Feedback, Configure Embed, and Settings according to your role.

### Workspace-level tabs

![The workspace Apps config tab showing the organization-installed demo app and its workspace access control](/manual/assets/product/ws-apps-config-20260813.webp)
_Org-installed apps start enabled in every workspace; this tab holds explicit per-workspace disable or re-enable overrides._

| Tab | What you can do |
| --- | --- |
| MCP Servers | Connect and manage MCP tool servers for this workspace. |
| Apps | Enable or disable org-installed apps for this workspace individually. |
| Participants | Manage workspace-scoped members and assign workspace roles. |
| Guardrails | Configure and review the workspace's input and tool-output protections. |
| Activity | View the workspace activity log and export to CSV. |
| Feedback | Review user feedback submitted from conversations in this workspace. |
| Configure Embed | Configure, preview, publish, or unpublish a public guest deployment. |

![The workspace Feedback page listing ratings and comments from conversations](/manual/assets/product/ws-feedback-20260813.webp)
_Feedback centralizes conversation ratings and comments for workspace review._

### Public Deployment (Configure Embed)

Organization Owners/Admins with workspace access can open _Configure Embed_, set allowed origins, choose a chat model, customize appearance and an optional lead form, then preview the guest-facing app/tool exposure. Publishing creates a public deployment; unpublishing stops new guest access without requiring an organization API key in the browser.

- **Origin, session, and IP checks** are enforced by the public-chat boundary.
- **Guest conversations** consume the workspace allocation from the separate Guest Credit pool.
- **Preview before publish** and review the admitted apps/tools and any allocation warning.

![The Configure Embed page for a workspace public deployment](/manual/assets/product/ws-embed-20260813.webp)
_Configure, preview, publish, and later unpublish the workspace's guest deployment from one page._

> [!NOTE]
> Quick-action prompts
>
> The workspace list shows contextual quick-action prompts — e.g. _Add datasource_, _Invite member_, _Try assistant_ — so common tasks are one click away.

### Workspace settings

From the workspace detail screen, open _Settings_ (bottom of the workspace sidebar) to rename the workspace or delete it. Deletion is presented as a modal dialog with a confirmation step — once deleted, workspace data cannot be recovered.

![The workspace settings drawer with identity, slug, description, tags, the assistant system prompt and response format](/manual/assets/product/ws-settings-20260813.webp)
_Workspace settings open as a drawer, not a separate page._

### Installing apps

Apps are installable extensions that add specialized capabilities to your org. Admins manage the live inventory from the **Apps** tab in the Admin Console (_org detail → Apps_).

![The organization App Store with Installed, Catalog and Recovery tabs and a locally seeded contract-probe app](/manual/assets/product/console-apps-20260813.webp)
_The real local catalog varies by deployment; this capture uses the TestStack contract-probe app._

> [!NOTE]
> Organization install plus workspace overrides
>
> **Org level (App Store):** installing an exact app environment grants it to the organization. **Workspace level (Apps tab):** every workspace inherits enabled access by default; an admin can create an explicit disable override for one workspace and re-enable it later.

### Example apps

The catalog is deployment- and organization-dependent. Counts below are examples from one captured artifact, not a fixed platform inventory; use each current app detail page as authority.

| App | What it adds | Surfaces |
| --- | --- | --- |
| Knowledge Base | Shared document store with hybrid semantic + full-text search and source citations. Upload internal documents so the assistant can answer with evidence. Key tools: `hybridSearch`, `searchDocuments`, `structuredQuery/Sql`, file management. | 19 Tools · 1 Skill · 2 UI Slots |
| Office App | Generate and edit Word docs, Excel spreadsheets, PowerPoint/PDF presentations, and dashboards inside a sandbox. Includes a slide deck editor with automatic quality checks. | 11 Tools · 1 Skill · 1 UI Slot |
| Web Search | Real-time public web search and page content extraction. Key tools: `webSearch`, `fetchWebPage` (up to ~50 000 chars per URL). | 3 Tools · 1 Skill · 1 UI Slot |
| CAD | CAD/BIM workflows for construction — ingest PDF/DWG/DXF/IFC files, layout-aware OCR, Q&A with page citations, automatic symbol counting, area calculation, and interactive BIM model viewer. | 5 Tools · 2 Skills · 7 UI Slots |
| Remagine | AI short-video creation: the assistant writes Remotion (React/TSX) code in a per-video workspace, provides live preview via esbuild, and renders final MP4 via Remotion Lambda. | 11 Tools · 1 Skill · 1 UI Slot |

### The App Store interface

The Apps tab has four sub-views:

- **All** — every app available to your org.
- **Installed** — apps currently active.
- **Catalog** — the full marketplace of apps you can add.
- **Recovery** — soft-uninstalled apps. Deleted apps stay here for 30 days before permanent removal, so you can restore them if needed.

### Installing an app

1. #### Open the Catalog tab

   Go to _Console → Your org → Apps → Catalog_.

2. #### Install

   Open the exact Development, Staging, or Production environment card, review its version and contributions, and click _Install_. The environment is installed for the organization and inherited as enabled by every workspace.

![The exact Production environment detail for the locally seeded contract-probe app, with version, status and surface overview](/manual/assets/product/console-app-detail-20260813.webp)
_App detail stays pinned to one exact environment and shows its current installed state._

3. #### Review workspace access

   Go to the workspace detail screen → _Apps_ tab. The newly installed app is enabled by default. Disable it only for workspaces that must not expose it; re-enable it there to remove that restriction.

![The workspace Apps config tab showing the installed demo app and its inherited access state](/manual/assets/product/ws-apps-config-20260813.webp)
_Missing override means enabled; the control creates or changes an explicit workspace override._

4. #### Uninstall

   Click _Uninstall_ from the Installed tab. The app moves to Recovery for 30 days. During that window you can restore it. After 30 days it is permanently removed.

> [!WARNING]
> App credit usage
>
> Each app consumes credit when used. Check the Credit Logs tab to see per-app consumption, and set workspace-level credit alerts if you need to cap spending.

### Inviting people

Invite teammates from _Console → Your org → Participants → Invite_.

1. #### Send the invite

   Enter the person's email and pick their org role (Member or Admin). Optionally add them to a workspace and set a workspace role at the same time.

![The invite panel on the members page with an email address entered and a role selected, ready to send](/manual/assets/product/console-invite-form-20260813.webp)
_Enter the address, pick the organization role, send._

2. #### The invitee accepts

   They receive an email with a link. Clicking it asks them to sign in or register, then auto-joins them to the org and any workspace you specified.

3. #### Resend or remove

   Until accepted, an invitee appears as _Inactive_ in the Participants table. Open their actions menu to copy the invite link, resend the invite, or remove it if it was sent in error.

![The Participants table with the inactive invitee action menu open, showing Copy invite link, Resend invite, and Remove](/manual/assets/product/console-invite-pending-20260813.webp)
_The Inactive row remains until the invitation is accepted or removed; its action menu owns every pending-invite action._

### API keys

API keys authenticate approved machine-to-machine integrations. The first-party _Configure Embed_ public deployment does not use an organization API key in the browser; it exchanges its publish/embed identity for a constrained guest session. If you only use chat or the built-in public widget, do not create a key for that purpose.

1. #### Open API Keys

   Go to _Console → Your org → API Keys_ in the left sidebar.

![The API Keys screen listing keys with their public key, scope, status, last use and creation date](/manual/assets/product/console-api-keys-20260813.webp)
_Each key shows its scope and whether it has ever been used._

2. #### Create a key

   Click _Create API Key_ (top-right). A dialog opens — enter an optional **Name** (e.g. "Production environment") to identify it later, then click _Create_.

![The Create API Key dialog with a name entered](/manual/assets/product/console-api-key-dialog-20260813.webp)
_The name is only a label for you; it does not affect what the key can do._

3. #### Save the key and secret immediately

   The "API Key created" screen is shown **only once**. It displays both the **API Key** (format: `sota_ek_…`) and the **API Secret**. Copy both now and store them securely — they cannot be retrieved after you close this screen.

![The dialog shown once after creation, with the API key, the masked secret, and a warning that the secret will not be shown again](/manual/assets/product/console-api-key-created-20260813.webp)
_The secret is shown once. Copy it before closing this dialog._

4. #### Confirm and close

   After saving, click _I have saved the Secret_ to close the dialog.

5. #### Use the key in approved API calls

   Use the key only with the machine API and scopes documented for your integration. Its main use is signing the ticket handshake that puts SotaAgents inside your own systems — see [Embedding SotaAgents in your own systems](/manual/enterprise-integration/embedding-in-your-systems). For a public guest widget, use _Workspace → Configure Embed_ instead.

6. #### Disable a key

   To stop using a key without deleting it, click the _Actions_ button on the key's row and choose _Disable_. Confirm in the dialog. The key stops working immediately and remains visible in the list in a disabled state.

> [!WARNING]
> Keep keys secret
>
> Never commit API keys to source control or share them in chat. Treat them like passwords. If a key is exposed, disable it immediately and create a new one.

### Audit logs

Audit logs give admins a full record of who did what and when — at both the org level and individual workspace level. They are read-only and can be exported to CSV.

### What's logged

| Column | Description |
| --- | --- |
| Time | Timestamp of the action. |
| Actor | The user or system that performed the action. |
| Action | The type of event: _create_, _update_, _delete_, or _execute_. |
| Target | What was acted on — an app, datasource, integration, participant, or chat. |
| Details | Click to expand the full event payload, including the complete conversation context and structured data for the action. |

### Accessing logs

![The organization activity log with filters by action type, summary analytics and the event rows](/manual/assets/product/console-activity-20260813.webp)
_The org-level log covers every workspace; filters narrow it by action type._

- **Org-level** — go to _Console → Your org → Activity_. Shows all events across every workspace in the org.
- **Workspace-level** — go to _Console → Your org → Workspaces → [workspace] → Activity_. Scoped to that workspace only.

### Filtering and exporting

Use the search box, actor filter, action filter, and date-range picker to narrow the log. Click _Export CSV_ to download the current filtered view.

![The workspace activity log with its analytics strip, event chart and per-conversation rows](/manual/assets/product/ws-activity-20260813.webp)
_The workspace log narrows the same record to one workspace._

### Credit management

SotaAgents bills usage as _credit_ at the organization level. Every chat turn, document generation, image generation, and tool call consumes credit. Navigate to _Console → Your org → Credits_. The current screen has five tabs: Credit Allocation, Member Allocation, Credit Alerts, Credit Logs, and Guest Credits (when admitted for your role/deployment).

![The Credits screen on its Credit Allocation tab, showing the plan, seat configuration and the base credit unit](/manual/assets/product/console-credits-20260813.webp)
_Credits are managed for the whole organization: the pool, the seats and the limits._

### Credit Allocation

The top of this tab shows a KPI band summarising the org's credit position. What appears depends on your plan:

- **Business / Enterprise (pooled):** Pool total, Consumed, Remaining, and Seats count. A live pool consumption bar shows usage against the org-wide cap.
- **Other plans (per-seat):** Seats, Base unit, Billing cycle, and Reset day.

Below the KPI band, a per-seat usage bar list shows the top 10 members by consumption. Bars are colour-coded: green (within limit), amber (near limit), red (blocked / over quota).

The **Reset day** (cutoff day, 1–28) and **Seats** fields are editable by the SotaAgents operations team only — not by org owners or admins.

#### Credit packages (Business and Enterprise only)

On Business and Enterprise plans, org owners and admins can create named credit packages — each defined by a multiplier applied to the org's base unit (base unit is plan-derived and cannot be edited). One package is marked as the default and assigned to new members automatically. The table shows a _Members_ count. A package assigned to one or more members cannot be deleted; reassign those members first. Existing assignments are preserved when a package is deactivated.

![The Credit Packages table showing multipliers, credits, member counts, status, and disabled delete actions for assigned packages](/manual/assets/product/console-credit-packages-members-20260903.webp)
_The Members column explains why an assigned package cannot be deleted._

#### Rolling 5-hour limit

Editable by org owners and admins. Set a maximum number of credits any single user can spend within any rolling 5-hour window. Leave blank for unlimited. Once a member hits their limit, further requests are blocked until the window moves forward. Available on all plans.

### Member Allocation

A paginated table of all org members showing each person's consumed credits, allocated credits, remaining balance, and a progress bar. Rows highlighted in red are over quota (consumed exceeds allocated).

![The Member Allocation tab showing each member with their credit usage and remaining balance](/manual/assets/product/console-credits-allocation-20260813.webp)
_Member Allocation shows what each person has used and what is left._

- **Search and filter** by name, role (Owner / Admin / Member), or credit package.
- **Assign a package** to a member (Business and Enterprise only, org owner or admin) — overrides the default package for that user.

![Member Allocation with the credit-package filter open above the member table](/manual/assets/product/console-credit-package-filter-20260903.webp)
_Package filtering makes it easy to review everyone on the same allocation policy._

### Credit Alerts

Configure per-workspace credit alerts so admins are notified by email when usage crosses a threshold. Alerts fire based on a configurable rolling time window (1–24 hours).

1. #### Select a workspace

   Go to _Console → Your org → Credits → Credit Alerts_ and choose the workspace to configure from the dropdown.

2. #### Set the monitoring window

   Enter a _Window (hours)_ value between 1 and 24. The alert checks credit consumption within this rolling window.

3. #### Set thresholds

   Toggle on a _Warning_ threshold, a _Critical_ threshold, or both. Enter a credit amount for each. The critical threshold must be higher than the warning threshold.

4. #### Add extra recipients (optional)

   Enter additional email addresses (comma or newline separated, up to 10) to notify beyond the default workspace admins.

5. #### Save

   Click _Save_. Email alerts fire when the workspace crosses a threshold within the configured window.

### Credit Logs

An append-only ledger of every credit transaction. Filter by date range (30d / 90d / custom), provider (chat, Knowledge Base, Office App, Web Search, CAD, Remagine, sandbox…), user, or free text. Export to CSV. Visible to org admins and owners only.

Each entry records: timestamp, user, provider, credits charged, USD cost, and the balance after the transaction.

### Guest Credits

Guest/public chat draws from a dedicated organization guest pool, separate from normal member allocation. Organization Owners/Admins can read the add-on, review pool and per-workspace usage, and set workspace monthly limits and alert thresholds. Creating, pausing, resuming, scheduling cancellation, or changing the Guest Credit add-on itself is a SotaAgents operations action.

### Chat models

The **Chat Models** tab (_Console → Your org → Chat Models_) controls which AI models are available across your org and sets the org-wide default.

![The Chat Models tab showing the organization default, the number of selected models, provider filters and the model rows with their credit multipliers](/manual/assets/product/console-chat-models-20260813.webp)
_Chat Models decides which models the organization exposes, and which one is the default._

### Availability

Use the organization allow-list to control which models members can select. Platform-restricted models do not appear in the chat picker. An eligible Owner/Admin can submit or cancel an access request here; the SotaAgents operations team reviews it, and only an approved grant makes the model selectable. Organization owners and admins receive an email when access is granted.

### Org default

Mark one model as the org default — this is pre-selected in the chat input for every member who hasn't pinned a different model themselves.

> [!NOTE]
> Save before leaving
>
> Changes take effect only after you click _Save_. A save bar appears at the bottom of the page when you have unsaved edits.

### Security

The **Security** page (_Console → Your org → Security_) has SSO and IP Access Rules surfaces for org owners and admins (plus the SotaAgents operations team when assisting you).

### IP Access Rules

Add the trusted IPv4 CIDR ranges, then enable the organization IP policy. Once enabled, requests from an address outside every active rule are rejected with `ORG_IP_DENIED`. The workspace shell shows a persistent access-denied notice; use an allowed network or ask an administrator to correct the rules. Plan and role gates still apply.

> [!WARNING]
> Enterprise plan required
>
> Configuring or enforcing SSO requires an Enterprise Cloud subscription. You can still remove an existing provider or disable enforcement on any plan.

### SSO protocol

Choose between two protocols:

| Protocol | What to provide |
| --- | --- |
| OIDC | Issuer URL, Client ID, Client Secret. Optionally a custom Discovery Endpoint (defaults to `issuer/.well-known/openid-configuration`). Copy the _Callback URL_ shown on the form into your IdP's allowed redirect URIs. |
| SAML | IdP Entry Point URL, IdP Issuer, and the IdP certificate (PEM). Optionally paste the IdP metadata XML. Copy the _ACS URL_, _SP Entity ID_, and _SP Metadata URL_ shown on the form into your IdP. |

> [!NOTE]
> Secrets are write-only
>
> Client Secret (OIDC) and certificate (SAML) are never shown after saving — the form only displays whether one has been set. Re-enter the value to replace it.

### Email domains

Add the email domains your org uses (e.g. `company.com`). A domain must be verified before SSO enforcement can be enabled.

#### Verification methods

| Method | How it works |
| --- | --- |
| DNS TXT | A challenge token is generated when you add the domain. Create a DNS TXT record at `_sotaagents-sso-verify.<domain>` containing the token, then click _Verify DNS_. |
| Manual | The SotaAgents operations team reviews and approves or rejects the domain request. |

### Test SSO

Click **Test SSO** to start a real round-trip with the configured IdP. The result appears inline — the connection test must pass before enforcement can be turned on.

### Enforce SSO

The _Enforce SSO_ toggle requires members to authenticate through the configured IdP. It can only be enabled when:

- A provider has been saved.
- At least one domain is verified.
- The SSO connection test has passed for the current configuration.

> [!WARNING]
> Locked out?
>
> If enforcement is on and your IdP becomes misconfigured, contact support. The SotaAgents operations team can disable enforcement via a recovery endpoint to restore access.

### Guardrails

Guardrails keep conversations safe: they block prompt-injection and jailbreak attempts, mask personal data, and screen tool/document output. Configure them from the workspace's direct **Guardrails** navigation item. Available to authorized organization owners/admins.

### What guardrails check

The Hub exposes two workspace-controlled planes. Core also applies a platform-managed reasoning safeguard:

| Mode setting in the Hub |  | Meaning |
| --- | --- | --- |
| _User input enforcement_ | Your messages | The text people type, checked before the assistant answers. Personal data (emails, phone numbers) can also be masked here. |
| _Tool output enforcement_ | Tool & document content | Text the assistant brings in from tools, uploaded files, the web, or the knowledge base — checked before it is used. |
| _Model reasoning_ | Reasoning on its way to the browser or storage | Platform-managed detection masks secrets and personal data before reasoning is streamed or saved. This plane is not a workspace mode selector. |

### What each mode does

You choose a mode separately for _User input enforcement_ and _Tool output enforcement_. Here is what each mode does in both cases:

| Mode | User input enforcement | Tool output enforcement |
| --- | --- | --- |
| Off | Workspace-selected input checks do not run. | Workspace-selected tool-output checks do not run. |
| Observe (log only) | A match is recorded in the log; nothing is changed or blocked. | A match is recorded in the log; nothing is changed or blocked. |
| Redact (mask) | Personal data (e.g. an email) is hidden before the assistant sees it. | The matched part is hidden when possible, then the content continues. |
| Enforce (block) | A risky message (e.g. a jailbreak attempt) is blocked; personal data is still masked. | Unsafe content is normally withheld and a safety notice is shown. Knowledge Base app tool results are capped at soft enforcement: matches can be logged or redacted but the retrieved document is not blocked. |

> [!NOTE]
> Platform protections still apply
>
> The workspace master switch and _Off_ disable workspace-selected checks only. Platform-mandatory guards can still run, and the Hub marks them as _Enforced by the platform_.

### Turn guardrails on for a workspace

1. #### Open the Guardrail Hub

   Open the workspace in Console and choose _Guardrails_ from its sidebar. If it is absent, your current role cannot view it.

![The Guardrails tab showing platform-enforced safeguards, model-reasoning masks, User input set to Enforce, Tool result set to Redact, and two of eleven workspace guards selected](/manual/assets/product/ws-guardrails-20260813.webp)
_The Hub separates platform-enforced safeguards from workspace controls; this example enforces user input, redacts tool results, and selects 2 of 11 guards._

2. #### Turn on the master switch

   Toggle _Enable guardrails for this workspace_. This controls workspace-selected guards; platform-mandatory protections remain active.

3. #### Choose how strict to be

   Set how guardrails act on _your messages_ and on _tool output_ (Observe, Redact, or Enforce). A short note under each option explains what it does.

4. #### Pick which guards run

   Tick the guards you want for this workspace; search or filter to find them. Leave the list untouched to keep the recommended defaults. A few guards need some values from you (a list of banned words, competitor names, or allowed topics) — fill those in, otherwise you can't save.

5. #### Save

   Click _Save_. Changes take effect immediately for new messages. Use _Export rules (CSV)_ to download the current configuration.

### Available guards

Guards marked _Recommended_ are on by default. For the last three, you provide the list they check against.

![The recent blocks log listing blocked, redacted, shadow, and model-reasoning events with their stage, guard, and a masked text preview](/manual/assets/product/ws-guardrails-events-20260813.webp)
_Recent blocks records input, tool-result, and model-reasoning detections without exposing the sensitive text._

| Guard | Protects against | You provide |
| --- | --- | --- |
| Jailbreak / Prompt Injection | Attempts to trick or hijack the assistant, including hidden instructions inside documents | Nothing (Recommended) |
| Secrets & Credentials | API keys, passwords and tokens in a message | Nothing (Recommended) |
| Personal Data (PII) | Emails, phone numbers and other personal data (masked) | Nothing (Recommended) |
| Toxic Language | Toxic, hateful or harassing language | Nothing |
| Profanity | Profane or obscene words | Nothing |
| NSFW Text | Sexually explicit / not-safe-for-work text | Nothing |
| Toxic Language (Multilingual) | Toxic language across languages, including Vietnamese and Japanese | Nothing |
| Gibberish / Nonsense | Garbled or nonsensical input | Nothing |
| Unusual / Manipulative Prompt | Manipulative or social-engineering prompts | Nothing |
| Banned Words | Words or phrases you don't allow | Your list of words |
| Competitor Mentions | Mentions of competitors you name | Competitor names |
| Restrict to Topics | Anything outside the topics you allow | Allowed topics |

### Who can view and edit guardrails

Guardrails use a single permission: whoever can open the Hub can also change it. There is no read-only view — the configuration, the guard list, and the _Recent blocks_ log are all covered by the same right to manage the workspace.

| Role | Guardrails access |
| --- | --- |
| Organization Owner | Yes — in every workspace of the organization |
| Organization Admin | Yes — in every workspace of the organization |
| Workspace Admin | Yes — in the workspaces they administer |
| Workspace Member | No — the _Guardrails_ item is not shown |
| Organization Member with no workspace admin role | No |

Organization Owners and Admins receive workspace-admin rights in every workspace automatically, so granting either role hands over guardrail control for the whole organization. To limit someone to one workspace, leave them an Organization Member and make them a Workspace Admin there. SotaAgents platform support staff can also reach the Hub when assisting you.

> [!NOTE]
> Hiding the menu is not the control
>
> The missing sidebar item only reflects the same rule the API enforces: a request from an account without workspace-admin rights is rejected, so guardrails cannot be read or changed by other means.

### Which guards the platform enforces

Two guards in the table above are platform-mandatory by default. They run on every workspace even when the master switch is off or a mode is set to _Off_, and they have no tickbox:

| Guard | Who controls it |
| --- | --- |
| Jailbreak / Prompt Injection | Platform — always on, including the screening of tool and document output |
| Secrets & Credentials | Platform — always on |
| Personal Data (PII) | You — except in model reasoning, where the platform masks it |
| The other nine guards | You — tick, untick, and choose the mode per workspace |

Model reasoning is also platform-managed: secrets and personal data are masked there regardless of which guards you tick. A platform-mandatory row follows the platform's own mode, not your workspace mode, so it can be blocking while your workspace sits on _Observe_.

Your deployment is the final word on this, because the mandatory set is an operator setting. Read it off the Hub: everything under _Enforced by the platform_ is out of your hands, and everything with a tickbox is yours. If that section is absent, the platform layer is switched off in your deployment and all twelve guards are workspace-controlled.

### How long guardrail logs are kept

_Recent blocks_ keeps events for **30 days** by default, after which each entry is deleted automatically. The window is a platform setting, so a self-hosted or dedicated deployment may be configured differently — ask your platform operator if you need the exact number for an audit.

Each entry records the time, the stage (user input, tool output, or model reasoning), what happened (blocked, redacted, observed, or not evaluated), the guard that fired, and the mode in force. The flagged message itself is never stored: entries keep its length and a short fingerprint, plus a masked excerpt of at most 280 characters. For secret and personal-data findings even that excerpt is omitted, so the log cannot leak what the guard just caught.

> [!NOTE]
> Treat the log as a debugging aid, not an archive
>
> It answers "which message was stopped, where, and by which guard" while the incident is recent. If you must retain guardrail activity for longer than the retention window, copy what you need out before it expires. _Export rules (CSV)_ exports the configuration, not the log.

## Developer Guide

Build, test, deploy, release, and publish a SotaAgents app from one maintainable project.

### What apps can do

A SotaAgents app is a versioned capability package that extends the platform inside an organization and its workspaces. One app can contribute callable **tools**, reusable **skills**, native UI surfaces, tool-result renderers, hooks, events, prompts, and scoped data access. The platform owns identity, installation, authorization, environment selection, asset delivery, and audit. Your app owns its business logic and app-specific data.

### Why apps exist

SotaAgents ships one assistant, but no assistant can know your CAD drawing standards, your legal-review procedure, or the API of the ERP your company runs on. Rather than growing the core product for every domain, the platform is deliberately incomplete: an app is how a team adds the missing capability, and the platform bends around it. The same shell serves a legal document-review app, a knowledge base, a CAD generator, an office-document builder, and a web-search app precisely because none of that domain logic lives in the core.

Concretely, an app is a directory containing a `manifest.yaml` and — when the capability needs computation, private credentials, external APIs, or durable domain data — an HTTP service you host yourself. Nothing is discovered by inspecting your code. The platform exposes exactly what the manifest declares, for exactly the organization, workspace, and environment it resolved, and refuses anything undeclared.

Two things follow. The assistant gains abilities it could not otherwise have: a **tool** it can call to query or change your systems, a **skill** that teaches it your procedure, a **screen** that renders your data inside the workspace. And your team ships those abilities on its own release cadence, in its own language and runtime, without any change to SotaAgents itself.

> [!NOTE]
> An app is the unit of extension, not a plugin script.
>
> It is versioned, installed per organization, enabled by default in every workspace unless an administrator adds a workspace override, and resolved to one exact environment on every request. That is what makes third-party domain logic safe to run beside first-party features.

Tools

Typed backend actions the assistant can call.

Skills

Instructions and workflows that guide the assistant.

Native UI

Workspace pages, admin screens, inline views, and artifact renderers.

App data

Authorized, environment-scoped storage and backend access.

![Organization App Store containing installed and catalog apps](/manual/assets/developer/app-store-catalog.webp)
_Apps are discovered and installed at organization level, inherit enabled access in workspaces, and support explicit workspace overrides._

![Knowledge Base app running as a native workspace page](/manual/assets/developer/app-workspace-native-ui.webp)
_A production app can provide a complete workspace experience while keeping platform navigation, identity, and access control._

![An expanded Web Search tool result rendered inside a SotaAgents conversation](/manual/assets/developer/web-search-tool-result.webp)
_The expanded app action shows its query, visual previews, and source cards directly inside the conversation._

![An Office App presentation artifact open beside its SotaAgents conversation](/manual/assets/developer/slide-artifact.webp)
_The durable slide artifact opens in the workspace panel while its originating conversation and artifact card remain visible._

### Where app experiences appear

| Manifest surface | Use it for | Typical host |
| --- | --- | --- |
| `page` | Persistent workflows with navigation and state. | Workspace or admin pages. |
| `tool-view` | Render one bound tool call, including optional input-streaming state. | Inside a conversation tool result. |
| `message-part` | Render a bound message contribution inline. | Inside an assistant or user message. |
| `artifact` | A durable output users can reopen, inspect, or export. | Artifact panel. |
| `card` | Compact app-owned status or entry content. | Workspace assistant card slot. |
| `composer-action` | A compact action beside the chat input controls. | `chat.composer.actions`. |
| `composer-panel` | Contextual, app-owned UI that can read and atomically edit the active draft. | `chat.composer.panel`. |

`useComposer` is intentionally available only inside a `composer-panel`. A `composer-action` is a separate surface and does not receive direct draft-edit authority.

> [!NOTE]
> The platform always resolves an exact environment.
>
> Every tool, screen, asset, and data request belongs to Development, Staging, or Production. There is no automatic fallback from one environment to another.

### Architecture philosophy

SotaAgents deliberately separates the **platform control plane** from the **app data plane**. The platform knows the user, organization, workspace, installation, selected environment, exact artifact, and granted capabilities. Your backend knows the domain: how to search documents, generate CAD, call a proprietary API, or apply business rules.

![Architecture diagram showing a request resolved through SotaAgents to an exact app environment](/manual/assets/developer/app-architecture.svg?v=2)
_One authority decision binds the request to an exact artifact, UI bundle, backend, and environment-scoped data._

### Why should an app have its own backend?

- **Independent ownership:** ship business logic on your cadence without adding domain code to the SotaAgents core.
- **Security boundary:** keep proprietary credentials and third-party integrations server-side; accept only signed platform context.
- **Data boundary:** choose the database, retention, residency, and scaling model the domain requires.
- **Operational isolation:** an app can scale, fail, and recover without coupling unrelated apps.

> [!WARNING]
> A separate backend is not a separate identity system.
>
> Do not ask the browser to send an arbitrary actor, workspace, or environment. Verify the signed Sota request and use the exact context selected by the platform.

### How the assistant calls your tool

Declaring a tool in the manifest is only half the story. This is what the platform does with that declaration at runtime — the part that makes an app more than a hosted web service.

1. #### The tool is offered to the model

   When a workspace resolves its app surface, every published tool becomes one model-callable function named `app_<appId>_<toolName>`. Development and Staging insert the environment (`app_my-app_stg_example`); Production omits it, so the name the model learns in Production stays clean. Characters the model-facing name cannot carry — including the dots in a name like `documents.query` — become underscores, names longer than 64 characters are truncated with a short hash suffix, and a collision between two apps gets a numeric suffix. The description the model reads is your manifest `description` followed by `App: <appId>. Runtime tool: <name>.`, and your `inputSchema` is handed to the model verbatim as the parameter schema. That is why a vague description or a loose schema degrades tool selection immediately: they are the entire basis on which the model decides.

2. #### The platform resolves and authorizes the call

   Before any request leaves SotaAgents, the arguments are validated against your input schema, and the platform resolves the organization, workspace, installation, environment, and the one exact artifact that applies. A tool that is not published in that artifact, or an app that is not installed for that workspace, is refused here — your backend is never reached.

3. #### A signed request reaches your backend

   The platform issues a server-to-server HTTP request to the `service.baseUrl` resolved for the active environment, at your declared path. In Development the same request travels through the `sota dev` tunnel to your laptop instead. The browser is never in this path, so your backend does not need to be reachable from the public internet by users.

4. #### Your backend verifies, works, and answers JSON

   Verify the token, do the domain work, return JSON. A non-2xx response, a body that is not valid JSON, or a timeout is treated as a tool failure.

5. #### The result re-enters the conversation

   The platform streams the tool result into the conversation as the tool's output part, records it, and — if a UI contribution declares `surface: tool-view` with this tool in `toolNames` — renders your React module in place of the plain result. The model sees a compact JSON projection of the same result.

### The request the platform sends

The body is an envelope. The model's arguments are nested at `body.input`, which is why the scaffolded route reads `request.body.body.input` rather than `request.body`.

HTTP

```
POST https://my-app.example.com/tools/example
content-type: application/json
authorization: Bearer <invocation JWT>
x-sota-core-token: <delegated Core token>   # only when the app declares Core tool grants

{
  "requestId": "01K...",
  "organizationId": "6a66...",
  "workspaceId": "6a66...",
  "appId": "my-first-project-demo",
  "hook": { "type": "tool", "name": "example" },
  "body": {
    "input":   { "message": "hello" },
    "context": { "organizationId": "…", "workspaceId": "…", "userId": "…",
                 "conversationId": "…", "agentId": "…", "toolCallId": "…", "config": {} }
  }
}
```

> [!WARNING]
> Trust the token, not the envelope.
>
> The `organizationId`, `workspaceId`, and `context` fields in the body are conveniences for logging and debugging. Every authorization decision must come from the verified JWT claims, because only those are signed.

### The invocation token

The `Authorization` header carries a short-lived, EdDSA-signed JWT minted for this one call. Your backend verifies it against the platform's public keys, published at `<core origin>/.well-known/jwks.json`. The app holds no Sota private key and never mints tokens itself.

| Property | Value |
| --- | --- |
| Algorithm / `typ` | `EdDSA` (Ed25519), header type `sota-invocation+jwt` |
| `iss` | `sota/invocation-token` |
| `aud` | Your `appId` — reject a token minted for another app |
| Lifetime | 60 seconds, with 30 seconds of accepted clock skew |
| `oid` / `wid` | Organization and workspace the call belongs to |
| `sub` | The acting user, when the call has an actor |
| `iid` | Compatibility install identity. Treat it as opaque; do not infer Development/Staging/Production from a prefix. |
| `ae` / `aei` / `ag` | Exact App Environment name, environment identity, and Backend Access Generation. The three claims arrive together and bind authorization to one resolved backend. |
| `aer` | Signed exact execution reference. Persisted/replayed work must keep this reference instead of resolving a newer artifact. |
| `scp` | Granted scopes. Current families include `tool:<name>`, `route:prompt:<name>`, `route:systemPrompt:<name>`, `event:<name>`, `route:artifact:<name>`, `route:resolver:<path>`, lifecycle route scopes, and `app:http`. Require the exact scope presented for that endpoint. |
| `rid` | The `requestId` from the envelope, for correlating logs |

Enforce the narrowest scope on each route: the `/tools/example` handler should require `tool:example` and nothing broader. The scaffolded `src/backend/sota-auth.ts` shows the complete check — issuer, audience, algorithm, type, expiry, tenant claims, and scope — in one reusable middleware.

### What the response has to look like

Return a normal JSON HTTP response. There is no Sota-specific wrapper to construct.

| Outcome | What the platform does |
| --- | --- |
| 2xx with a JSON body | Success. The body becomes the tool result. |
| Non-2xx | Failure. If the body is `{ "code": "…", "message": "…" }` — optionally with `details` and `hint` — those exact values are carried through instead of a generic message. Return a real status code rather than a success-shaped error body. |
| Body is not valid JSON | Failure with `VALIDATION_ERROR`. |
| No answer within the deadline | Failure with `TIMEOUT`. |

`outputSchema` is not enforced against the live response — it is checked when you publish, where it drives breaking-change detection between versions. Validate your own output at the boundary; the schema is the contract you promise consumers and renderers, not a runtime guard.

Two optional reserved keys let one result serve two audiences. Put the compact conclusion the model needs and the rich payload your renderer needs in the same response, then use `_sota.modelOutput` to give the model a completely different projection, or `_sota.modelProjection.omitKeys` to drop renderer-only keys from what the model reads. The model-visible projection is truncated at roughly 32,000 characters, so keep it to conclusions, ids, counts, and citations rather than raw documents.

### Lifecycle & environments

![Lifecycle diagram: a development session becomes an immutable Staging artifact, which sota release promotes unchanged to Production, with separate data partitions and independent visibility](/manual/assets/developer/app-lifecycle.svg?v=2)
_Development is a personal live session — local UI and backend, one selected workspace, no immutable artifact. `sota deploy` packs the Staging artifact; `sota release` promotes that exact artifact to Production. Data partitions stay separate, and visibility is managed independently._

![App Registry lifecycle view with Staging, Production, and App Store status](/manual/assets/developer/app-registry-lifecycle.webp)
_App Registry shows the exact Staging and Production versions. App Store listing is a separate Production governance state._

### Sota CLI

**Sota CLI** is the supported developer interface. It scaffolds projects, validates manifests, builds native UI contracts, opens live Development sessions, creates immutable Staging artifacts, and promotes them to Production.

Terminal

```
curl -fsSL https://app.sotaagents.ai/cli/install.sh | sh
sota --version
sota login
sota whoami
sota update
```

Windows PowerShell:

PowerShell

```
irm https://app.sotaagents.ai/cli/install.ps1 | iex
```

### The command surface

Run `sota --help` for the authoritative list on your installed version. Every command accepts the global options `--cwd <dir>`, `--origin <url>`, `--manifest <path>`, `--json`, and `--verbose`.

| Group | Commands | What they do |
| --- | --- | --- |
| Session | `login`, `logout`, `whoami` | Authorize this machine with a scoped app-developer session for one origin. `login --manual` does copy/paste authorization for headless or remote machines. |
| Scaffold | `init [dir]`, `add [features…]` | Create a project, or add capabilities to an existing one. Both are additive: they preserve your files and report collisions instead of overwriting. |
| Config | `config set-origin <url>`, `config show`, `config set <key> <value>` | Manage `.sota/config.json`. `set-origin --env <name>` saves a per-environment origin that `deploy -e` and `validate -e` then select. |
| Manifest | `manifest schema [--version <major>]`, `manifest examples`, `manifest explain <topic>`, `manifest diff <left> <right>` | Print the exact supported schema major the server validates against, show worked examples, explain one field, and diff two manifests. |
| Checks | `validate`, `build`, `contracts ensure`, `env template`, `env check` | Server-side manifest preflight (local lint when offline), package existing build outputs, materialize the App UI type contract, and render or check `.env.example` from the manifest's `env` declarations. |
| Develop | `dev`, `dev status`, `dev stop` | Publish a personal Development session bound to your local backend and locally built assets, inspect it, and stop it. |
| Ship | `deploy` (optionally `--release`), `release` | Build one immutable artifact and run it in Staging; optionally promote that exact artifact in the same command, or promote the artifact Staging already runs. |
| Inspect | `status`, `logs`, `workspaces`, `catalog`, `installed --org <id>`, `app info <slug>`, `app config` | Read what the platform currently holds: current environments, backend logs (including exact `--environment`/`--environment-id` filters), the workspaces you can develop in, the app catalog, and per-project app configuration. |
| Maintenance | `update`, `update skills`, `docs [topic]` | Replace the binary, refresh the bundled development skill in an app project, and open version-aware developer documentation. |

> [!NOTE]
> Targeting another platform deployment
>
> Use the global `--origin` option when developing against Staging, for example `sota --origin https://v4.stg.sotaagents.ai login`. Keep the same origin for subsequent lifecycle commands, or save it once with `sota config set-origin`.

> [!WARNING]
> The CLI never builds your app for you.
>
> `sota build` packages outputs that already exist; it does not replace your frontend or backend build, and `sota dev` does not start or stop your processes. Build your source first, then let the CLI package, publish, or tunnel it.

### Create a project

Terminal

```
sota init my-app --features all
cd my-app

# terminal 1 — your own watchers
npm run dev

# terminal 2 — the Development session
npm run dev:sota
```

`sota init` creates a current Manifest v3 app. Run it without `--features` and it asks which capabilities to scaffold; pass `--features admin-screen,skill,backend,tool,tool-result-ui` (or `all`) to answer up front. Add more later with `sota add`, which is additive and reports collisions rather than replacing your code.

### Scaffold features

| Feature | What it adds | Implies |
| --- | --- | --- |
| `admin-screen` | A React + TypeScript + Vite native page with a watch build for local development. | — |
| `skill` | A content-backed `SKILL.md` contribution. No backend required. | — |
| `backend` | A minimal Express service with request logs, Core JWT verification, and a health route. | — |
| `tool` | An agent-facing tool: manifest entry, input/output JSON Schemas, and the backend route that answers it. | `backend` |
| `tool-result-ui` | A tool plus a React surface rendered below its result in the conversation. | `tool`, `backend` |

> [!NOTE]
> The scaffold is a convenience, not a requirement.
>
> Express, npm, and TypeScript are used because they are widely understood. The platform contract is plain HTTP plus a manifest — any language or runtime that can serve the declared routes and verify the invocation token is a valid app backend.

### What the template contains

| Path | Purpose |
| --- | --- |
| `manifest.yaml` | Canonical identity, version, contributions, runtime bindings, health, locales, and platform compatibility. |
| `src/backend/` | Authenticated service, health endpoint, tool routes, and Sota request verification. |
| `src/ui/` | Native React modules and styles for declared UI slots. |
| `src/skills/` | Content-backed assistant skills. |
| `src/schemas/` | JSON Schemas for tool inputs and outputs. |
| `src/locales/` | Localized app labels and strings. |
| `.sota/app-ui-contracts/` | Generated platform UI contract declarations; regenerate, do not hand-edit. |
| `.agent/skills/` | Bundled development guidance for architecture, security, testing, tools, and UI. |

### Manifest explained

`manifest.yaml` is the declarative contract between your app and SotaAgents. It describes what the app is, what it contributes, where its backend lives, which native modules the platform may load, and which platform versions it supports. The server validates it again; the client manifest is never treated as authority. Unknown properties are rejected outright — run `sota manifest schema` to read the exact JSON Schema and `sota manifest explain contributes.tools` to explain one field.

The five root fields `manifestSchemaVersion`, `appId`, `version`, `publisher`, and `contributes` are required. Everything else is opt-in.

YAML

```
manifestSchemaVersion: 3
appId: my-first-project-demo
version: 1.0.0
displayName: "My First Project Demo"
description: "SotaAgent app."
publisher:
  id: local-dev
  displayName: "Local Dev"
  contact: dev@local-dev.example

contributes:
  skills:
    - name: example
      description: Example content-backed skill.
      appendsTo: system
      content: src/skills/example
      timeoutMs: 3000
      failure_mode: skip
  tools:
    - name: example
      description: Example tool that echoes its input and verified tenant context.
      route: POST /tools/example
      inputSchema: src/schemas/tool-input.schema.json
      outputSchema: src/schemas/tool-output.schema.json
      timeoutMs: 15000
      failure_mode: abort
  ui:
    - id: admin-screen
      kind: nativeModule
      surface: page
      slot: admin.workspace.tab
      sectionId: my-first-project-demo
      label: Admin screen
      route: /admin/*
      module:
        entry: dist/ui/app.js
        export: AdminScreen
        styles: dist/ui/app.css

service:
  baseUrl: https://my-first-project-demo.example.com
health:
  url: https://my-first-project-demo.example.com/health
  intervalSeconds: 60
platform: ^1.2.0
locales:
  default: en
  files:
    en: src/locales/en.json
```

- **Identity fields** are stable. Treat `appId` as a permanent public identifier.
- **Version** is immutable per artifact. Change bytes after a deploy only with a new version.
- **Contributions** are the complete surface the platform may expose; undeclared routes or modules are unavailable.
- **Environment overlays** are optional overrides named exactly `local`, `stg`, and `prod`. The root service is the hosted default; `local` belongs to `sota dev`. Global `--origin` selects a SotaAgents deployment, while command-specific `-e` selects a saved environment profile or manifest overlay.

Each native UI surface and public runtime API is documented on its own page after this manifest reference.

YAML

```
service:
  baseUrl: https://my-app.example.com
health:
  url: https://my-app.example.com/health
  intervalSeconds: 60

environments:
  local:
    service:
      baseUrl: http://127.0.0.1:8787
    health:
      url: http://127.0.0.1:8787/health
  stg:
    service:
      baseUrl: https://stg.my-app.example.com
  prod:
    service:
      baseUrl: https://my-app.example.com
```

> [!WARNING]
> environments.local
>
> The base manifest describes the deployable app. A `127.0.0.1` URL in the root `service` block is rejected, and a deployable base URL still ending in `.invalid` means the real hosted backend was never configured.

### Declaring a tool

A tool is how the assistant reaches your backend. You declare it once under `contributes.tools`; the platform derives everything else — what the model is offered, how the call is routed, how it is authenticated, and how long it may take — from that declaration. The schema is closed, so an undeclared key fails validation.

| Field | Required | Rules |
| --- | --- | --- |
| `name` | Yes | The stable public identifier. Lowercase first letter, then letters, digits, and hyphens, optionally dot-segmented (for example `documents.query`); each segment is at most 64 characters. Keep it stable after release — prompts, renderers, and stored conversations refer to it. |
| `description` | Yes | Non-empty. This is the text the model reads when deciding whether to call the tool, so state when to use it, what the backend does, what comes back, and any precondition. Do not merely restate the name. |
| `route` | Yes | `METHOD /path`, where method is one of `GET`, `POST`, `PUT`, `PATCH`, `DELETE` and the path starts with `/`. The path is resolved against the `service.baseUrl` of the active environment, and may not escape that origin. Declare `POST`: the assistant's invocation path sends the arguments as a JSON body, and the scaffold and every system app use `POST /tools/<name>`. |
| `inputSchema` | Yes | A path to a JSON Schema file inside the app, or an inline schema object. Prefer explicit `required`, `additionalProperties: false`, bounded strings and arrays, and enums for known modes. |
| `outputSchema` | Yes | Same form as the input schema. Required even when the result is trivial — it documents the contract your backend must keep. |
| `timeoutMs` | No | Positive integer, clamped to at most 30,000 ms when the artifact is built. Declare one. A tool that declares none gets no wall-clock budget on a hosted backend, and the Development tunnel bounds it at ten minutes. |
| `failure_mode` | No | `abort` or `skip`; tools default to `abort`. Choose `abort` when a failure must be reported as a failure; choose `skip` only when the contribution is genuinely optional and the assistant can still answer honestly without it. Never hide a failed write or an authoritative query behind a graceful-looking response. |
| `searchHint` | No | Extra keywords that help the tool be found. Non-empty. |
| `undoable` | No | Boolean. Marks the action as reversible. |
| `configGate` | No | An app-config key. The tool is only offered when that configuration key is present. |

The schema files the declaration points at are ordinary JSON Schema. The compiler resolves and inlines them into the artifact, so a referenced file must exist before build or deploy even if the route is never exercised locally.

JSON

```
// src/schemas/tool-input.schema.json
{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "message": {
      "type": "string",
      "minLength": 1,
      "maxLength": 4000,
      "description": "Message for the example tool to echo."
    }
  },
  "required": ["message"]
}
```

Declaring any tool makes `service` mandatory — the platform has to know where to send the call. Tool names must be unique within one manifest, and a UI contribution with `surface: tool-view` must list, in `toolNames`, names that actually exist in `contributes.tools`; the validator rejects a renderer bound to a tool you never declared. Two installed apps may not claim the same tool name for a renderer — the later app loses the contested names.

For what the platform then does with this declaration — the model-visible name, the signed request, the response contract — see _Architecture philosophy_.

![Staging app detail page showing the resolved surface overview](/manual/assets/developer/app-surface-overview.webp)
_The resolved environment exposes only the tools, skills, UI slots, prompts, and grants declared by its exact artifact._

### Workspace pages

A workspace page is a full app-owned screen reached from workspace navigation. Core resolves the current app execution for the installed lane, owns routing and the mount boundary, then renders the exported component. The content inside that boundary belongs to the app; Core does not inject an artifact launcher, environment badge, or use-case-specific controls.

YAML

```
contributes:
  ui:
    - id: reports-page
      kind: nativeModule
      surface: page
      slot: workspace.nav
      sectionId: reports
      label: Reports
      icon: chart
      route: /reports/*
      module:
        entry: dist/ui/app.js
        export: ReportsPage
        styles: dist/ui/app.css
```

Use `workspace.nav` for a normal navigation entry or `workspace.nav.section` for a section placement. Supply a leading-slash `route`; `/*` allows nested app routes. A navigable slot must have a route or `sectionId`. Optional `roles` and `activation` further restrict placement.

TSX

```
import { useAppContext } from '@sota/platform';

type PageProps = { route?: string; subroute?: string };

export function ReportsPage({ route, subroute }: PageProps) {
  const { workspaceId, ui } = useAppContext();
  return (
    <main>
      <h1>Reports</h1>
      <button type="button" onClick={() => ui.openArtifact('reports.viewer')}>
        Open latest report
      </button>
      <small>Workspace: {workspaceId}</small>
      <small>Route: {subroute ?? route}</small>
    </main>
  );
}
```

Core passes the selected app route as `route` and `subroute`; use it as input to an app-local router. Read identity, locale, theme, backend access, lifecycle, and host actions through platform hooks. Do not derive the exact app execution from the route.

### User settings pages

A user settings page is an app-owned tab inside Account Settings. It is modal-local and appears only when Core has an explicit active workspace where the app is installed, enabled, and admitted.

YAML

```
contributes:
  ui:
    - id: user-settings
      kind: nativeModule
      surface: page
      slot: user.settings.tab
      label: Reports preferences
      icon: settings
      route: /reports/user-settings
      module:
        entry: dist/ui/app.js
        export: UserSettings
        styles: dist/ui/app.css
```

Core reads the projected page placement and mounts its exact contribution; the semantic route is not written to the browser URL. Only the selected tab is mounted, so switching tabs or closing Settings aborts scoped requests and runs disposers. Keep drafts in app-owned storage when they must survive unmounting. Every backend mutation must authorize the signed invocation identity; tab visibility is not a security boundary.

### Admin pages

An admin page is the same native `page` primitive placed in workspace administration. Use it for app configuration and operational controls, not ordinary member workflow.

YAML

```
contributes:
  ui:
    - id: reports-admin
      kind: nativeModule
      surface: page
      slot: admin.workspace.tab
      sectionId: reports
      label: Reports settings
      route: /admin/reports/*
      roles: [OWNER, ADMIN]
      module:
        entry: dist/ui/app.js
        export: ReportsAdmin
        styles: dist/ui/app.css
```

`slot` decides where Core mounts the page. Core passes `route`/`subroute` just as it does for a workspace page; the rendered content remains app-owned. Declare `roles` for the intended administrators and still enforce authorization in the app backend. UI visibility is not a security boundary.

TSX

```
import { useAppFetch } from '@sota/platform';

export function ReportsAdmin() {
  const appFetch = useAppFetch();
  async function save() {
    const response = await appFetch('/settings', {
      method: 'PUT',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ enabled: true }),
    });
    if (!response.ok) throw new Error('Could not save settings');
  }
  return <button type="button" onClick={save}>Enable reports</button>;
}
```

### Tool views

A `tool-view` replaces or enriches the visual representation of tool calls declared by the same app. Bind it with non-empty `toolNames`. Core passes one `toolResult` prop and preserves the same mount as the state changes.

YAML

```
contributes:
  ui:
    - id: calendar-result
      kind: nativeModule
      surface: tool-view
      slot: chat.message.inline.below
      toolNames: [readCalendar]
      renderBeforeOutput: true
      module:
        entry: dist/ui/app.js
        export: CalendarResult
```

TSX

```
import type { ToolResultSurfaceProps } from '@sota/platform';

type Input = { from: string; to: string };
type Output = { events: Array<{ id: string; title: string }> };

export function CalendarResult({ toolResult }: ToolResultSurfaceProps<Input, Output>) {
  if (toolResult.state === 'input-streaming') return <p>Preparing…</p>;
  if (toolResult.state === 'output-pending') return <p>Waiting for an action…</p>;
  if (toolResult.state === 'output-error') return <p>{toolResult.errorText}</p>;
  if (toolResult.state === 'output-denied') return <p>Permission denied.</p>;
  if (toolResult.state !== 'output-available') return <p>Running…</p>;
  return <p>{toolResult.result?.events.length ?? 0} events</p>;
}
```

| State | Contract |
| --- | --- |
| `input-streaming` | Best-effort partial `unknown` input; enabled only by `renderBeforeOutput`. |
| `input-available`, approval states | Completed, schema-validated input. |
| `output-pending` | The original tool call is deferred; `deferred.operationId` and opaque app `data` may be present. |
| `output-available` | `result` is the app result after Core transport metadata is removed; `output` retains the raw host value. |
| `output-error`, `output-denied` | Render failure or denial without assuming a result. |

`execution` identifies the app lane that produced the call. It is diagnostic context for the payload, not a request to load that producer's historical artifact.

### Message parts

A native `message-part` is an inline tool surface mounted below the relevant chat message. It uses the same `ToolResultSurfaceProps`, states, producer-lane context, `toolNames`, and optional `renderBeforeOutput` contract as a tool view; choose it when the result should read as message content.

YAML

```
contributes:
  ui:
    - id: source-summary
      kind: nativeModule
      surface: message-part
      slot: chat.message.inline.below
      toolNames: [searchSources]
      renderBeforeOutput: true
      module:
        entry: dist/ui/app.js
        export: SourceSummary
```

TSX

```
import type { ToolResultSurfaceProps } from '@sota/platform';

type SearchOutput = { sources: Array<{ title: string; url: string }> };

export function SourceSummary({ toolResult }: ToolResultSurfaceProps<unknown, SearchOutput>) {
  if (toolResult.state === 'output-pending') {
    return <aside aria-live="polite">Waiting for the app…</aside>;
  }
  if (toolResult.state !== 'output-available') return null;
  return (
    <aside aria-label="Sources">
      {toolResult.result?.sources.map((source) => (
        <a key={source.url} href={source.url} rel="noreferrer" target="_blank">
          {source.title}
        </a>
      ))}
    </aside>
  );
}
```

> [!NOTE]
> This is not a declarative message renderer.
>
> `kind: messageRenderer` decorates matched message text as a citation, mention, or pill. `surface: message-part` loads app React for named tool calls.

### Artifact surfaces

An artifact surface owns the body of a durable side-panel view. Declare a stable app-namespaced `artifactKind`; Core uses it to resolve the current native module in that app lane when the app opens the artifact.

YAML

```
contributes:
  ui:
    - id: report-viewer
      kind: nativeModule
      surface: artifact
      slot: artifact.slot.reports-viewer
      artifactKind: reports.viewer
      module:
        entry: dist/ui/app.js
        export: ReportViewer
        styles: dist/ui/app.css
```

TSX

```
import { useAppContext } from '@sota/platform';

type ArtifactProps = {
  artifact: { artifactKind: string; context?: Record<string, unknown> };
};

export function ReportViewer({ artifact }: ArtifactProps) {
  const { ui } = useAppContext();
  const title = typeof artifact.context?.title === 'string'
    ? artifact.context.title
    : 'Report';
  return (
    <article>
      <h1>{title}</h1>
      <button type="button" onClick={ui.closeArtifact}>Close</button>
    </article>
  );
}
```

Open it from any surface in the same app lane with `useAppContext().ui.openArtifact('reports.viewer', context)`. The lane's current app renders the opaque `artifact.context`; apps own compatibility with payloads created by earlier releases. Artifact kinds must be unique for the resolved workspace; the `core.*` namespace is reserved.

TSX

```
import { useAppContext } from '@sota/platform';

export function OpenReportButton({ reportId }: { reportId: string }) {
  const { ui } = useAppContext();
  return (
    <button type="button" onClick={() => ui.openArtifact('reports.viewer', {
      reportId,
      title: 'Quarterly report',
    })}>
      Open report
    </button>
  );
}
```

Validate `artifact.context` before using it; it is structural transport data, not a substitute for loading authoritative records from the app backend.

### Workspace cards

A `card` is compact app UI placed in the workspace assistant area. It receives no card-specific props; use platform hooks for context, data, and actions.

YAML

```
contributes:
  ui:
    - id: reports-card
      kind: nativeModule
      surface: card
      slot: workspace.assistant-card
      module:
        entry: dist/ui/app.js
        export: ReportsCard
```

TSX

```
import { useAppContext } from '@sota/platform';

export function ReportsCard() {
  const { ui } = useAppContext();
  return (
    <section aria-label="Reports">
      <p>Three reports need review.</p>
      <button type="button" onClick={() => ui.openArtifact('reports.viewer', {
        filter: 'needs-review',
      })}>Review</button>
    </section>
  );
}
```

Keep the component responsive to its host instead of assuming page dimensions. Use a workspace page or artifact when the workflow needs a full canvas.

### Composer actions

A `composer-action` is a compact app-owned action beside the chat input controls. It is appropriate for opening app UI or starting an app workflow.

YAML

```
contributes:
  ui:
    - id: report-action
      kind: nativeModule
      surface: composer-action
      slot: chat.composer.actions
      module:
        entry: dist/ui/app.js
        export: ReportAction
```

TSX

```
import { useAppContext } from '@sota/platform';

export function ReportAction() {
  const { ui } = useAppContext();
  return <button type="button" onClick={() => ui.openArtifact('reports.viewer')}>Reports</button>;
}
```

`useComposer` is deliberately unavailable here. If UI must read or edit the active draft, declare a `composer-panel`; do not reach into Core stores.

### Composer panels

A `composer-panel` is contextual app UI above the composer. It is the only surface bound to the public composer API, so it can observe the draft, make atomic edits, see pending deferred results for its exact execution, and acquire a composer lock.

YAML

```
contributes:
  ui:
    - id: report-panel
      kind: nativeModule
      surface: composer-panel
      slot: chat.composer.panel
      module:
        entry: dist/ui/app.js
        export: ReportPanel
```

TSX

```
import { useComposer } from '@sota/core/hooks';

export function ReportPanel() {
  const value = useComposer((composer) => composer.value);
  const applyEdit = useComposer((composer) => composer.applyEdit);
  if (!value.includes('/report')) return null;
  return (
    <button type="button" onClick={() => applyEdit({
      mode: 'replace',
      content: [{ kind: 'text', text: 'Create a weekly report for ' }],
    })}>
      Use weekly report template
    </button>
  );
}
```

The slot must be exactly `chat.composer.panel`. The panel may render nothing when its app-owned condition is absent. A slash command and a panel are independent contributions.

### useAppContext

`useAppContext()` is the primary runtime API for every hosted native surface. Import it from `@sota/platform`; it throws when no app surface provider is mounted.

TSX

```
import { useAppContext } from '@sota/platform';

export function RuntimeDetails() {
  const context = useAppContext();
  return (
    <dl>
      <dt>App</dt><dd>{context.appId}@{context.appVersion}</dd>
      <dt>Organization</dt><dd>{context.organizationId}</dd>
      <dt>Workspace</dt><dd>{context.workspaceId ?? 'none'}</dd>
      <dt>Surface</dt><dd>{context.surface}</dd>
    </dl>
  );
}
```

| Field | Meaning |
| --- | --- |
| `appId`, `appVersion` | The current app package selected for this lane. |
| `organizationId`, `workspaceId` | Verified tenant context. `workspaceId` may be absent outside workspace scope. |
| `surface` | The current surface kind; use it for presentation differences, not authorization. |
| `theme`, `locale`, `t` | Current host presentation and localization. |
| `fetch` | The same scoped data-plane function returned by `useAppFetch`. |
| `lifecycle` | Mount-owned cancellation and cleanup primitives. |
| `ui` | Host UI actions such as toast, navigation, artifact, and lightbox. |

Values are stamped from the server-resolved execution. Never reconstruct organization, workspace, app, or environment identity from URLs or local storage.

### useAppFetch

`useAppFetch()` returns the mounted surface's authenticated data-plane fetcher. Pass an app-relative path and ordinary `RequestInit`; Core resolves it under the selected environment's backend, injects the bearer credential, binds cancellation to the mount, and refreshes an expired credential once.

TSX

```
import { useState } from 'react';
import { useAppFetch } from '@sota/platform';

export function SaveSettingsButton() {
  const appFetch = useAppFetch();
  const [status, setStatus] = useState('idle');

  async function save() {
    setStatus('saving');
    const response = await appFetch('/settings', {
      method: 'PUT',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ digest: 'weekly' }),
    });
    if (!response.ok) {
      setStatus('error');
      return;
    }
    const saved = await response.json();
    setStatus(saved.digest === 'weekly' ? 'saved' : 'error');
  }

  return <button type="button" onClick={save}>{status}</button>;
}
```

The returned value is a standard `Response`; JSON parsing and domain errors remain app logic. Absolute cross-origin URLs and paths escaping the scoped base path are rejected. Do not add or persist an authorization header yourself.

| Case | Behavior |
| --- | --- |
| Normal response | Returned unchanged, including non-2xx status codes. |
| Expired app-data credential | Core refreshes the descriptor and retries once in the same environment. |
| Surface unmount | The request signal is aborted. |
| One-shot stream body | Core tees it before a possible credential retry. |

### useLocale

`useLocale()` returns `{ locale, t }` for the current mount. `locale` is the locale actually loaded for this app surface. `t(key, values)` reads the app's declared locale messages and interpolates named values; missing app keys fall back to the shell translator.

TSX

```
import { useLocale } from '@sota/platform';

export function Greeting({ name }: { name: string }) {
  const { locale, t } = useLocale();
  return <p lang={locale}>{t('greeting', { name })}</p>;
}
```

YAML

```yaml
locales:
  default: en
  files:
    en: src/locales/en.json
    vi: src/locales/vi.json
```

`src/locales/en.json`

JSON

```json
{ "greeting": "Hello, {{name}}" }
```

`src/locales/vi.json`

JSON

```json
{ "greeting": "Xin chào, {{name}}" }
```

The selected locale falls back according to the app locale contract; a missing app key is then offered to the shell translator. A key missing from both translators is not business data, so provide a sensible component fallback where the label is essential. Keep business data out of translations and provide accessible fallback text for icons and images.

### useTheme

`useTheme()` returns `'light'` or `'dark'` and re-renders with the host theme. It is read-only: apps adapt to the workspace theme; they do not mutate it.

TSX

```
import { useTheme } from '@sota/platform';

export function Preview() {
  const theme = useTheme();
  return <div className="preview" data-theme={theme}>Preview</div>;
}
```

Code

```css
.preview {
  color: var(--foreground);
  background: var(--background);
}

.preview[data-theme='dark'] .diagram {
  filter: brightness(0.9);
}
```

Prefer the shared design tokens inherited by native surfaces. Use the hook only when behavior or non-CSS assets genuinely differ by theme.

### Platform UI components

`@sota/platform/ui` is the supported component library for native apps. These components inherit Core tokens, theme, focus behavior, portals, and accessibility defaults. Import from this entry point instead of Core source paths or a second copy of Radix/Recharts.

| Group | Exports |
| --- | --- |
| Actions and status | `Button`, `Badge`, `Spinner`, `Skeleton`, `buttonVariants`, `badgeVariants`. |
| Content and layout | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `Separator`, `ScrollArea`, `ScrollBar`. |
| Forms | `FormField`, `Label`, `Input`, `Textarea`, `Checkbox`, `Switch`, `Select`, `SelectContent`, `SelectGroup`, `SelectItem`, `SelectLabel`, `SelectSeparator`, `SelectTrigger`, `SelectValue`. |
| Overlays | `Dialog`, `DialogClose`, `DialogContent`, `DialogDescription`, `DialogFooter`, `DialogHeader`, `DialogTitle`, `DialogTrigger`, `Popover`, `PopoverContent`, `PopoverTrigger`, `Tooltip`, `TooltipContent`, `TooltipProvider`, `TooltipTrigger`. Their portals stay inside the app surface host. |
| Navigation | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`. |
| Grounded prose | `CitationText` maps inline `[n]` markers to source pills. |
| Charts | `ChartContainer`, `ChartStyle`, `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, `Area`, `AreaChart`, `Bar`, `BarChart`, `Line`, `LineChart`, `Pie`, `PieChart`, `RadialBar`, `RadialBarChart`, `CartesianGrid`, `XAxis`, `YAxis`, `ChartLabel`, `ChartLabelList`, `Cell`, `Sector`, `ReferenceLine`. |

### Form and card

TSX

```
import { useState } from 'react';
import {
  Button, Card, CardContent, CardFooter, CardHeader, CardTitle,
  FormField, Input, Switch,
} from '@sota/platform/ui';

export function DigestSettings() {
  const [email, setEmail] = useState('');
  const [enabled, setEnabled] = useState(true);
  return (
    <form onSubmit={(event) => event.preventDefault()}><Card variant="flat">
      <CardHeader><CardTitle>Weekly digest</CardTitle></CardHeader>
      <CardContent>
        <FormField id="digest-email" label="Delivery email" required>
          <Input id="digest-email" type="email" value={email}
            onChange={(event) => setEmail(event.target.value)} required />
        </FormField>
        <Switch checked={enabled} onCheckedChange={setEnabled}
          aria-label="Enable weekly digest" />
      </CardContent>
      <CardFooter><Button type="submit" disabled={!email}>Save</Button></CardFooter>
    </Card></form>
  );
}
```

`Button` supports `default`, `secondary`, `destructive`, `outline`, `link`, and `ghost` variants; sizes include `default`, `sm`, `lg`, `icon`, and `icon-sm`. Use `loading` for pending actions. `CardTitle` intentionally defaults to a compact heading.

### Dialog

TSX

```
import {
  Button, Dialog, DialogClose, DialogContent, DialogDescription,
  DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
} from '@sota/platform/ui';

export function DeleteDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild><Button variant="destructive">Delete</Button></DialogTrigger>
      <DialogContent preset="confirm" size="sm">
        <DialogHeader>
          <DialogTitle>Delete report?</DialogTitle>
          <DialogDescription>This action cannot be undone.</DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild><Button variant="outline">Cancel</Button></DialogClose>
          <Button variant="destructive">Delete</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
```

Dialog presets are `modal`, `editor`, `confirm`, and `command`; sizes are `sm` through `xl`. Keep a `DialogTitle` and `DialogDescription`. Set `dismissible={false}` only while closing would corrupt an in-flight critical action.

### Citations

TSX

```
import { CitationText } from '@sota/platform/ui';

export function AnswerWithSources() {
  return <CitationText
    text="Revenue grew by 12% [1]."
    citations={[{
      index: 1,
      url: 'https://example.com/q3',
      title: 'Q3 filing',
      snippet: 'Revenue increased twelve percent year over year.',
    }]}
  />;
}
```

### Chart

TSX

```
import {
  Bar, BarChart, CartesianGrid, ChartContainer, ChartTooltip,
  ChartTooltipContent, XAxis, YAxis, type ChartConfig,
} from '@sota/platform/ui';

const chartConfig = {
  credits: { label: 'Credits', color: 'var(--chart-1)' },
} satisfies ChartConfig;
const data = [{ month: 'Jul', credits: 24 }, { month: 'Aug', credits: 31 }];

export function CreditChart() {
  return (
    <ChartContainer config={chartConfig} className="min-h-48 w-full">
      <BarChart data={data}>
        <CartesianGrid vertical={false} />
        <XAxis dataKey="month" /><YAxis />
        <ChartTooltip content={<ChartTooltipContent />} />
        <Bar dataKey="credits" fill="var(--color-credits)" />
      </BarChart>
    </ChartContainer>
  );
}
```

Import only the primitives a surface uses. The generated declarations are the exact prop reference for the installed Core version.

### Platform icons

`@sota/platform/icons` exposes the stable icon names bundled for native apps: `alert`, `check`, `chevronDown`, `chevronLeft`, `chevronRight`, `close`, `info`, `loading`, `search`, `settings`, and `warning`.

TSX

```
import { Icon, PLATFORM_ICON_NAMES, type IconName } from '@sota/platform/icons';

export function StatusIcon({ name, label }: { name: IconName; label: string }) {
  return (
    <span>
      <Icon name={name} size={16} strokeWidth={2} aria-hidden />
      <span>{label}</span>
    </span>
  );
}

console.log(PLATFORM_ICON_NAMES);
```

`Icon` also accepts normal SVG attributes. Use `aria-hidden` when adjacent text supplies the name; otherwise pass an accessible `aria-label`. The icon-name union is intentionally closed, so TypeScript catches unavailable names before deploy.

### Surface lifecycle

`useAppContext().lifecycle` owns resources for one surface mount. Cleanup runs when the mount unmounts or its exact runtime identity changes.

| API | Use |
| --- | --- |
| `signal` | An `AbortSignal` aborted on unmount. |
| `onDispose(dispose)` | Registers app cleanup and returns a function that unregisters it. |
| `listen(target, type, listener, options?)` | Adds an event listener owned by this mount and returns its disposer. |

TSX

```
import { useEffect, useState } from 'react';
import { useAppContext } from '@sota/platform';

export function OnlineState() {
  const { lifecycle } = useAppContext();
  const [online, setOnline] = useState(navigator.onLine);
  useEffect(() => {
    const update = () => setOnline(navigator.onLine);
    const stopOnline = lifecycle.listen(window, 'online', update);
    const stopOffline = lifecycle.listen(window, 'offline', update);
    return () => {
      stopOnline();
      stopOffline();
    };
  }, [lifecycle]);
  return <p>{online ? 'Online' : 'Offline'}</p>;
}
```

TypeScript

```
import { useEffect } from 'react';
import { useAppContext } from '@sota/platform';

export function MountTimer() {
  const { lifecycle } = useAppContext();
  useEffect(() => {
    const timer = window.setInterval(() => console.log('tick'), 30_000);
    const unregister = lifecycle.onDispose(() => window.clearInterval(timer));
    return () => {
      window.clearInterval(timer);
      unregister();
    };
  }, [lifecycle]);
  return null;
}
```

Use these primitives instead of process-global listeners or timers. React cleanup may call returned disposers early; Core still guarantees forced cleanup when the surface disappears.

### Platform UI bridge

`useAppContext().ui` lets app-owned UI ask the host to perform host-owned presentation. It contains primitives, not business workflows.

| API | Effect |
| --- | --- |
| `toast(message, options?)` | Shows an info, success, warning, or error notice. |
| `confirm(options)` | Returns a promise for a host confirmation decision. |
| `navigate(to)` | Navigates through the host router. |
| `openArtifact(kind, context?)` | Opens an artifact in the same app lane using its current renderer. |
| `closeArtifact()` | Closes the active artifact host when one exists. |
| `openImageLightbox(images, startIndex?)` | Opens the shared image viewer with optional captions and source attribution. |

TSX

```
import { useAppContext, useAppFetch } from '@sota/platform';

export function DeleteReportButton({ reportId }: { reportId: string }) {
  const appFetch = useAppFetch();
  const { ui } = useAppContext();

  async function remove() {
    const confirmed = await ui.confirm({
      title: 'Delete report?',
      description: 'This cannot be undone.',
      confirmLabel: 'Delete',
      destructive: true,
    });
    if (!confirmed) return;
    const response = await appFetch('/reports/' + encodeURIComponent(reportId), {
      method: 'DELETE',
    });
    ui.toast(response.ok ? 'Report deleted' : 'Delete failed', {
      kind: response.ok ? 'success' : 'error',
    });
  }

  return <button type="button" onClick={remove}>Delete</button>;
}
```

TSX

```
import { useAppContext } from '@sota/platform';

export function PreviewButton({ previewUrl }: { previewUrl: string }) {
  const { ui } = useAppContext();
  return <button type="button" onClick={() => ui.openImageLightbox([{
    url: previewUrl,
    alt: 'Report preview',
    caption: 'Page 1',
    sourceTitle: 'Quarterly report',
    sourceUrl: 'https://reports.example.com/q3',
  }], 0)}>Preview</button>;
}
```

Methods may be harmless no-ops when the current host cannot perform the action, such as closing an artifact from a surface without an artifact host. Keep app state authoritative in the app backend.

### useComposer

`useComposer(selector)` is available only inside a hosted `composer-panel` and throws elsewhere. Import it from `@sota/core/hooks`. Select only the field a component needs so unrelated draft changes do not force work.

| Field | Contract |
| --- | --- |
| `value` | Plain-text view of the current draft. |
| `content` | Structured text and `app-reference` nodes. |
| `references` | Resolved slash, conversation, and lane-scoped app references. |
| `pendingToolResults` | Deferred tool results belonging to this panel's exact app execution. |
| `applyEdit(edit)` | One undoable editor transaction in `replace`, `insert-at-cursor`, or `append` mode. |
| `focus()` | Moves focus to the composer editor. |
| `acquireLock()` | Disables send, draft edits, existing-message edits, and queued auto-send while held; returns an idempotent release function. |

TSX

```
import { useEffect } from 'react';
import { useComposer } from '@sota/core/hooks';

export function PendingActionPanel() {
  const pending = useComposer((composer) => composer.pendingToolResults);
  const acquireLock = useComposer((composer) => composer.acquireLock);
  const active = pending.find((item) => item.toolName === 'readCalendar');

  useEffect(() => {
    if (!active) return;
    return acquireLock();
  }, [acquireLock, active]);

  if (!active) return null;
  return <p>Complete the app action to continue.</p>;
}
```

Locks are reference-counted: multiple panels or pending operations can hold them independently. Core auto-releases a lock when its surface unmounts, but app code should still return the release function from its effect. Preserve structured `content`; rebuilding from `value` flattens references.

TSX

```
import { useComposer } from '@sota/core/hooks';

export function InsertReportButton() {
  const applyEdit = useComposer((composer) => composer.applyEdit);
  return <button type="button" onClick={() => applyEdit({
    mode: 'insert-at-cursor',
    content: [
      { kind: 'text', text: 'Review ' },
      {
        kind: 'app-reference',
        referenceType: 'report',
        referenceId: 'report-42',
        label: 'Q3 report',
        fallbackText: 'Q3 report',
      },
    ],
  })}>Insert report</button>;
}
```

`pendingToolResults` items contain `operationId`, `toolCallId`, canonical `toolName`, collision-resolved `modelToolName`, optional opaque `data`, and exact `execution`. Narrow app-owned `data` at runtime before rendering it. The `pendingToolResults`/`acquireLock` fields require platform contract `1.3.0` or newer.

### Deferred tool results

A tool backend may defer the result of the current tool call without creating a user message or asking the model to call it again. Core understands only an operation id and opaque app data; login, approval, payment, device pairing, or any other workflow remains app logic.

JSON

```json
{
  "_sota": {
    "deferredToolResult": {
      "operationId": "app-defined-globally-unique-id",
      "data": { "anyAppOwnedValue": true }
    }
  }
}
```

Core registers the exact run, tool call, tool name, app installation, and environment behind that id. It streams `output-pending` to matching tool surfaces and adds the same item to the exact composer panel's `pendingToolResults`. The model has not received a tool result yet.

YAML

```yaml
coreToolGrants:
  - tool: core.app-operations.complete
    scope: write
  # Only when a longer-lived job callback token is needed:
  - tool: core.tokens.issueJobCallback
    scope: write
```

Send the callback from the app backend to the Core origin. The bearer value is the delegated capability received in the original invocation's `x-sota-core-token` header.

Code

```
POST /v1/app-operations/app-defined-globally-unique-id/complete
Authorization: Bearer <delegated-token>
Content-Type: application/json

{
  "status": "completed",
  "result": { "events": [] }
}
```

Use `status: "failed"` with an arbitrary `error` to reject it. The completion atomically becomes the result of the original tool call, and the same agent run continues. Parallel tool calls each have their own operation id; the model step continues only after all calls in that step settle.

JSON

```json
{
  "status": "failed",
  "error": {
    "code": "ACTION_NOT_COMPLETED",
    "message": "The requested action was not completed"
  }
}
```

- Grant `core.app-operations.complete:write` to the app backend.
- Operation ids are trimmed, 1–200 characters, and globally unique per tool call. Identical completion retries are idempotent.
- Registration occurs after Core receives the deferred response, so retry `app_operation_not_found` with bounded backoff.
- If completion can outlive the 60-second delegation token, exchange it for a job callback token while the original invocation is still valid, then store that token safely and send its job id in `x-sota-job-id`.
- Keep `data` small and free of secrets: it is opaque to Core but deliberately client-visible. Narrow its shape in app UI before use.
- The current primitive stays attached to the live run. Stopping or steering the run fails a still-pending operation with `operation_cancelled`. App-owned expiry should complete the operation as failed; this is not a background job queue.

### UI contract helpers

The generated `@sota/platform` package also exposes build-time contract metadata and one typed descriptor helper. Manifest v3 remains the registration authority.

| Export | Contract |
| --- | --- |
| `PLATFORM_API_VERSION` | The public API version compiled into the runtime. |
| `getAppUiContractHash()` | Returns the exact installed UI declaration hash; it throws outside an installed app runtime. |
| `assertCoreCompatibility(hash)` | Source-compatible no-op retained for early native apps. Exact hashes identify artifacts and are not runtime compatibility gates. |
| `defineAppExtension(descriptor)` | Validates non-empty `id`/`appId` and returns a frozen typed copy. It does not register a contribution omitted from the manifest. |

### Type-only exports

| Group | Types |
| --- | --- |
| Runtime context | `AppPlatformContextValue`, `AppLocale`, `AppTheme`, `AppSurfaceKind`, `AppLightboxImage`. |
| Tool UI | `ToolResultSurfaceProps`, `ToolResultSurfaceValue`, `ToolResultSurfaceState`, `ToolResultSurfaceCommon`, `ToolResultSurfaceExecution`. |
| Descriptors | `AppExtensionDescriptor`, `AppSurfaceSlot`, `NativeModuleSurfaceDescriptor`, `ToolResultSurfaceDescriptor`, `MessageDecorationDescriptor`, `ArtifactSurfaceDescriptor`, `ArtifactSurfaceRenderInput`. |

TypeScript

```
import {
  PLATFORM_API_VERSION,
  defineAppExtension,
  getAppUiContractHash,
} from '@sota/platform';

const descriptor = defineAppExtension({ id: 'reports', appId: 'reports-app' });
console.log(PLATFORM_API_VERSION, getAppUiContractHash(), descriptor.id);
```

TypeScript

```
import type {
  AppPlatformContextValue,
  ToolResultSurfaceProps,
  ToolResultSurfaceState,
} from '@sota/platform';

export type RuntimeIdentity = Pick<AppPlatformContextValue, 'appId' | 'appVersion'>;
export type CalendarSurface = ToolResultSurfaceProps<
  { from: string; to: string },
  { events: unknown[] }
>;
export const terminalStates: readonly ToolResultSurfaceState[] = [
  'output-available', 'output-error', 'output-denied',
];
```

Regenerate `.sota/app-ui-contracts/` with the current CLI instead of copying declaration files between projects or hand-editing them.

> [!WARNING]
> The generated declaration bundle is not an API catalog.
>
> Apps may use the documented `@sota/platform` exports and `useComposer`. Other host declarations that happen to appear under `@sota/core/hooks` are internal unless they receive their own developer API page.

### Local development

Local development uses two independent processes in two terminals. **You** own the app's watchers; **the CLI** owns the Development session and the tunnel that reaches your machine. Sota CLI never starts, restarts, or kills your processes — including when the session stops.

Terminal

```
# terminal 1 — your app's watchers
npm run dev

# terminal 2 — the Development session and tunnel
npm run dev:sota   # equivalent to: sota dev
```

1. #### Start your own watchers

   Run `npm run dev`. In a project scaffolded with both a backend and native UI this one script already runs **both**: `concurrently` starts the backend watcher and the UI watch build side by side, labelled `backend` and `ui`, with `--kill-others` so one crash stops the pair instead of leaving half a stack running.

2. #### Open Development

   Run `sota dev`, select an organization and workspace, and keep the process running. The CLI prepares everything locally first — it compiles the manifest, verifies the built frontend output, and opens the tunnel transport — and only then publishes the session in one commit. A failure during preparation creates nothing on the server.

3. #### Iterate safely

   The CLI watches every file the compiled manifest was built from — the manifest itself, JSON Schemas, locale files, native assets, and inlined skill bodies — and re-syncs your personal session on change. A compile error prints diagnostics and keeps the last known-good manifest instead of replacing it. A heartbeat every 15 seconds extends the session lease.

4. #### Stop cleanly

   Run `sota dev stop`, or press Ctrl-C in the `sota dev` terminal. The Development app disappears and its Core-managed data cleanup is scheduled. Your own frontend and backend processes are left running — the CLI says so explicitly when it exits.

### Running the UI and the backend separately

`npm run dev` is a convenience wrapper over two scripts that also exist on their own. Run them individually whenever you want to restart one half without disturbing the other, attach a debugger to just one, or — most importantly — when your backend is not a Node.js process at all.

| Script | What it actually runs | Notes |
| --- | --- | --- |
| `npm run dev` | `concurrently --kill-others --names backend,ui "tsx watch src/backend/server.ts" "sota contracts ensure && vite build --watch"` | Both halves at once. In a backend-only or UI-only project it collapses to just that half. |
| `npm run dev:backend` | `tsx watch src/backend/server.ts` | The app's HTTP service, restarted on source change. Listens on `PORT`, default `8787`. |
| `npm run dev:ui` | `sota contracts ensure && vite build --watch` | A _watch build_, not a dev server. It refreshes the App UI type contract, then rebuilds `dist/ui/app.js` and `dist/ui/app.css` on every change. |
| `npm run dev:sota` | `sota dev` | The Development session, manifest sync, and tunnel. Independent of the two above. |

> [!NOTE]
> There is no local UI server, and that is deliberate.
>
> Native UI runs inside the SotaAgents host, not on `localhost`. `dev:ui` only has to keep the built module on disk current; the tunnel serves those exact bytes to the platform, which is why the same files work unchanged once they are packed into a deployed artifact.

### Bringing your own backend runtime

The scaffold is TypeScript and Express because that is widely understood, not because the platform requires it. The contract between SotaAgents and your backend is plain HTTP plus a verified token, so a Go, Python, Java, or Rust service is a first-class app backend. One of the SotaAgents system apps is a FastAPI service that verifies the very same invocation token in Python against the same Core JWKS endpoint.

In that setup you simply do not use the Node backend scripts. Start your service however your stack starts it, keep `npm run dev:ui` for the native UI if the app has one, and point the local overlay at whatever port your service listens on:

Terminal

```
# terminal 1 — your backend, in your language
uvicorn app.main:app --reload --port 8787

# terminal 2 — native UI watch build (only if the app contributes UI)
npm run dev:ui

# terminal 3 — the Development session
sota dev
```

`sota dev` resolves the Local Backend Endpoint in this order: the `--local-url` flag, then `environments.local.service.baseUrl` from the manifest, then the endpoint you last used (remembered in `.sota/dev.json`). In an interactive terminal it prompts if none of those is set; otherwise it fails with `LOCAL_BACKEND_UNAVAILABLE`. The CLI then polls that endpoint about once a second and prints `[backend] available` or `[backend] unavailable`. That check is advisory only — it never gates publishing the session, so a backend that is temporarily down does not tear down your Development app.

> [!WARNING]
> A live session is personal.
>
> It belongs to one developer and one selected workspace. Running `sota dev` again replaces only your own previous session; it is not a shared Staging environment. Staging and Production never depend on a tunnel — they call your hosted backend directly.

### Validate & test

Terminal

```
sota validate
npm run typecheck
npm run build
sota deploy --dry-run
```

- Exercise every declared tool with valid, invalid, unauthorized, and timeout inputs.
- Open every native UI slot and tool-result renderer in light and dark themes.
- Verify all entry chunks, CSS, locale files, and AppData calls load through the platform.
- Test owner, contributor, workspace admin, member, and non-member expectations.
- Check Development, Staging, and Production bindings do not share secrets or data unintentionally.

### Deploy Staging

The browser cannot safely or reproducibly load source files from a developer laptop. Deployment turns the declared app surface into immutable, content-addressed bytes that every screen, tool-result renderer, locale loader, and artifact consumer can resolve in the same way.

![Diagram of source files becoming an immutable SotaAgents artifact](/manual/assets/developer/bundle-pipeline.svg?v=2)
_The uploaded artifact contains package assets and the resolved definition. Your live backend, database contents, secrets, and operational data stay outside it._

Terminal

```
sota validate
sota deploy --dry-run
sota deploy --description "Add grounded search results"
sota status
```

Deploy compiles and packs deterministic bytes, uploads them, and asks Core to validate the authoritative definition. The shared Staging pointer moves to that artifact; Production remains unchanged.

> [!WARNING]
> Versions are immutable.
>
> Deploying different bytes with an already-used version fails with `VERSION_IMMUTABLE_CONFLICT`. Increment `version`; do not overwrite history. If a change is intentionally breaking, use the explicit acknowledgement and migration declaration required by the CLI.

To test Staging, install and enable the exact Staging environment in the target organization/workspace. The tester must also be the app owner or a contributor and an active workspace member.

### Release Production

Terminal

```
sota release
sota status
```

Release promotes the **exact current Staging artifact** to Production. It does not rebuild, copy Staging data, or wait for release approval. The first Production release starts **Private**; later releases preserve the current visibility and App Store eligibility.

- Confirm the Staging artifact ID and version are the ones you tested.
- Verify Production backend, health URL, secrets, CORS, and data stores.
- Release, then smoke-test installation, workspace enablement, tools, native UI, and audit events.
- Keep a previous version available; release a new version to roll forward.

### Publish to App Store

A Production release and an App Store listing are separate decisions. Production can remain Private or be restricted to selected organizations without appearing in the public catalog.

| Visibility | Discovery and installation |
| --- | --- |
| Private | Management-only. Absent from install catalogs; nobody can install it. |
| Restricted | Available to up to 50 explicitly selected organizations. |
| Public / App Store | Discoverable in the public catalog after initial eligibility approval by the SotaAgents operations team. |

1. Release a tested Production artifact.
2. Open _App Registry → My apps → app detail → Settings_.
3. Choose public App Store visibility and submit the listing request.
4. The SotaAgents operations team reviews discovery eligibility. Approval changes listing eligibility only; it does not release or replace Production.
5. After approval, the owner may delist and relist without a new request unless eligibility is revoked.

### Best practices

### Design narrow capabilities

Give tools one clear job, strict schemas, bounded timeouts, and safe failure modes. Avoid destructive actions unless essential.

### Trust platform context

Verify signed Sota requests. Never accept workspace, actor, app, or environment identity from arbitrary client input.

### Keep UI portable

Use generated UI contracts and platform loaders. Do not construct asset URLs, tokens, navigation, or environment fallbacks yourself.

### Observe exact versions

Log artifact/version, environment, request ID, and tool name without leaking secrets. Health endpoints should be cheap and deterministic.

### Separate environments

Use different credentials and data stores where isolation matters. Staging traffic uses real organization credit and audit context.

### Release forward

Never mutate a deployed version. Increment, validate, deploy to Staging, test the exact artifact, then release it.

### Developer troubleshooting

| Symptom | Check |
| --- | --- |
| Native app entry returns 401/403 | Do not call package assets directly. Confirm the UI is mounted through the platform and the exact environment is installed, enabled, and authorized. |
| Native app entry returns 404 | Confirm the artifact contains the declared entry/chunk/CSS/locale path and the selected environment points to that exact artifact. |
| AppData or tools are unauthorized | Verify actor membership, owner/contributor rules for Staging, workspace enablement, signed request validation, and environment-scoped credentials. |
| Manifest change is ignored | Use only `local`, `stg`, or `prod` overlays; run `sota validate`; increment the version before deploying changed bytes. |
| Staging works but Production fails | Compare environment bindings, secrets, health, CORS, external data stores, and installation/visibility—not only source code. |

Useful commands: `sota status`, `sota logs`, `sota manifest diff <left> <right>`, and `sota docs`.

## Enterprise integration

Run the assistant inside the systems your company already uses — an intranet, a customer portal, a line-of-business app — behind your own sign-in.

### Embedding in your systems

SotaAgents can run inside a page your company already owns, as a floating bubble or an inline panel. Your users stay signed in to _your_ system and never see a SotaAgents login screen: your backend vouches for who they are, and the platform trusts that signature rather than anything the user typed.

[Fifteen-second film: signing a ticket, mounting the panel, and the assistant running inside a partner portal](/manual/assets/video/enterprise-embed-1080p.mp4)
_Fifteen seconds, no sound. Your backend signs a request and receives a ticket; your frontend mounts the panel with one call; the assistant appears inside the partner's own portal — inline, or as a floating bubble._

### Two ways to embed

| Mode | Who the visitor is | Use it for | Set up by |
| --- | --- | --- | --- |
| Public Deployment (_Configure Embed_) | Anonymous guest, drawing on a separate Guest Credit pool | Public website, marketing page, pre-sales chat | Org owner or admin — no code |
| Embed SDK (signed handshake) | A named person who is already a member of your organization | Intranet, customer portal, internal tools | Org owner or admin, plus your development team |

This page covers the second one. For the first, see [Workspaces](/manual/admin-console/workspaces).

### Before you start

- An **API key and secret** from _Console → your organization → API Keys_. The secret is shown once — see [API keys](/manual/admin-console/api-keys).
- Every person who will use the embed must already be an **active member** of the organization. There is no just-in-time account creation: an unknown user is refused, not created. [Invite them](/manual/admin-console/inviting-people) first, or provision them through SSO.
- A backend you control. The secret signs requests server-side and must never reach a browser.

### How the handshake works

![Sequence diagram: the enterprise frontend asks the enterprise backend for a ticket, the backend signs a request to the SotaAgents embed API, the ticket returns and is exchanged inside the iframe for a session cookie, and sign-out revokes that session](/manual/assets/developer/embed-flow.svg?v=2)
_The signed handshake between three parties. The API secret never leaves the middle column; the browser holds only a 60-second ticket, then a partitioned session cookie._

1. #### Your page asks your backend for a ticket

   The SDK never talks to your identity system. It calls a `getTicket` function you supply, which calls your own endpoint with your own session cookie.

2. #### Your backend signs and mints

   It reads the user identity from its session, signs the request with the API secret, and receives a single-use ticket valid for 60 seconds.

3. #### The iframe exchanges the ticket

   The SDK hands the ticket to the SotaAgents frame, which exchanges it for its own session. The ticket cannot be reused.

4. #### The SDK re-tickets on its own

   Near expiry, and after any 401, it repeats step 1 without being asked. Your session is the only lifetime that matters — there is nothing to keep in sync.

### What your backend does

Three duties, and the first two are where integrations go wrong.

1. #### Identify the user server-side

   Whatever your portal already does — SSO, OIDC, its own session table. Send an `externalUserId` that is **stable for that person forever**: the mapping from it to a SotaAgents user is created once and never updated. Treat that as _your_ invariant, not ours — we do not enforce it. A new id for someone we already know is **not** rejected: we match the person by email and silently add a second mapping, leaving the old one behind where identity revocation will not find it. What _is_ refused is the reverse — an existing id arriving with a different email.

2. #### Expose a ticket endpoint

   Read the identity from the **session**, never from the request body — reading it from the body would let any signed-in employee mint a ticket impersonating a colleague. Sign the call as described below and return the ticket to the browser.

3. #### Answer with a status the SDK can act on

   The SDK treats **4xx as permanent** and stops; **5xx as retryable** and backs off. Answer `401` when your own session has expired, `409` when the user has no active membership, and `502` when SotaAgents is unreachable.

> [!WARNING]
> The secret stays on your server
>
> The browser only ever receives short-lived, single-use tickets. An API secret shipped to the front end can mint a ticket for any identity in your organization. Store it in your secret manager, never in source control.

### Signing the request

The recipe is the same in every language: build a five-line canonical string, HMAC-SHA256 it with your API secret, and send the result as `v1=<lowercase hex>`. The five lines are the method, the path, a Unix timestamp in **seconds**, a nonce, and the SHA-256 of the request body — joined by `\n`, with no trailing newline.

Code

```
POST
/api/embed/auth/tickets
1786800000
3f1c9a7e-5b02-4d6f-9a11-0c8e2d4b7f30
9b2a4c1d8e3f5a70b6c9d2e4f8a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d9e1f3a5
```

> [!WARNING]
> Hash the bytes you actually send
>
> Serialize the body once and reuse that exact string for both the hash and the request. Re-serializing the object for the second use is the single most common cause of a signature that verifies nowhere — two JSON encoders, or the same one called twice, can order keys or spacing differently.

#### Node.js

```javascript
import { createHash, createHmac, randomUUID } from 'node:crypto';

const body = JSON.stringify({ externalUserId, email, name }); // serialize ONCE
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomUUID();
const bodyHash = createHash('sha256').update(body, 'utf8').digest('hex');

const canonical = ['POST', PATH, timestamp, nonce, bodyHash].join('\n');
const signature = 'v1=' + createHmac('sha256', apiSecret).update(canonical).digest('hex');
```

#### Python

```python
import hashlib, hmac, json, time, uuid

body = json.dumps({"externalUserId": external_user_id, "email": email})  # serialize ONCE
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
body_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()

canonical = "\n".join(["POST", PATH, timestamp, nonce, body_hash])
signature = "v1=" + hmac.new(
    api_secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256
).hexdigest()
```

#### Java

```java
String body = objectMapper.writeValueAsString(payload); // serialize ONCE
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String nonce = UUID.randomUUID().toString();
String bodyHash = HexFormat.of().formatHex(
    MessageDigest.getInstance("SHA-256").digest(body.getBytes(StandardCharsets.UTF_8)));

String canonical = String.join("\n", "POST", PATH, timestamp, nonce, bodyHash);

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = "v1=" + HexFormat.of().formatHex(
    mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));
```

#### C#

```csharp
var body = JsonSerializer.Serialize(payload); // serialize ONCE
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var nonce = Guid.NewGuid().ToString();
var bodyHash = Convert.ToHexString(
    SHA256.HashData(Encoding.UTF8.GetBytes(body))).ToLowerInvariant();

var canonical = string.Join("\n", "POST", Path, timestamp, nonce, bodyHash);

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret));
var signature = "v1=" + Convert.ToHexString(
    hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical))).ToLowerInvariant();
```

#### Go

```go
body, err := json.Marshal(payload) // serialize ONCE
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonce := uuid.NewString()
sum := sha256.Sum256(body)
bodyHash := hex.EncodeToString(sum[:])

canonical := strings.Join([]string{"POST", path, timestamp, nonce, bodyHash}, "\n")

mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(canonical))
signature := "v1=" + hex.EncodeToString(mac.Sum(nil))
```

#### PHP

```php
$body = json_encode($payload); // serialize ONCE
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$bodyHash = hash('sha256', $body);

$canonical = implode("\n", ['POST', $path, $timestamp, $nonce, $bodyHash]);
$signature = 'v1=' . hash_hmac('sha256', $canonical, $apiSecret);
```

### The request on the wire

Whatever language produced the signature, this is what SotaAgents receives. The timestamp and nonce headers must carry the same values that went into the canonical string; a nonce is accepted once and replays are rejected for five minutes.

HTTP

```
POST https://app.sotaagents.ai/api/embed/auth/tickets
content-type: application/json
x-sota-key: sota_ek_...
x-sota-timestamp: 1786800000
x-sota-nonce: 3f1c9a7e-5b02-4d6f-9a11-0c8e2d4b7f30
x-sota-signature: v1=4d5e...

{"externalUserId":"e-1042","email":"an.tran@company.com"}
```

| Header | Value |
| --- | --- |
| `x-sota-key` | Your API key (`sota_ek_…`) — not the secret |
| `x-sota-timestamp` | Unix seconds, the same value you signed. Requests more than five minutes old are rejected |
| `x-sota-nonce` | Unique per request; a repeat is treated as a replay |
| `x-sota-signature` | `v1=` followed by the lowercase hex HMAC |

SotaAgents answers `201` with the core envelope — `{ success: true, data: { ticket, expiresAt }, timestamp }`. **Unwrap it:** return the inner `data` object to the browser, because the SDK reads `ticket` from the top level and treats an envelope as an invalid ticket — a failure that looks like `200` on the wire and a panel that never fills in. Error responses are _not_ wrapped; they arrive flat as `{ statusCode, code, message }`, so read `code` from the top level. The ticket is valid for 60 seconds and can be exchanged once.

**Rate limits.** Minting is capped at **60 requests per minute per API key** and **120 per minute per source IP**; the ticket exchange inside the iframe is capped at **10 per minute**, counted against the end user's IP as well as the ticket. Over the limit you get `429` with `Retry-After` — pass it through to the SDK, which backs off and retries, rather than folding it into a generic 4xx. Your whole backend shares one source IP and everyone behind a single office NAT shares the exchange budget, so size your peak sign-in burst against these numbers and talk to us before go-live if it does not fit.

### What your frontend does

Load `<script src="https://app.sotaagents.ai/embed/sdk.js"></script>`, then mount it. Two things matter: the ticket relay must send your session cookie, and sign-out must revoke the SotaAgents session first.

The origin in these samples is an example. Your SotaAgents contact gives you the embed origin for your tenant, and production and staging differ — keep it, the SDK script URL and your ticket API base in configuration rather than in code. The SDK rejects any `embedUrl` that is not HTTPS.

TypeScript

```
const embed = window.SotaEmbedSDK.createSotaEmbed({
  embedUrl: 'https://app.sotaagents.ai/embed',
  mode: 'bubble',
  getTicket: async () => {
    const response = await fetch('/api/sota/embed-ticket', {
      method: 'POST',
      credentials: 'include',
    });
    if (!response.ok) {
      // Status rides along so the SDK can fast-fail a 401 or 409 instead of
      // retrying a permanent failure four times.
      throw Object.assign(new Error('ticket_failed'), { status: response.status });
    }
    return response.json(); // { ticket, expiresAt }
  },
});

// On sign-out: revoke the SotaAgents session BEFORE clearing your own.
const revoked = await embed.logout();
if (!revoked) console.warn('SotaAgents session may still be active');
await fetch('/api/logout', { method: 'POST', credentials: 'include' });
```

> [!WARNING]
> Revoke before you sign the user out
>
> The SotaAgents session lives on the embed origin, so clearing your own session does not touch it. Skip `logout()` and the next person to sign in on that browser inherits a live session — landing in the conversations of whoever used it before. Call it first, then clear your own. It resolves `false` when the revoke could not be confirmed — sign the user out of your own portal anyway, but on a shared or kiosk machine tell them the SotaAgents session may still be live.

### Making it look like yours

Everything here is optional and every default is the stock SotaAgents one, so an integration that sets none of it looks exactly as it does today. Set them and the chat carries your name instead of ours.

Code

```
createSotaEmbed({
  embedUrl: 'https://app.sotaagents.ai/embed',
  getTicket,

  // Inside the iframe
  language: 'vi',                  // 'en' | 'vi' | 'ja'
  branding: {
    logoUrl: 'https://intranet.acme.com/logo.svg',
    showUser: false,
    greeting: 'How can the Acme assistant help?',
    placeholder: 'Ask about policies, invoices, anything',
  },

  // The bubble chrome, drawn on your own page
  mode: 'bubble',
  title: 'Acme Assistant',
  primaryColor: '#0b5cff',
  position: 'bottom-left',
  width: 420,
  height: 640,
  openOnLoad: false,
});
```

**Two mount shapes.** `mode` takes exactly two values. `'bubble'` is the default: the SDK draws its own floating launcher and panel on your page, and `container` is ignored. `'inline'` puts the iframe straight into the element you name — no launcher, no panel — and `container` is required there, the call throwing without it. One difference worth planning around: bubble builds the iframe lazily, on the first click, so nothing authenticates until someone opens the chat; inline builds it on load, so a broken ticket endpoint shows up immediately and every page view spends a ticket.

**Inside the chat.** These ride on the iframe URL rather than arriving after sign-in, so the first frame the user sees is already yours — nothing flashes the SotaAgents logo and then swaps it.

| Option | Default | What it does |
| --- | --- | --- |
| `language` | Follows the browser | `en`, `vi` or `ja` (`vn` is accepted for `vi`). A starting point, not a lock: a user who switches language inside the chat keeps their choice for that session |
| `branding.logoUrl` | SotaAgents wordmark | Your logo in the sidebar and beside the greeting. Must be an absolute `http(s)` URL — anything else is ignored and the default comes back |
| `branding.showLogo` | `true` | `false` renders no logo at all, and wins over `logoUrl` if you set both |
| `branding.showUser` | `true` | `false` hides the signed-in user block at the foot of the sidebar — the right choice when your own page already shows who is signed in |
| `branding.greeting` | "How can I help you today?" | The line on the new-chat screen |
| `branding.placeholder` | "Ask anything" | The composer's placeholder |

Greeting and placeholder are trimmed to 120 characters — long enough for a sentence, short enough that it cannot push the composer off screen.

**The bubble chrome.** The launcher and panel the SDK draws on your page, outside the iframe. Everything here but `title` is ignored in `mode: 'inline'`, where the `container` you pass decides the size and your page decides the surroundings.

| Option | Default | What it does |
| --- | --- | --- |
| `title` | `SotaAgents` | Text in the panel header — left unset, the header shows only its controls. It also names the iframe for assistive technology, and that half applies in `mode: 'inline'` too, which makes it the one row here that is not bubble-only. Leave it unset and a screen reader announces the frame as _SotaAgents_; set it if you white-label |
| `primaryColor` | `#2f6bff` | Launcher and panel header |
| `position` | `bottom-right` | Or `bottom-left` |
| `width` / `height` | `400` / `620` | Panel size in px. _Expand_ grows beyond it |
| `zIndex` | `2147483000` | Lower it if the panel has to sit under something of your own |
| `openOnLoad` | `false` | `true` opens the panel on load instead of waiting for a launcher click |

Two more govern the iframe itself. `allowMicrophone` is off by default; voice input needs it. `sandbox` replaces the default `allow-scripts allow-same-origin allow-forms allow-popups` — and here it inverts the HTML convention you are used to: **`sandbox: ''` is not the strictest policy, it removes the attribute altogether** and takes the iframe's isolation with it. To tighten rather than remove, pass a narrower list of tokens; never an empty string. Leave both alone unless you have a reason.

> [!WARNING]
> Serve the logo over HTTPS
>
> The chat runs over HTTPS, so the browser silently upgrades an `http://` logo and then blocks it: your mark disappears, the SotaAgents one returns, and no failed request appears in the network tab to explain why. It is the most common branding surprise — check it on a page served the way production will serve it.

### Third-party cookies

When the embed runs on a different site from your portal, its session cookie is a third-party cookie.

| Browser | Behaviour | Result |
| --- | --- | --- |
| Chrome, Edge | Cookie kept, partitioned per site | Full session |
| Firefox | Third-party cookies partitioned by default | Full session |
| Safari | Third-party cookies blocked | Falls back to a 10-minute token and re-authenticates automatically |

The fallback works, but it re-authenticates every few minutes. Serving the embed from a subdomain of your own site through a reverse proxy — `ai.yourcompany.com` in front of SotaAgents — makes the cookie first-party and removes the question for every browser. Two rules if you do: do not let the proxy rewrite `Set-Cookie`, and turn response buffering off, because chat answers stream.

### Before you go live

- The secret is in your secret manager, not in source control.
- Every intended user is an active member of the organization.
- The ticket endpoint reads identity from the session only.
- `logout()` runs before your own sign-out, on every path that ends a session.
- The workspace has enough credit allocation — embedded conversations draw on it like any other.
- Ask support to restrict which origins may frame the embed to your own domains.

> [!NOTE]
> Full SDK reference
>
> Every option, callback and error code is in the Embed SDK integration guide, along with a runnable reference integration. Ask your SotaAgents contact for it.

## Reference

Troubleshooting tips for common issues.

### Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| "Out of credit" | The applicable member, organization, or guest allocation is exhausted. | Owner/Admin reviews allocation and policy; the SotaAgents operations team performs top-up when money movement is required. |
| "This organization is inactive" | Trial expired or admin paused the org. | Owner upgrades or reactivates. |
| Sign-in keeps redirecting | Stale tokens in your browser. | Sign out, clear site data for the domain, sign in again. |
| The assistant does not use a connected document | The Knowledge Base app/source may be disabled, unsynced, unprocessed, or not selected for this run. | Check the Knowledge Base app status, then explicitly request Knowledge Base retrieval and inspect citations. |
| A provider connection is missing | That Knowledge Base provider or optional connector is not configured in this deployment. | Ask an admin to check the app configuration; do not rely on a fixed provider list. |
| `ORG_IP_DENIED` | Your current address is outside the enabled organization IP rules. | Move to an allowed network or ask an org admin to correct the CIDR rules. |
| The assistant stopped using a tool that used to work | The provider changed the tool's name or arguments. | Workspace admin clicks _Refresh tools_ on the MCP server. |
| "Tool call failed: not authorized" | The MCP server's saved token expired or was revoked. | Workspace admin disconnects and reconnects the server. |
| Invitation email never arrived | Filtered to spam, or address typo. | Resend the invite, or copy the invite link and share it directly. |
| I can see a workspace but can't change anything | You're a Workspace Member without an administrator grant. | Ask a workspace administrator to grant Workspace Admin if you need configuration access. |
| An installed app isn't visible in a workspace | The exact environment is unavailable or uninstalled, a workspace override disabled it, or your role cannot access it. | Ask an organization administrator to review the installed environment, then check the workspace Apps override and your role. |
| Credit Logs show unexpected app usage | An app is enabled in a workspace you didn't expect. | Review the workspace Apps tab and disable apps that shouldn't be active. |
