How a developer or agent turns a MAIAS document into a working application (R6.3). This is the document to read when asked to "build the app described in this file". It defines conventions, not mandates — deviate deliberately, and record why.
Companion references: the spec (docs/spec/, condensed: llms.txt); the MAIAS Browser (MAIAS_browser/) is a living implementation of these conventions.
| MAIAS construct | Becomes |
|---|---|
app.name, app.description |
App display name, store/readme copy |
screens[] |
One route/screen module each; path is the route path verbatim (:param → the router's param syntax) |
app.navigation.primary.screens |
The tab bar / root navigator, in order; hide the bar if <2 |
app.navigation.secondary.screens |
Non-tab routes registered on the enclosing stack |
flows[] |
Navigation stacks / groups and the seams between them; first flow's entry_screen = initial route |
presentation (push/replace/modal/sheet) |
Navigator action (push/replace) or screen presentation option (modal/sheet) |
screen elements[] |
The screen's component tree, top to bottom |
states (empty/loading/error) |
Conditional render branches keyed on the screen's data status |
data.reads / data.writes |
State selectors / queries and mutations — the app's data contract |
deep_link + app.links.scheme |
Linking configuration entries |
auth: required |
Route guard / gated group |
x_* fields & element types |
Ignore unless you know them; never fail on them |
- One module per screen, named after the screen
id. - Use the document
pathas the route path. Parameters (:id) bind to route params; a deep-linked screen MUST be renderable from its params plusdata.readsalone (no reliance on in-memory navigation state). - Static-over-param precedence (spec §2.4) is native to most routers; if yours is ambiguous, register static paths first.
navigation.primary.screensdefines the tab navigator: one tab per screen, document order, label = screentitle.- Where a screen registers — decided by how it is presented, then where it nests:
modal/sheetscreens register on the root stack (they present over the tab bar);- pushed screens whose paths nest under a tab's path belong to that tab's stack;
- tab-root screens sit at the tab's index route — the document
pathremains the canonical (deep-link) URL; add a route alias or redirect if you want it verbatim in the address bar.
- Flows mark journey seams: when a link crosses out of a flow whose purpose is complete (onboarding → main, checkout → confirmation), the document says
presentation: replace— implement it as a history replacement so back cannot re-enter the dead journey.replaceappears on links and on elements alike (buttonwithpresentation: replace); both mean the same navigator action. Per spec §4.4, a link-level presentation always wins over the target screen's default — apply each link's own value. - The first flow's
entry_screenis the app's initial route. If it isn't a primary screen (e.g. a welcome journey), the app opens on that journey andreplaces into the tabbed shell when it completes. - Concrete presentation values are router-specific: in Expo Router / native-stack,
modal→presentation: 'modal'andsheet→presentation: 'formSheet'.
- Map each element type to your design system's component (the taxonomy table gives per-type semantics; the adapter guide shows the registry pattern).
- Render order = document order.
- Structured labels: split
|forchips/segmented_control/radio_group/label_value,\nforbullets; the literalnonemeans skeleton placeholders. target-bearing elements navigate (honouringpresentation); targetless interactive elements are local actions — wire them to handlers named from the screen'sactions:list.- Unknown types: render a labelled placeholder, never crash (spec §10.1).
- Each distinct key across all
data.reads/data.writesis one data entity. The same key on different screens is the same entity — build one source of truth (store slice, query key, or endpoint) per key. reads→ the screen subscribes/queries that entity; drive theloading/error/emptystates from that query's status.writes→ the screen owns a mutation for that entity; screens that read the same key must observe the change.- A useful derivation:
grepevery key and the screens using it → that's the app's state-management inventory and a first cut of its API surface (read endpoints for read-only keys, CRUD for read+write keys).
auth: requiredscreens sit behind a guard: unauthenticated users are routed to the auth journey, then returned. Gate each marked screen — gating is per-screen, not inherited (spec §9.2).- If the document declares no auth screens (gates without a journey), scaffold a placeholder sign-in route, mark it clearly as an addition the document does not define, and flag the gap back to the document's author — it is an IA omission, not yours to silently design.
- Register
deep_link: truescreens with the OS under<scheme>://<path>; the guard still applies on arrival. (In Expo, the scheme goes inapp.json; Expo Router derives the per-route linking table from the file tree.)
The document: 5 screens, 2 tabs (home, settings), flows main (home, task_detail, task_new) and settings (settings, about).
app/
├── _layout.tsx # Root stack; task_new is presented modally
├── (tabs)/
│ ├── _layout.tsx # Tabs from navigation.primary: home, settings
│ ├── index.tsx # home (path /tasks → tab root)
│ └── settings/
│ ├── index.tsx # settings (auth-gated)
│ └── about.tsx # about (/settings/about nests under the settings tab)
├── tasks/
│ ├── [id].tsx # task_detail (/tasks/:id, deep-linked: todo://tasks/42)
│ └── new.tsx # task_new (/tasks/new; static wins over /tasks/:id)
// app/_layout.tsx — presentation: modal from the document
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="tasks/[id]" options={{ title: 'Task Detail' }} />
<Stack.Screen name="tasks/new" options={{ presentation: 'modal', title: 'New Task' }} />
</Stack>// app/(tabs)/_layout.tsx — one tab per navigation.primary entry, in order
<Tabs>
<Tabs.Screen name="index" options={{ title: 'My Tasks' }} />
<Tabs.Screen name="settings" options={{ title: 'Settings' }} />
</Tabs>| Key | Read by | Written by | Implementation |
|---|---|---|---|
tasks |
home | — | list query; its status drives home's empty/loading/error states |
task |
task_detail | task_new | fetch by :id; created by the form |
task_status |
— | task_detail | completion mutation, invalidates tasks + task |
const { data: tasks, status } = useTasks(); // reads: tasks
if (status === 'pending') return <HomeLoading />; // states.loading
if (status === 'error') return <HomeError />; // states.error (banner + retry per the document)
if (tasks.length === 0) return <HomeEmpty />; // states.empty (empty_state + "Add task" button)
return <TaskList tasks={tasks} />; // default elementssettings→ wrap in the auth guard;aboutinherits nothing — it's ungated by the document.task_detailhasdeep_link: true+ schemetodo→ linking config mapstodo://tasks/:id; the screen loads from theidparam and thetaskread only.
An agent following this section against examples/todo_list/maias.yaml should produce the skeleton above (or its equivalent in another router) without further input — that equivalence is the Phase 9 acceptance test for this guide.