Skip to content

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.

  • packages/types/src/apps.types.tsappContributionsSchema and everything it references: appContributedViewsSchema (the five array-valued web surfaces), appContributionEmbedLinkSchema (the singular web contribution item shape), the mobile* schemas, and iconSchema.
  • packages/demo-apps/simple-demo/config.json — a complete, valid example with both web link arrays and a mobile root.
  • App Configuration documents where contributions sits within the wider manifest: see App Configuration.

contributions is a strict object — unknown keys are rejected. It has five required web link arrays plus one optional mobile block:

KeyRequiredTypeSurface
sidebarMenuLinksyesAppContributedView[]Top-level entries in the app sidebar.
folderSidebarViewsyesAppContributedView[]Views shown in the sidebar while viewing a folder.
objectSidebarViewsyesAppContributedView[]Views shown in the sidebar while viewing an object.
objectDetailViewsyesAppContributedView[]Detail views for an object.
folderDetailViewsyesAppContributedView[]Detail views for a folder.
mobilenoMobileContributionsServer-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:

FieldRequiredTypeRules
pathyesstringMust be non-empty and start with /. Routes into the app’s own UI bundle.
labelyesstringNon-empty display label.
iconnoIconBuiltin 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.

The icon field uses the shared iconSchema, a discriminated union on source:

  • source: "builtin"{ source: "builtin", name, label? }. name must be one of the published catalog names: app, box, code, file, folder, settings, sparkles, terminal.
  • source: "custom" — an svg or png icon served from the app’s UI bundle. Custom icons require ui.enabled: true in the manifest, and each asset path must exist in the bundle (resolved as /ui/<path>). PNG icons must include at least one asset with scale: 2 or scale: 3; svg icons may use rendering: "template" to be tintable.

For the full icon model and its validation rules, see the icon field in App Configuration.

Note: This iconSchema applies 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 (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:

KeyTypePurpose
queriesMobileQueriesNamed data sources the views can read from.
rootMobileRootThe 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 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:

sourceFieldsResolves against
"lombok"path, method?, transform?The platform API.
"app"path, method?, worker, transform?A named app runtime worker.
  • path must be an absolute path starting with /.
  • method is one of GET, POST, PUT, PATCH, DELETE (optional).
  • For source: "app", worker must name a declared runtimeWorkers entry — this is cross-checked at install time and rejected if the worker does not exist.
  • transform optionally 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 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:

FieldRequiredTypeNotes
idyesstringNon-empty; unique within root.views.
componentsyesMobileComponent[]At least one; must include a component with id: "root".
navRootnobooleanMarks the single entry view.
refreshablenobooleanEnables pull-to-refresh (pairs with an actionMap refresh entry).
initialDataModelnoobjectSeeds the view’s data model.
initialQueriesnoMobileQueryBinding[]Query bindings run when the view loads.
actionMapnorecord<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 are a generic envelope — a few typed anchor fields plus freeform, component-specific properties:

FieldRequiredTypeNotes
idyesstringNon-empty; unique within the view.
componentyesstringThe component type name (e.g. Column, Text, Section).
weightnonumberLayout weight.
accessibilitynoMobileAccessibilityAccessibility label/description.
actionnoMobileActionAn event fired on interaction.
(any other key)JSONFreeform, component-specific props.

Child wiring lives in those freeform props and is read structurally by the validator.

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:

FieldRequiredTypeNotes
queryyesMobileQueryRef{ name, args? }. name must be a declared query.
targetPathyesstringWhere the query result is written in the data model.
loadingPathnostringToggled while the query is in flight.

Every binding’s query.name in initialQueries and actionMap must reference a query declared in mobile.queries.

A component’s action is { event: { name, context? } }.

  • actionMap actions — when an action name matches a key in the view’s actionMap, the listed query bindings are re-run.
  • navigate — an event named navigate drills into another view. Its context.target must be the id of a view that exists in root.views.

accessibility is { label?, description? } and requires at least one of the two.

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.

At install time the manifest is rejected unless:

  • contributions contains all five link arrays (use [] for unused surfaces)
  • every contribution path starts with / and every label is non-empty
  • custom contribution icons are accompanied by ui.enabled: true and 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.root has at least one view, view ids are unique, and exactly one view is navRoot: true
  • each view’s components form a valid tree
  • every query binding references a declared query, and every navigate action targets an existing view id

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.