UI extensions
Show your app inside SalesOS — record tabs, side panels, header buttons, list actions, dashboard widgets, full pages and dialogs, on desktop and phones — in a sandboxed frame that talks to SalesOS through @salesos/app-bridge.
An installed app can place its own pages in SalesOS. Each
UI extension in the manifest names a slot and an https URL; SalesOS
renders that URL in a sandboxed iframe and connects it with
@salesos/app-bridge.
| Slot | Where it appears | Context |
|---|---|---|
record.tab | A tab on Lead, Contact, Account, Deal and custom object record pages (objects). Mounted only while it is the active tab. | record |
record.sidePanel | A panel in the record page's field rail. | record |
record.headerAction | A button in the record page header; opens your page in a dialog. | record |
list.action | A button on the list page (standard and custom objects); opens your page in a dialog. | selection (selected ids, up to 500) |
dashboard.widget | A card on the dashboard home. | — |
app.page | A full page at /dashboard/apps/<slug>/<key>, with an item under Pages in the navigation. | — |
global.modal | Not placed anywhere: a dialog your other extensions open with openModal. | modal.params |
objects name standard objects (LEAD, CONTACT, ACCOUNT, OPPORTUNITY)
or custom objects as custom:<apiName> — an app's own objects use their manifest
key. Admins can reorder, hide and position record tabs and side panels with
page layouts, and place tabs, side panels, widgets
and app pages on page-builder pages (the palette's
App extension component).
Phones and form factors
formFactors lists where an extension renders: desktop (768 px wide and up)
and/or mobile (narrower). Leave it out to render on both. An extension that
doesn't render at the current width is simply not there — no tab, button, nav
item or widget — and an app.page opened on the wrong device explains that.
On phones record tabs and side panels stack in the page, header buttons wrap
under the title, and dialogs (list.action, record.headerAction,
global.modal) fill the screen. context.formFactor tells your page which one
it is rendering at, and changes (onContextChange) when the window crosses the
breakpoint.
modalSize (sm, md, lg, full) sets the dialog width for
record.headerAction, list.action and global.modal.
The sandbox
- No
allow-same-origin: your page runs with an opaque origin. It can't read SalesOS cookies, storage or DOM — and can't use its own cookies orlocalStorageeither, so keep state in memory or on your backend (use the context token to identify the user). - No top navigation, no camera, microphone, clipboard or other permissions. Popups are allowed (for OAuth into your own service, or help links).
- Only
httpsURLs from the manifest are framed. The frame gets no referrer. - Your page must allow being framed by SalesOS: if you send
Content-Security-Policy: frame-ancestors, includehttps://www.salesos.org.
SalesOS shows a skeleton until your page connects, and an error with Try again if it doesn't within 15 seconds.
Install the SDK
connect() options: allowedHostOrigins (default https://www.salesos.org
and https://salesos.org — add your staging host if you have one),
handshakeTimeoutMs (10 s) and requestTimeoutMs (10 s; api.fetch 30 s).
API
| Method | What it does |
|---|---|
getContext() / refreshContext() | The context: app, slot, record or selection, user, organization, locale, theme, context token. |
onContextChange(fn) | Called when the user switches theme or language, or the record changes. Returns an unsubscribe function. |
resize(height) / autoResize() | Size the frame (80–2000 px). |
navigate(path) | Send the user to a SalesOS route; only /dashboard… paths are accepted. |
toast(message, variant?) | success, error or info, shown with your app's name. |
confirm({ title, message?, confirmLabel?, destructive? }) | The SalesOS confirm dialog; resolves true when confirmed. |
refreshRecord() | Reload the record page after your app changed the record. |
openModal({ extensionKey, title?, size?, params? }) | Open one of your app's global.modal extensions in a SalesOS dialog; resolves { result, dismissed } when it closes. |
closeModal(result?) | From inside a dialog: close it and hand result back to the opener. |
api.fetch(path, { method?, query?, body?, idempotencyKey? }) | Call the SalesOS API — see below. |
destroy() | Stop the bridge. |
Errors reject with AppBridgeError and a code: NOT_EMBEDDED,
HOST_NOT_ALLOWED, HANDSHAKE_TIMEOUT, TIMEOUT, RATE_LIMITED,
NAVIGATION_BLOCKED, PATH_NOT_ALLOWED, NO_API_ACCESS, INVALID_REQUEST,
MODAL_OPEN, MODAL_NOT_FOUND, MODAL_NOT_AVAILABLE, NOT_IN_MODAL.
Dialogs
The dialog is its own sandboxed frame with its own bridge, token and context;
the two frames never talk to each other directly — params and result pass
through SalesOS as JSON. An app can open only its own global.modal
extensions, one dialog at a time (MODAL_OPEN while one is showing), and the
user can always close it (dismissed: true).
api.fetch
SalesOS makes the request, not your frame. The host mints a short-lived (5-minute) OAuth access token for your app that acts as the viewing user, limited to the scopes the organization granted your app, sends the request without cookies, and returns only the status, a few headers and the body. Your frame never sees the token or the user's session. The user's record visibility, profile and field-level security apply, so your app sees exactly what that person can see, and nothing outside its scopes.
Paths are API paths (/leads/…; a /v1 prefix is optional). Credential and
admin APIs (/auth, /api-keys, /oauth, /apps, /admin, …) are never
reachable. Each frame may have 8 requests in flight and 300 per minute. Your
own backend can call GET /app-runtime/installation the same way to read the
installation's settings.
Context
theme.tokens are the --salesos-* custom properties from
@salesos/design-tokens for the active mode; set
them on your root element to match SalesOS, and listen with
onContextChange for theme switches.
Context token
contextToken is a 5-minute HS256 JWT signed with your app's
installation secret
for the organization (aud = app:<slug>; claims sub user, org, inst
installation, ext extension key, rec record). Send it to your backend to
learn who is looking without a login of your own:
rec says which record the page showed; it is not an authorization — read the
record through api.fetch (as the user) before acting on it.
Security model
- Your page runs in a sandboxed frame with an opaque origin: it can't read SalesOS cookies, storage or DOM, and SalesOS never gives it the user's session or an access token.
- The SDK only connects to an allowed SalesOS origin, and SalesOS only accepts a connection from the frame it created for your extension. Messages after that travel on a private channel that other windows can't observe or inject into.
- Every request from the frame is validated by SalesOS (paths, sizes, methods)
before anything happens; a frame can't widen its scopes or reach another
installation.
openModalonly opens the same installation'sglobal.modalextensions, andcloseModalonly works in a frame SalesOS shows in a dialog. - Revoking the app under Connected apps, uninstalling it, or a marketplace suspension stops token minting immediately.
Marketplace
List your app in the SalesOS marketplace — publisher profile and domain verification, the listing, submission, automated checks, review policy and customer reviews.
Page layouts
Arrange Lead, Contact, Account, Deal and custom object pages for your organization or for one profile — tab order, the field rail and where installed apps appear.