UI Contributions
A Lombok app can inject navigation and UI surfaces into the platform. These are
declared under the contributions key of the app manifest (config.json) and are
validated at install time against appContributionsSchema. Contributions are how an
app becomes a first-class part of the Lombok interface rather than a background
worker bundle: they place links and embedded views into the web UI, and they
describe complete server-driven views for the mobile app.
This page is derived from the schema and is meant to be read as a reference. Where this page and the schema disagree, the schema wins.
Source of truth
Section titled “Source of truth”packages/types/src/apps.types.ts—appContributionsSchemaand everything it references:appContributedViewsSchema(the five array-valued web surfaces),appContributionEmbedLinkSchema(the singular web contribution item shape), themobile*schemas, andiconSchema.packages/demo-apps/simple-demo/config.json— a complete, valid example with both web link arrays and a mobileroot.- App Configuration documents where
contributionssits within the wider manifest: see App Configuration.
Shape of contributions
Section titled “Shape of contributions”contributions is a strict object — unknown keys are rejected. It has five
required web link arrays plus one optional mobile block:
| Key | Required | Type | Surface |
|---|---|---|---|
sidebarMenuLinks | yes | AppContributedView[] | Top-level entries in the app sidebar. |
folderSidebarViews | yes | AppContributedView[] | Views shown in the sidebar while viewing a folder. |
objectSidebarViews | yes | AppContributedView[] | Views shown in the sidebar while viewing an object. |
objectDetailViews | yes | AppContributedView[] | Detail views for an object. |
folderDetailViews | yes | AppContributedView[] | Detail views for a folder. |
mobile | no | MobileContributions | Server-driven mobile views and queries. |
The five arrays are required because the object is strict: if an app does not
contribute a given surface, supply an empty array ([]) rather than omitting the
key. The demo app, for example, sets folderDetailViews: [].
When the platform serves contributions to clients it groups them per app, alongside
the app’s appLabel, appIdentifier, and icon.
Web contribution entries (AppContributedView)
Section titled “Web contribution entries (AppContributedView)”Every entry in the five link arrays is the same strict shape:
| Field | Required | Type | Rules |
|---|---|---|---|
path | yes | string | Must be non-empty and start with /. Routes into the app’s own UI bundle. |
label | yes | string | Non-empty display label. |
icon | no | Icon | Builtin or custom icon (see below). |
The path is a route within the app’s embedded UI — the platform renders the app’s
UI bundle at that path inside the named surface. For example, the demo app’s
sidebarMenuLinks entry points at /, while its folder/object views use routes
like /folders/folder-sidebar. See
UI Integration for how that embedded UI is loaded
and authenticated.
Icons on contribution links
Section titled “Icons on contribution links”The icon field uses the shared iconSchema, a discriminated union on source:
source: "builtin"—{ source: "builtin", name, label? }.namemust be one of the published catalog names:app,box,code,file,folder,settings,sparkles,terminal.source: "custom"— ansvgorpngicon served from the app’s UI bundle. Custom icons requireui.enabled: truein the manifest, and each asset path must exist in the bundle (resolved as/ui/<path>). PNG icons must include at least one asset withscale: 2orscale: 3;svgicons may userendering: "template"to be tintable.
For the full icon model and its validation rules, see the icon field in
App Configuration.
Note: This
iconSchemaapplies to web contribution links only. Icons inside mobile components are freeform component properties (e.g.{ builtin, tint, style }) and are not the same shape — see below.
Mobile contributions
Section titled “Mobile contributions”mobile (MobileContributions, strict) describes server-driven views: the app
ships a JSON description of a screen, and the mobile client renders it natively. It
has two optional keys:
| Key | Type | Purpose |
|---|---|---|
queries | MobileQueries | Named data sources the views can read from. |
root | MobileRoot | The app’s home content — the set of views rendered at the app root. |
The mental model: a view owns a data model (a JSON tree). Components read from it by path; queries fetch data and write it into the model; actions re-run queries or navigate between views.
Queries (mobile.queries)
Section titled “Queries (mobile.queries)”queries is a record from a query key to a query definition.
Query keys must match ^[a-z0-9]+(?:[._][a-z0-9]+)*$ — lowercase segments separated
by . or _. Prefixing by data source (lombok.viewer, app.demo.greeting) is a
convention, not a requirement; the source field is what actually decides
resolution.
A query definition is discriminated on source:
source | Fields | Resolves against |
|---|---|---|
"lombok" | path, method?, transform? | The platform API. |
"app" | path, method?, worker, transform? | A named app runtime worker. |
pathmust be an absolute path starting with/.methodis one ofGET,POST,PUT,PATCH,DELETE(optional).- For
source: "app",workermust name a declaredruntimeWorkersentry — this is cross-checked at install time and rejected if the worker does not exist. transformoptionally reshapes the raw response before it lands in the data model (see Query transforms).
"queries": { "lombok.viewer": { "source": "lombok", "path": "/viewer" }, "app.demo.greeting": { "source": "app", "worker": "demo_api_request_worker", "path": "/", "method": "GET" }}Root and views (mobile.root)
Section titled “Root and views (mobile.root)”root is { views: MobileView[] } with at least one view. Across the views:
- view
ids must be unique; - exactly one view must set
navRoot: true— that is the entry surface embedded at the app root. The others are in-app drill-down (navigate) targets.
A MobileView (strict) is:
| Field | Required | Type | Notes |
|---|---|---|---|
id | yes | string | Non-empty; unique within root.views. |
components | yes | MobileComponent[] | At least one; must include a component with id: "root". |
navRoot | no | boolean | Marks the single entry view. |
refreshable | no | boolean | Enables pull-to-refresh (pairs with an actionMap refresh entry). |
initialDataModel | no | object | Seeds the view’s data model. |
initialQueries | no | MobileQueryBinding[] | Query bindings run when the view loads. |
actionMap | no | record<string, MobileQueryBinding[]> | Named actions to query bindings re-run on that action. |
The component list is validated as a tree: every id is unique, a root component
must exist, and every child reference must resolve to a component in the same view.
Components (MobileComponent)
Section titled “Components (MobileComponent)”Components are a generic envelope — a few typed anchor fields plus freeform, component-specific properties:
| Field | Required | Type | Notes |
|---|---|---|---|
id | yes | string | Non-empty; unique within the view. |
component | yes | string | The component type name (e.g. Column, Text, Section). |
weight | no | number | Layout weight. |
accessibility | no | MobileAccessibility | Accessibility label/description. |
action | no | MobileAction | An event fired on interaction. |
| (any other key) | — | JSON | Freeform, component-specific props. |
Child wiring lives in those freeform props and is read structurally by the validator.
The data model and bindings
Section titled “The data model and bindings”Components read dynamic values from the view’s data model using a dynamic string:
either a literal string, or { path } — a pointer into the data model.
A query binding (MobileQueryBinding) connects a query to the data model:
| Field | Required | Type | Notes |
|---|---|---|---|
query | yes | MobileQueryRef | { name, args? }. name must be a declared query. |
targetPath | yes | string | Where the query result is written in the data model. |
loadingPath | no | string | Toggled while the query is in flight. |
Every binding’s query.name in initialQueries and actionMap must reference a
query declared in mobile.queries.
Actions and navigation
Section titled “Actions and navigation”A component’s action is { event: { name, context? } }.
actionMapactions — when an actionnamematches a key in the view’sactionMap, the listed query bindings are re-run.navigate— an event namednavigatedrills into another view. Itscontext.targetmust be theidof a view that exists inroot.views.
Accessibility
Section titled “Accessibility”accessibility is { label?, description? } and requires at least one of the
two.
Query transforms
Section titled “Query transforms”A query may carry a transform that reshapes its raw response before it is written
to the data model. It is a small expression language evaluated server-side. Use
transforms sparingly — keep them to light reshaping rather than business logic.
Constraints summary
Section titled “Constraints summary”At install time the manifest is rejected unless:
contributionscontains all five link arrays (use[]for unused surfaces)- every contribution
pathstarts with/and everylabelis non-empty - custom contribution icons are accompanied by
ui.enabled: trueand resolve to real assets in the UI bundle - mobile query keys match the key pattern, and
source: "app"queries name an existing runtime worker mobile.roothas at least one view, view ids are unique, and exactly one view isnavRoot: true- each view’s components form a valid tree
- every query binding references a declared query, and every
navigateaction targets an existing view id
Worked example
Section titled “Worked example”A trimmed contributions block — web surfaces plus a single mobile entry view that loads two queries and offers pull-to-refresh:
"contributions": { "sidebarMenuLinks": [ { "path": "/", "label": "Landing Page" } ], "folderSidebarViews": [ { "path": "/folders/folder-sidebar", "label": "Demo Folder Sidebar View" } ], "objectSidebarViews": [ { "path": "/folders/object-sidebar", "label": "Demo Object Sidebar View" } ], "objectDetailViews": [ { "path": "/folders/object-detail", "label": "Demo Object View" } ], "folderDetailViews": [], "mobile": { "queries": { "lombok.viewer": { "source": "lombok", "path": "/viewer" }, "app.demo.greeting": { "source": "app", "worker": "demo_api_request_worker", "path": "/", "method": "GET" } }, "root": { "views": [ { "id": "home", "navRoot": true, "refreshable": true, "components": [ { "id": "root", "component": "Column", "children": ["title", "greeting"] }, { "id": "title", "component": "Text", "text": "Simple Demo", "variant": "h3" }, { "id": "greeting", "component": "Text", "text": { "path": "/viewer/user/username" }, "variant": "h5" } ], "initialDataModel": { "viewer": { "user": { "username": "" } }, "loading": "" }, "initialQueries": [ { "query": { "name": "lombok.viewer" }, "targetPath": "/viewer", "loadingPath": "/loading" }, { "query": { "name": "app.demo.greeting" }, "targetPath": "/greeting" } ], "actionMap": { "refresh": [ { "query": { "name": "lombok.viewer" }, "targetPath": "/viewer", "loadingPath": "/loading" } ] } } ] } }}The complete demo — including a multi-view mobile catalog and navigate
drill-downs — lives in packages/demo-apps/simple-demo/config.json.