Native app experiences for bots
A bot can deliver a complete app surface inside its Atlas chat: ride booking, food ordering, parcel delivery, task tracking, dashboards, forms and wizards. Compose the shared native components; Atlas supplies its typography, spacing, light/dark appearance, accessibility sizing, navigation and circular glass header buttons. The food demo uses this same renderer, with no food-specific layout code.
Import the library from atlas-bot-sdk/experiences. It ships in the npm package
alongside this guide and examples/native-ui/.
Build and send a screen
import { createAtlasBot } from 'atlas-bot-sdk';
import { ui, screen, page, scope, openPage, closePage, sendExperience } from 'atlas-bot-sdk/experiences';
const experience = {
id: 'dispatch-desk', title: 'Dispatch desk', version: '1.0.0', icon: 'shippingbox',
tools: ['ui.open_page', 'ui.close_page'], permissions: [],
properties: {
address: { type: 'string', title: 'Delivery address', default: '123 Market Street' }
},
ui: screen({ title: 'Dispatch desk', subtitle: 'Deliver to {{address}}', buttons: [
{ title: 'Edit delivery', icon: 'pencil', action: openPage('details') }
] }, [
ui('Hero', { headline: 'Your delivery', subtitle: 'Arriving today, 2–4 pm' }),
ui('SummaryRow', { title: 'Address', text: '{{address}}' }),
page('details', { title: 'Delivery details' }, [
ui('Control', { scope: scope('address') })
], [{ title: 'Done', action: closePage(), primary: true }])
], [{ title: 'Edit delivery details', action: openPage('details'), primary: true }])
};
const bot = createAtlasBot({ accessToken: process.env.ATLAS_BOT_TOKEN, llm: false });
bot.onCommand('delivery', ctx => sendExperience(ctx, experience));
bot.start();
The bot sends an .atlasexp attachment through its normal guarded document
sender. Atlas displays an install card; the user reviews grants and installs
it, then opens its launcher tile in that chat. On the page, the experience
replaces the chat canvas, conversation pill and bottom tabs. Native Back takes
them back. It does not silently install itself or change another chat.
Experiences are always available in Atlas. Recipients install the pack and review its grants before opening it; no developer setting is required.
Library imports
These modules are part of the same atlas-bot-sdk package; they do not require
separate dependencies. Use a build containing these exports and a client with
chat experience navigation support. The repository changes do not imply that a
new npm release has been published.
| Import | Exports to use |
|---|---|
atlas-bot-sdk |
createAtlasBot; types ChoiceInput, ChoiceRows, ExperienceButtonAction for existing chat buttons |
atlas-bot-sdk/experiences |
Recommended native UI entry point: builders, navigation helpers, bundle helpers, ExperienceDefinition, and sendExperience |
atlas-bot-sdk/experience-ui |
UI-only helpers and types, including UI_TYPES, UIElement, UIButton, UIOption, UIAction, Scope, and PageNavigationOptions |
atlas-bot-sdk/experience-bundle |
experienceYAML, packExperience, ExperienceFiles, EXPERIENCE_MAX_BYTES, and EXPERIENCE_MAX_FILES |
atlas-bot-sdk/wire |
Message wire types and normalization, including ChoiceOption, ExperienceButtonAction, normalizeChoices, and choicesMetadata |
experiences re-exports experience-ui and experience-bundle, so most bots
only need the first two imports. Use sendChoices to send chat buttons instead
of constructing Matrix metadata manually.
Library
| Export | Purpose |
|---|---|
ui(type, props) |
Compose any component in UI_TYPES; typed props use the YAML names |
screen(header, elements, buttons?) |
Native ScreenHeader, body and optional fixed BottomBar |
page(id, header, elements, buttons?) |
Declare a pushed page alongside the root content |
scope(key) |
Bind to a declared schema property: #/properties/<key> |
openPage(id, options?) / closePage() |
Navigate inside a mounted experience; options support push, replace, root, and workflow completion conditions |
openSheet(id) / closeSheet() |
Open/dismiss a native bottom sheet inside an experience |
ui("Sheet", { id, title, elements, buttons }) |
Declare sheet content; there is no separate sheet() builder |
createExperience(definition) |
Generate manifest.yaml and ui.yaml, plus provided assets |
experienceYAML(object) |
Serialize compatible YAML for lower-level authoring |
packExperience(files) |
Package text/binary files into the app's hashed atlasexp/1 envelope |
sendExperience(ctx, definition) |
Build, package and send a card; returns the normal send result |
The builder creates exp.<id>.app, variant: mobile_app, chrome: experience
and an inline form. Local workflow names in workflows become
exp.<id>.<name>; button workflow references use that full name. tools and
permissions are explicit manifest grants. The client performs full vocabulary,
scope, namespace, tool and permission validation on install. createExperience
is an authoring helper, not a replacement for client validation.
UIElement, UIButton, UIAction, UICondition, UIProperty and UIType are
exported for TypeScript. Fields use the same names as YAML, so developers can
move between object builders and hand-authored packs without a translation layer.
Plain JavaScript and model-produced definitions use the same functions. Validate
those generated packs with Atlas before offering them to users.
Complete UI element catalog
The UI element catalog documents all 48 native elements with bindings, supported styles, schema defaults, and an SDK example for each. It is checked against both UI_TYPES and the native catalog validator.
Compose a design for your use case
| Need | Components |
|---|---|
| Catalog, restaurants, vehicles | ImageCard, ItemRow, Grid, Carousel |
| Search and categories | SearchField, FilterBar, search_scope, filter_scope |
| Trip, parcel, order or task status | StatusBanner, Progress, SummaryRow, Metric |
| Forms, preferences, task editing | Control, Toggle, Picker, Slider, Stepper |
| Checkout or booking wizard | Page, Sheet, Dialog, Button, BottomBar |
| Dashboards and richer content | Chart, Graph, Markdown, DocumentPreview, MapView |
| Layout | VerticalLayout, HorizontalLayout, Group, Panel, Card |
Use elements for child content and buttons for actions. Only properties
implemented by that component affect its rendering; for example, native photo
components use image_scope, not an arbitrary CSS background. Consult the
exported types and the repository's docs/workflows/yaml-mini-app-authoring.md
for component-specific contracts and collection/chart/map data shapes.
Declare Page, Sheet or Dialog in the root UI; declarations are hidden until
opened. ui.open_page, ui.open_sheet, and ui.open_dialog take a target that
matches the declaration's id. Grant the actions you use. Pages share form state,
so separate wizard steps can collect fields before a final workflow runs.
For chat alongside pages, keep ordinary bot messages and offer an experience
card. An advanced hand-authored workflow can use the default chrome: atlas for
native grouped forms. Use full experience chrome for an app-style surface.
Header, state and actions
ScreenHeader.title, subtitle, caption and action labels interpolate {{property}}.
Set caption: "Deliver to", title: "{{address}}" and action: openPage("address")
for a tappable delivery header. SummaryRow with style: "section" gives a
section heading and secondary trailing text. Discovery screens can omit bottom
actions and put the basket in the header.
Editing address in the example updates the top bar automatically. A workflow
can update status, brand or other header information through state.set.
Headers use standard SF Symbols and stable native toolbar identities: Atlas
owns circular Liquid Glass, the morphing navigation transition, Back and Close.
Two custom actions are directly visible; additional actions go into More.
BottomBar reserves the bottom safe area for the experience's actions.
Buttons can run a named workflow or an action { tool, args }. Workflow steps
use the existing YAML tool contract (tool, parameters, if/unless, etc.).
For example:
workflows: {
complete: { steps: [
{ tool: 'state.set', key: 'status', value: 'done' },
{ tool: 'ui.close_page' }
] }
}
// Button: { title: 'Complete', workflow: 'exp.team-tasks.complete', primary: true }
// Grants: workflow.run, state.set, ui.close_page
visible_when and enabled_when control presentation. Workflows/backend logic
must validate the operation itself, especially for real bookings/payments.
Form defaults initialize state; reinstalling a new pack is not a live state
update and should preserve existing user data.
The shipped examples use local mock state. Server-backed operations use the
existing capability bridge and declared tools (for example http.request with
its required grants), or the experience event/referee backend documented in
docs/experiences/backend.md and AtlasExperienceServerSDK/. Native page taps
run workflow actions; they do not automatically call bot.onCommand handlers.
Do not embed server credentials in the pack. Use a backend for authoritative
fares, inventory, assignment, transactions and shared task state.
Photos and distribution
import { readFile } from 'node:fs/promises';
import { createExperience, packExperience } from 'atlas-bot-sdk/experiences';
const definition = { ...experience,
assets: { 'photos/item.jpg': await readFile('./item.jpg') },
properties: { ...experience.properties,
photo: { type: 'string', default: 'asset:photos/item.jpg' }
},
ui: screen({ title: 'Catalog' }, [
ui('ImageCard', { title: 'Featured item', image_scope: scope('photo') })
])
};
const files = createExperience(definition);
const bytes = packExperience(files); // write to disk or sendDocument with application/x-atlasexp
Local photos are confined to the installed pack and decoded off the main thread. HTTPS images are also supported. Keep the complete envelope below 2 MiB including base64 overhead, at most 64 files, with safe ASCII relative paths of at most three components. The packer rejects excessive or unsafe input. Hashes detect changed bytes; they are not publisher signatures.
For advanced multi-view/stream packs, call packExperience with hand-authored
manifest.yaml, ui.yaml, scripts and assets. All declared catalog IDs must
carry exp.<id>.; page-local IDs are navigation targets. The full photo-based
food example is in docs/experiences/examples/food-ordering/ in the Atlas repo.
It demonstrates restaurant browsing, filters, favorites, basket arithmetic,
checkout pages, header updates, photos and mock tracking. Compose checkout
fields directly on the page so their native labels and surfaces have room.
Group prices in a quiet Panel with SummaryRow children; hide the receipt
when the basket is empty. Review pages can reuse ItemRow with no buttons
and a quantity subtitle. For tracking, use state-conditioned Hero and
Panel content with one BottomBar action per stage. These patterns work
for any pack; the renderer does not special-case the food example.
Run the examples
From the SDK directory:
npm run examples:native-ui -- /tmp/atlas-native-ui
ATLAS_BOT_TOKEN=... node examples/native-ui/bot.js
The build command writes three folders and .atlasexp files. The bot responds
to /rides, /delivery and /tasks. examples/native-ui/experiences.js is the
source; examples/native-ui/packs/ contains corresponding YAML fixtures verified
by the real iOS loader. When editing the sources, regenerate and copy only the
YAML files into those fixture folders. npm test checks parity and all exported
component names against the native vocabulary; the Atlas test suite installs
these fixtures through ExperiencePackLoader.
The native vocabulary is composable, not arbitrary Swift or CSS execution.
For a custom drawing surface beyond it, Atlas also supports canvas packs with
canvas: true and index.html, packaged by the same packExperience function.
Canvas HTML supplies its own styling; it does not automatically inherit native
Atlas controls. Prefer native components when consistent Atlas design matters.
Completed flows and page history
ui.open_page accepts mode: push (default), replace (replace the current
page), or root (return to the experience home before opening the target).
Opening an existing page unwinds to it rather than duplicating it. Use a focused
edit sheet with ui.close_sheet to return edits to their caller. Use root navigation
after committing an order/task, so Back cannot reopen completed checkout steps.
For a workflow followed by navigation, add workflow_id, success_scope, and
success_value to the open-page action arguments. The form waits for the normal
workflow runner, then navigates only if the specified state changed to the expected
value and the user is still on the originating page. A validation failure or
unchanged success state stays on the current page. Keep the workflow's own validation
and duplicate-submission guards. The SDK supports these arguments as the optional
second parameter of openPage(target, options).
A Panel with style: navigation, title, caption, subtitle, icon and action
renders as one accessible navigation card. Use it for an active order or task near
the top of the home page, outside scroll-bottom checkout actions.
Use a Sheet for focused edits such as delivery address or checkout details.
Open it with ui.open_sheet (SDK: openSheet(id)) and dismiss with
ui.close_sheet (closeSheet()). Native sheets support medium/large heights,
a drag handle, swipe dismissal and Atlas's standard Close control. Use grouped
fields and an ActionBar inside the sheet, not page-only ScreenHeader or
BottomBar elements. Edits update the shared draft as they are made; Done and
Close both dismiss to the unchanged underlying page. Keep the primary checkout,
confirmation and tracking journey as pages.
Complete ride-booking example
examples/ride-booking/experience.js composes a map home, destination/vehicle/
driver/cancellation sheets, review, guarded trip stages and receipt using the
public helpers. Run node examples/ride-booking/build.js /tmp/atlas-ride-booking
after building the SDK. The output includes the installable bundle and the same
YAML used by Atlas's bundled Ride booking demo. Fixed sample coordinates render
through MapKit; the demo does not request GPS, calculate routes or contact drivers.
Detailed selection and itinerary layouts
Use Picker with style: 'cards' to show mutually exclusive options with
title, subtitle, and an SF Symbol icon. It binds the selected value to
scope, works inside native sheets, wraps long labels, and announces the
selected option to VoiceOver. Prices and estimated arrival times belong in the
option copy supplied by your bot. Omit the style to retain the standard picker.
Use Panel with style: 'route' and ordered SummaryRow children for a connected
stop list. Each child's title labels the stop and text describes its location.
This is an itinerary, not a map route or live navigation service.
The examples/ride-booking/experience.js example demonstrates destination,
vehicle, and payment cards, pickup instructions, driver/contact/safety sheets,
and a receipt that preserves the payment selected at booking. Everything uses
local sample data; contact, location sharing, payment and dispatch are not live.
For a map-led experience, add one visible MapView with style: 'stage' to a
screen. Atlas places it edge-to-edge above a rounded content surface while
retaining its native toolbar and safe-area action bar. Use max for map height.
Other screens retain their existing spacing and layout.
Selection cards accept an optional trailing label for a price or other short
value. Picker style: 'rows' provides compact destination-like options without
card borders; search_scope filters option titles and subtitles and keeps the
selected value while searching. Panel style: 'navigation-row' provides a quiet
navigation row. These styles are generic and available to all experience packs.
Picker style: 'rating' renders ordered options as accessible star buttons.
Provide options from lowest to highest; the bound value stores the chosen
option exactly like a standard picker. A default outside the options represents
an unrated state.
Compact experiences with bundled artwork
Picker style: compact uses quiet rows with a selected outline. Options may
provide image: asset:assets/example.jpg, caption, and trailing. Images
follow the same per-install asset resolver as other experience photos. The
card image fits its available space instead of cropping the vehicle.
Hero style: compact uses a smaller heading and caption. ScreenHeader style:
compact reduces spacing in map-led content without replacing native navigation.
BottomBar style: midnight uses a neutral primary action, with optional text
for a trailing value and footer for a short supporting line.
Panel style: profile accepts image_scope for an avatar, secondary_scope for
supporting artwork, title, subtitle, text for an identifier badge, footer
for its description, and buttons for contact or other actions. Panel style:
arrival uses headline/subtitle plus caption/text for a compact status value.
Panel style: route-plain shows the existing stop list without a filled card.
MapView items_scope may point to JSON containing an array of [longitude,
latitude] pairs, or an object keyed by the value at selected_index_scope.
The renderer draws the supplied route; it does not calculate directions or
request location. Invalid coordinates and routes over 1,000 points are rejected.
The ride demo supplies an illustrative SFMOMA route; other destinations show
only the sample pickup area.
Open a page or bottom sheet from a chat button
See the complete chat-button guide for a typed, self-contained bot example, field reference, and troubleshooting.
Use the existing sendChoices buttons with an action. The experience must
already be installed on the receiving device.
The target is a declared Page or Sheet ID in the pack's primary UI definition
(the same definition opened by the experience launcher).
await bot.sendChoices(chatId, "How can I help with your ride?", [
[{
label: "Book a ride",
action: {
type: "experience.page",
experienceId: "ride-booking",
target: "destination",
},
}],
[{
label: "Payment method",
action: {
type: "experience.sheet",
experienceId: "ride-booking",
target: "payment",
},
}],
]);
In a message handler, ctx.sendChoices(prompt, options) takes the same options.
These IDs work with the bundled ride-booking example. For your own food ordering,
tracking, or wizard UI, replace the experience and target IDs with your pack's IDs.
ChoiceInput, ChoiceRows, and ExperienceButtonAction are exported SDK types.
Pages open full-screen with native navigation; sheets use medium/large system
detents. Close returns to the originating chat, preserving its composer and
selection. A page can push further pages or open sheets using the usual
ui.open_page / ui.open_sheet actions. At the entry page, ui.close_page
returns to chat; at the entry sheet, ui.close_sheet closes the sheet. Normal
form discard protection still applies.
Navigation happens locally. It does not send a reply or silent callback, and
buttons remain reusable. action takes precedence over value and silent.
Ordinary options without an action keep their existing behavior. Navigation
also works in reply keyboards, which stay available after navigation.
editMessage can replace or disable these buttons normally. Missing packs,
wrong target types, and unavailable destinations show an error; Atlas never
installs a pack automatically or grants new capabilities on a button tap.
Canvas/web experiences are not supported by this action.
Older clients that do not understand action may send the option's fallback
value as an ordinary reply. Handle that value in your bot if you support older
clients, for example by sharing the installable pack or explaining the required
client update. Opening a destination does not run a submit workflow; any writes
or external operations still require the experience's usual actions and grants.