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.