Apps
Package custom fields, objects, actions, hooks, webhooks and UI extensions in one versioned manifest that any SalesOS organization can install, upgrade and cleanly uninstall.
An app bundles everything an integration needs in SalesOS into one file,
salesos.app.json: the OAuth app it uses and the scopes it asks for, the custom
fields and objects it adds, its custom actions,
before-save hooks, webhook subscriptions,
UI extensions and the settings an admin fills in.
- Publishers register a manifest as an immutable, semver-numbered version. The publisher is the organization that owns the manifest's OAuth app.
- Installing organizations review exactly what an app asks for and install it. Every component is created through the same service that creates it by hand, so all their rules apply. Uninstalling removes exactly what the app created, and nothing else.
Installing apps and page layouts are part of the Professional plan
(app_platform); publishing also needs an OAuth app, which is part of API
access (Enterprise, or any developer org).
Publish an app
- Register an OAuth app under Settings → OAuth apps and allow every scope your manifest will request. Turn on multi-tenant if other organizations should be able to install it; a private app can only be installed by your own organization.
- Write
salesos.app.json(reference below) and publish it under Settings → Apps → Publish, or:
The first version returns the app's signing secret once (sas_…). Store
it with your service; SalesOS keeps only an encrypted copy. Publishing is
session-only (never an API key or OAuth token), like OAuth app management.
Later versions must be newer (semver) than every published one; a version can't be changed once published. The slug and OAuth app are fixed by the first version.
Manifest reference
The JSON Schema is at
/schemas/salesos.app.schema.json — point
your editor at it with "$schema". SalesOS checks more than the schema can
express: scopes and event types must exist, every URL must be a public https://
address (no private, loopback or internal hosts), and schemas must use the
custom-action subset.
| Field | Meaning |
|---|---|
manifestVersion | Always 1. |
slug | Lowercase letters, digits and -, 3–40 characters. Globally unique; claimed by the first publisher. |
version | Semantic version (1.2.0, 2.0.0-beta.1). |
publisher, icons, urls | Shown to installers. URLs must be https. |
oauth.clientId | Your OAuth app. oauth.scopes are the resource scopes (READ_<RESOURCE> / WRITE_<RESOURCE>) installations grant; identity scopes (openid, offline_access…) are not used by installations. |
customObjects[] | key becomes the object's API name; fields use text, textarea, number, currency, date, boolean, select, email, phone, url, lookup or master_detail. Relationship fields name a target (LEAD, CONTACT, ACCOUNT, OPPORTUNITY, TASK, CASE, or one of the app's own objects as custom:<key>) and an onDelete rule, as in custom objects. |
customFields[] | A field on LEAD, CONTACT, ACCOUNT, OPPORTUNITY, PRODUCT or QUOTE, of type TEXT, TEXTAREA, NUMBER, CURRENCY, DATE, DATETIME, CHECKBOX, PICKLIST, MULTI_PICKLIST, EMAIL, PHONE or URL. key becomes its API name. Picklists need options. A field's type can't change in a later version — publish a new key. |
customActions[] | As in Custom actions; key is the apiName. objectTypes may name the app's own objects as custom:<key>. |
beforeSaveHooks[] | As in Before-save hooks. |
webhooks[] | A subscription to event types from the catalog, delivered with the versioned envelope. |
uiExtensions[] | slot is record.tab, record.sidePanel, record.headerAction, list.action, dashboard.widget, app.page or global.modal; record and list slots list objects (LEAD, CONTACT, ACCOUNT, OPPORTUNITY, or custom:<apiName>). Optional formFactors (desktop, mobile) and modalSize. See UI extensions. |
settings | The custom-action schema subset. The installing admin fills it in; your app reads it. Settings are not secret — never ask for passwords or keys here. |
Limits: 25 custom fields, 5 custom objects (30 fields each), 20 actions, 10 hooks, 10 webhooks, 20 UI extensions, 30 settings, 256 KB.
Metadata components
An app can also ship automation, pages and content. Each of these sections is
an array of components in the metadata source format
— the same JSON a salesos metadata pull file holds, so you can build them in a
developer org, pull them, and paste them in:
| Section | Component (source file) | Limit |
|---|---|---|
functions[] | functions/<apiName>/function.json plus source (and language) | 10 |
validationRules[] | validation-rules/*.rule.json | 50 |
workflows[] | workflows/*.workflow.json | 25 |
flows[] | flows/*.flow.json | 25 |
approvalProcesses[] | approval-processes/*.approval.json | 10 |
customLabels[] | labels/*.label.json | 500 |
translations[] | translations/*.translation.json, one per locale | 10 |
pageLayouts[] | layouts/*.layout.json | 20 |
pages[], appPages[] | pages/*.page.json, app-pages/*.apppage.json | 20, 10 |
reports[], dashboards[] | reports/*.report.json, dashboards/*.dashboard.json | 50, 20 |
emailTemplates[] | email-templates/*.email.json | 50 |
customEventTypes[] | events/*.event.json (custom events) | 20 |
namedCredentials[] | credentials/*.credential.json, configuration only — secret and clientSecret are refused | 10 |
They install through the metadata engine, exactly like a deploy: validated against the installing org (references, compile, page and layout checks), written in dependency order, recorded against the installation one by one, and rolled back in reverse if one fails. Rules that follow:
- Functions run as the installing admin with the user role (never
runAsAdmin), may only use scopes that are also inoauth.scopes(the admin consents to them), and cannot declare secrets. TheirallowedHostsare listed on the consent screen. - Automation is on: workflows, flows and approval processes arrive active, because the admin consented to the app. A marketplace suspension turns the app's functions, workflows and flows off with its actions, hooks and webhooks.
- A component with the same key that the org already has stops the install
(
APP_INSTALL_FAILED, 409 from the component) — the app never takes over something the org built. - Layouts and pages refer to the app's own extensions as
app/<your slug>:<key>. - Translations add keys to the org's locale; uninstall removes only the keys the app added.
- Reports, dashboards and email templates belong to the installing admin.
- Named credentials install inactive; the installing admin sets the secret under Settings → Developer → Connections, where an app's credential accepts only its secrets and on/off. The app's webhooks may subscribe to the custom event types it declares.
Signing: one secret per organization
Everything SalesOS signs for your app in an organization — custom action and hook calls, webhook deliveries and UI-extension context tokens — uses that organization's installation secret, which you derive from your app's signing secret:
Every signed payload carries organizationId, so your endpoint derives the
secret and then verifies X-SalesOS-Signature exactly as described in
Events → Verifying signatures:
A secret an organization's admins can see (a webhook's, say) is only that organization's, and reveals nothing about your app secret or other organizations. Settings → Apps → Publish → rotate issues a new app secret and re-signs every installed component at once.
Install
An org owner or admin installs from Settings → Apps → Available. The consent screen lists every scope in plain language and every component the app will create, then asks for the settings. Through the API (session only):
acceptedScopes must contain every scope the version requests — it records
that the admin saw them. Installation is all or nothing: components are
created in dependency order (objects, fields, actions, hooks, webhooks), each
recorded against the installation as it is created; if one fails, the ones
already created are removed and the installation is left FAILED with the
reason (400 APP_INSTALL_FAILED names the component). Installing the version
that is already installed is a no-op (alreadyInstalled: true).
The admin's consent becomes a grant on your OAuth app for the organization (shown under Connected apps). Revoking it there cuts the app's access immediately.
Upgrade
GET /v1/apps/installations/{id}/upgrade-preview lists the scopes and
components a newer version adds or removes; POST /v1/apps/installations/{id}/upgrade with the accepted scopes applies it. New
components are created, existing ones updated, and components the new version
no longer declares are retired — custom fields and objects are kept with their
data. If anything fails, the updated components are restored and the
installation stays on its old version. Narrowing scopes revokes outstanding
extension tokens.
Uninstall
POST /v1/apps/installations/{id}/uninstall with dataPolicy:
keep(default): custom fields and objects stay, with their data, and reattach if the app is installed again;delete: they are deleted with every value stored in them.
Actions, hooks, webhooks and every metadata component are removed, and the app's grant and every token it
holds in the organization are revoked. If a component can't be removed, the
uninstall stops with 409 APP_UNINSTALL_INCOMPLETE; what was removed stays
removed and the call can be repeated.
Managed components
Everything an installation created is managed by the app. The settings screens
show Managed by <app> with a link to the installation, and the APIs refuse
to edit, delete or re-key a managed custom field, custom object, custom action,
before-save hook, webhook, function, validation rule, workflow, flow, approval
process, custom label, page layout, page, report, dashboard or email template
with 409 COMPONENT_MANAGED_BY_APP (turning an action, hook or webhook on or
off still works; copying, previewing and testing a managed component is fine).
Metadata deploys and change sets skip managed components on retrieve and refuse
to change them. Change them by publishing a new
version, or uninstall. An org admin can take one over with
POST /v1/apps/installations/{id}/components/{componentId}/detach: it becomes
an ordinary component of the organization, upgrades and uninstall leave it
alone, and the detach is recorded in the audit log.
Events
Subscribe your manifest's webhook to these to learn about your own lifecycle:
| Event | When |
|---|---|
app.installed | Installed, or upgraded (previousVersion is set). Sent once the app's webhooks exist. |
app.uninstalled | Uninstalled. Sent before the app's own webhooks are removed, with removedComponents and retainedFields. |
Reading your installation
With an OAuth access token issued to your app — including from an extension
through api.fetch — GET /v1/app-runtime/installation returns the installation in the token's
organization: id, version, granted scopes and settings.
Errors
| Code | Meaning |
|---|---|
APP_MANIFEST_INVALID | The manifest failed validation; errors[] has path and message for each problem. |
APP_OAUTH_APP_NOT_FOUND | oauth.clientId isn't an OAuth app of the publishing organization. |
APP_SCOPE_NOT_ALLOWED / APP_SCOPE_INVALID | A scope the OAuth app doesn't allow, or an identity scope. |
APP_SLUG_TAKEN, APP_VERSION_EXISTS, APP_VERSION_NOT_NEWER | Publishing conflicts. |
APP_CONSENT_REQUIRED | acceptedScopes is missing a requested scope. |
APP_SETTINGS_INVALID | Settings don't match the schema. |
APP_INSTALL_FAILED | A component was refused; nothing was left behind. |
APP_INSTALL_IN_PROGRESS | Someone else is changing this installation. |
APP_ACCESS_REVOKED | An admin revoked the app under Connected apps. |
Approvals
Multi-step approval processes on any record — entry criteria, approvers by user, profile or manager, record locking while pending, and status write-back.
Marketplace
List your app in the SalesOS marketplace — publisher profile and domain verification, the listing, submission, automated checks, review policy and customer reviews.