Skip to content

Manifest format

A micro app is stored data in D1: a JSON manifest that describes data, screens, actions and reminders, downloaded to the phone and drawn with real iOS components by the host. And an optional compute source, JavaScript that runs on the server only, for anything that needs calculation. Nothing executable ever reaches the phone. The phone downloads data; the server runs code.

The agent is signed in to the owner’s Daiz account: the owner signs in once in the browser when the agent connects (/mcp or /mcp/oauth). Tools take no email or code. If access fails, ask the owner to reconnect the Daiz server in their agent (in Claude Code: /mcp, then Authenticate).

  1. Closed grammar. A fixed set of components, bindings and actions, all compiled into the host. No eval, no expressions, no URLs, no embedded JS, no downloadable modules. (Codex review, 2.5.2, DPLA 3.3.1(B))
  2. Logic lives on the server. Anything beyond filter, sort, count, sum and average is a compute function. Its result is a stored document the screen binds to.
  3. Validated before deploy. The MCP rejects a manifest that fails the schema, with the exact path and reason, so the agent fixes it on the computer, not after a crash on the phone.
  4. Data outlives code. Collections are declared; changing a screen never touches data. Removing a field hides it, it does not delete it.
  5. The host owns the feel. Navigation, gestures, keyboard, typography, dark mode and accessibility come from the host. The manifest picks colors, an icon, layout and copy.
{
"format": "microapp/0",
"id": "daisy",
"name": "Daisy",
"icon": { "symbol": "pawprint.fill", "color": "indigo" },
"accent": "indigo",
"permissions": ["camera"],
"collections": { ... },
"computed": { ... },
"reminders": [ ... ],
"badge": { ... },
"screens": { ... },
"start": "home"
}
Key What
icon Required SF Symbol and palette color, plus optional asset key for an uploaded logo. See App logo below
accent One of the system palette: blue indigo purple pink red orange yellow green teal mint cyan brown gray
permissions camera, photos, location. Each triggers the host’s per-app consent sheet (4.7.3)
collections The data model
computed Names of values the compute file produces, and the keys each one returns with their kinds, so screens can bind to them and the validator can check
reminders Push rules
badge What number the launcher icon shows
screens The UI
"collections": {
"log": {
"fields": {
"type": { "type": "enum", "values": ["night", "bark", "touch", "lick"] },
"minutes": { "type": "number", "optional": true },
"diverted":{ "type": "bool", "optional": true },
"at": { "type": "datetime", "default": "now" }
}
}
}

Field types: text, number, bool, date, datetime, enum, photo, location, ref (to another collection), refs, email, phone, money, duration, multiEnum. See v0.5 for constraints and relationships. default may be now or today or a literal. Every record also gets id, createdAt, updatedAt, createdBy, updatedBy, and an integer version.

A value in a component is either a literal or one of these. No other syntax exists.

Binding Example Means
{"field": "minutes"} in a list row A field of the current record
{"computed": "nextService.kmNow"} in a stat One key of a value the server computed. Always <name>.<key>, and the key must be in returns
{"computed": "careDue.byId", "key": {"field": "id"}} in a list row Look up one entry of a computed map by the current record
{"state": "draft.minutes"} in a form A screen-local value
{"count": Q} / {"sum": Q, "of": "minutes"} / {"avg": ...} in a stat An aggregate of a query
{"format": "relativeDays", "value": B} in text A fixed host formatter: date, time, relativeDays, number, currency:ILS, km, minutes
{"text": "{0} km", "args": [B]} in text Template with positional args only

A query Q:

{ "from": "log",
"where": [["type", "eq", "night"], ["at", "in", "thisNight"]],
"sort": ["at", "desc"], "limit": 20 }

Operators: eq ne lt lte gt gte in notIn contains startsWith isEmpty. Time ranges for in: today, thisNight (18:00 to 12:00), thisWeek, lastWeek, last7Days, last30Days, thisMonth. v0.5 also supports literal membership lists and nested filters below.

Component Native Key props
screen navigation stack page title, largeTitle, children, toolbar ({symbol, action} buttons)
section grouped list section title, footer, children
callout tinted banner tone (info warn danger), title, text
stat big number tile label, value, unit, tone, caption
statRow 2-3 stats side by side children (stats)
tapGrid grid of big buttons with a count items: {label, count, caption, action}
list list bound to a query query, row, empty, swipe (delete / custom action)
row list cell title, subtitle, trailing, pill, action, symbol
table scrollable table columns, rows (bound to a computed array)
chart native SVG kind (bar line area donut), series (computed array), x, y; optional seriesFields below
text body text value, style (body caption headline)
copyField value with a copy button label, value
form input group children (fields), submit
fields textField numberField dateField toggle segmented chips label, bind (state path), options, default (a literal, or {"field": ...} to prefill from the record in scope), multiline (text)
photo thumbnail value (photo field)
empty empty state symbol, title, text, action
Action Does
{"insert": "log", "values": {...}} Adds a record. Values are literals or {"state": ...}
{"update": "{record}", "values": {...}} Updates the current record
{"delete": "{record}"} Moves to 30-day trash and offers Undo. Optional legacy confirm prompts first
{"undoLast": "log"} Removes the last record this device added
{"navigate": "screenId", "with": "{record}"} / {"back": true} Navigation
{"sheet": "screenId"} / {"dismiss": true} Bottom sheet
{"set": "state.path", "value": ...} Screen state
{"capture": "photo", "into": "state.photo"} Camera or picker, host-owned sheet. Needs permissions
{"locate": true, "into": "state.where"} Foreground location, once
{"toast": "Logged"} Host toast
{"haptic": "success"} Pending Apple’s 4.7.2 permission. Validator rejects it until enabled
{"sequence": [A, B, C]} Run in order

<app>.compute.js exports named functions. The host runtime calls them on the server, in an isolated sandbox per micro app, with read-only access to that app’s data. They return plain JSON that is stored and pushed to the phone.

export async function nextService({ data, now }) {
const odo = await data.list("odometer", { sort: ["date", "asc"] });
...
return { label: "04/2027", kmLeft: 2840, tone: "ok" };
}

What they return and when they run, declared in computed:

"computed": {
"nextService": { "returns": { "kmNow": "number", "subtitle": "text", "pill": "pill" },
"runOn": ["odometer", "services"] },
"weekly": { "returns": { "rows": "rows" }, "runOn": ["log"], "schedule": "daily 00:05" }
}

The function returns one object; returns lists its keys and what each holds:

Kind Holds Bind it in
number text bool date a plain value any value slot, and insert/update values of the same type
pill {text, tone}, tone one of the palette row.pill, any text slot
map { [recordId]: value } a lookup: {"computed": "x.byId", "key": {"field": "id"}}
rows an array of flat objects table.rows, chart.series (chart rows newest first; the chart draws them oldest to newest, left to right)
notify [{key, title, body, open}] a computed reminder

The validator checks every binding against returns, and the kind against the slot. The server checks that the function’s output matches returns before storing it.

runOn re-runs on any write to those collections. schedule re-runs on a timer. Results are cached on the phone, so screens show the last value offline. The sandbox has no network in v0.

"reminders": [
{ "id": "care-due", "when": { "computed": "careDue.notify" } },
{ "id": "evening", "when": { "schedule": "daily 21:00" },
"title": "Daisy", "body": "Anything to log from today?", "open": "home" }
]

Two kinds. A schedule with fixed text. Or a computed rule: a compute function returns a list of {key, title, body, open}, and the server sends each key at most once. Every push carries the app name as the title, the app id as the thread, and open as the deep link. The server enforces the daily cap.

"badge": { "computed": "careDue.overdueCount" }

or {"count": Q}. Shown on the icon inside the launcher.

Schema shape; every binding points at a declared collection, field, computed key or state path; every computed key is the right kind for its slot; every value written into a collection fits the field’s type (a text input cannot feed a number field, chip options must be enum values); every navigate target exists; every action is in the catalog; permissions cover capture and locate; enums are respected; no unknown keys. Errors come back to the agent as a list of path: problem: fix.

Style on every component (closed vocabulary, no CSS):

"style": { "tone": "teal" | "accent" | <palette>, "size": "small|regular|large", "emphasis": "plain|tinted|filled", "align": "leading|center" }

Also on every component: "id" (a stable name; the owner’s hide and reorder choices hold on to it, so give top-level home components an id) and "showIf" (any binding; the component shows when it is truthy: a count above 0, a non-empty value, true).

App theme: "theme": { "background": "plain|tinted|gradient", "corners": "rounded|soft|square", "density": "comfortable|compact" }. The font is always the system font.

New components

Component Props
hero title, subtitle, value, unit, symbol. A big header block, filled with the tone by default
progress label, value, max, caption. A bar
ring label, value, max, unit, caption. A circular gauge
button label, symbol, action. Filled by default
buttonRow children: 2-4 buttons side by side
grid columns (2 or 3), children: any components
tabs items: 2-5 of {label, children}. A segmented control switches them
divider, spacer Spacing. spacer takes style.size

tapGrid items take a symbol. text takes variant (body caption headline title); the v0 form "style": "caption" still works.

New fields: stepper (min max step unit), slider (min max step unit, min and max required), rating (max 3-10, symbol). All three write a number.

What the owner controls on the phone (Customize, per app, saved to their account, survives new versions): name, icon (symbol or their own photo), color, background (plain, tinted, color fade, five ready-made gradients, or their own photo with a dim level), corners, spacing, and which home-screen components show and in what order. Notifications per app: off, each reminder, quiet hours, their own reminders.

The launcher icon can be an image instead of a symbol. Upload it with the MCP tool upload_asset (a square PNG or JPEG, 1024 by 1024 is ideal, opaque, the subject centered with margin, since the launcher rounds the corners), declare it under assets, and point icon.asset at that key:

"assets": { "logo": { "source": "asset:<id from upload_asset>", "alt": "Tricity, a teal scooter" } },
"icon": { "symbol": "scooter", "color": "teal", "asset": "logo" }

symbol and color stay required: they are the fallback and the color behind the image. Only use an image the owner made or has the right to use; never one found on the web. Asset ids are readable by anyone holding them, so never upload anything private. The owner can still override it on the phone (Customize: their own photo, or a symbol), and can go back to the app’s logo.

Host builds with the native widget extension can show this same app name and image in a small or medium Home Screen widget. Each instance selects one of the signed-in account’s apps through the iOS Edit Widget picker and opens that app directly inside Daiz. Owner image/symbol/name/color overrides still win. Use upload_asset and icon.asset as above; no new manifest keys are needed.

Requires iOS 17+ and a widget-enabled native build, not Expo Go or an older TestFlight binary. The person must add the widget through the iOS widget gallery; an MCP agent cannot place it. Only app identity and cached icon images reach the widget, not records, computed output or tokens. The host clears this catalog on sign-out; iOS controls when existing widget snapshots redraw. These are fixed host-provided launcher widgets, not arbitrary manifest-defined widget layouts.

Every record also has createdBy and updatedBy (user ids, set by the server). Show a name with {"format": "person", "value": {"field": "createdBy"}} (“You” for the viewer). Filter to the viewer with ["createdBy", "eq", "me"]. Compute functions receive { data, now, members }, where members is [{id, name, role}], so a compute can count per person. Reminders reach everyone on the app.

Design each app around its work. There is no required hero/stats/list template. Compose the following primitives with existing components; keep the system font and owner preferences.

Component Props
stack direction: vertical (default) or horizontal; distribution: natural, equal, between; children (1-30)
card children (1-30); styled surface with independent content layout
disclosure bound title, optional bound subtitle, expanded (default false), children
badge bound value (including a computed pill), optional symbol
icon symbol, optional accessibility label; style controls size, tone, emphasis
collection query, children repeated in each record’s scope; details below

Style adds padding and gap (none small regular large), corners (rounded soft square), border (none subtle accent), shadow (none soft), text weight (regular medium semibold bold), and align: trailing. These are named host tokens, not CSS. Gap applies to composition layouts and collections. Corners inherit through containers, unless the owner chose a corner preference, which wins. A filled composition card uses a strong tint so nested controls keep their own readable foregrounds. A filled hero/button uses solid color. Photo adds aspect (square landscape portrait wide), fit (cover contain), and bound caption. Buttons add bound disabled, a busy state, and accessible button semantics.

Top-level navigation declares 2-5 bottom tabs, each {screen, label, symbol}. Targets must exist and be distinct. The bar stays fixed on those screens and respects the device safe area. Switching tabs updates a search parameter in a persistent shell, so repeated switches do not build a back stack. Only content transitions; the bar stays mounted and fixed. Default motion is a 180 ms fade with a small vertical settle, disabled when Reduce Motion is enabled. Opening details pushes a screen; back returns to the originating area. Sheets remain dismissible. Use tabs inside an area for secondary views; do not confuse them with main navigation.

{
"type": "collection",
"query": { "from": "tasks", "sort": ["due", "asc"], "limit": 50 },
"match": { "field": "project", "value": { "field": "id" } },
"search": { "fields": ["title", "note"], "placeholder": "Find a task" },
"filters": [{ "label": "Open", "where": [["status", "eq", "open"]] }],
"sorts": [{ "label": "Newest", "sort": ["createdAt", "desc"] }],
"children": [{ "type": "card", "children": [
{ "type": "text", "value": { "field": "title" }, "variant": "headline" },
{ "type": "button", "label": "Details", "action": { "navigate": "task", "with": "{record}" } }
] }]
}

layout is list (default), grid (2 or 3 columns) or horizontal (a row that scrolls sideways). A horizontal row without columns uses 260-point cards; with columns (2 or 3) the cards narrow so that many fit plus a peek of the next, which suits short items such as a code and a status (host builds after 2026-10-16; older builds keep 260). empty accepts an empty-state component. swipe has the same delete/custom-action shape as list. Keep swipes on vertical lists to avoid competing with horizontal scrolling. Every action has a visible alternative. groupBy splits one collection into titled groups by a field: {"field": "floor", "title": "Floor {0}", "count": true}. Grouping runs after match, filters and search, so a group with no matching rows disappears, and the groups follow the query’s sort (sort by the grouped field first). The field must be text, number, enum, boolean or date; {0} in title is the value; missing names the group of rows without a value (default “Other”); count adds the number of rows. Each group uses the collection’s layout, so layout: "grid" gives a grid per group. Prefer one grouped collection with search over a section per value: a section’s collection cannot follow another collection’s search. Needs a host build after 2026-10-16; older builds show one list.

search.keyboard picks the keyboard the search field opens: text (default, the full keyboard), number (digits, for codes such as a cell number), decimal, phone or email. It needs a host build after 2026-10-16; older builds open the full keyboard. Do not build a form just to get a number pad: a form adds a submit button and an unsaved-changes prompt. Search is case-insensitive substring matching. Query conditions, relation match, search and the selected filter run before sorting and limit. No results gets a distinct search-empty state.

match.value resolves in the containing record/screen scope, while the repeated children see the queried record. A missing/null match value produces no rows. This supports project-to-task and other master/detail relationships without expressions. Use detail/edit screens for forms in repeated records: form state is screen-local, not separately scoped to each repeated item. The validator rejects repeated forms and detail screens receiving incompatible record collections.

Every display component accepts menu: [{label, action, destructive?}] (1-8 entries). A visible ellipsis button and long press open the same menu. Actions use the component’s record scope. Navigation with a record propagates through detail/edit screens and menus. Delete remains recoverable through Undo. Viewers and the guest demo cannot write, delete, undo, upload or locate.

Each form field adds showIf (a binding), help, required, and optional validation: min/max for number inputs, minLength/maxLength for text. Visible fields are checked before submit and show inline errors. Hidden fields retain their state and are not checked; explicitly decide which values your submit action writes. These are UI constraints, not replacements for server-side collection validation. required checks presence, so zero and false are valid. Defaults work inside tabs and disclosures. Number inputs retain raw editing text while accepting late-arriving defaults. The submit button exposes busy/disabled state and cannot save in a demo.

The signed-out host opens Daisy with database-backed fictional data and stored sample compute output. Navigation, search, filters, disclosures and input exploration work; mutations are blocked at the action dispatcher and form submit. Public GET /v0/demos reads curated demo_snapshots, never owner tables. No guest changes are persisted. Studio is a second demo at /demo/home?sample=studio. The host contains no sample manifests, records or asset fallbacks. Curated content lives only in D1, not repository files. Admin-only PUT /v0/admin/demos/:slug validates the complete snapshot and increments its version. Queries use its snapshotAt clock. Pull to refresh fetches the latest published samples. This publication is separate from MCP deploy/publish, which updates the owner’s private app.

Add an app from the Community library (and the Daisy showcase)

Section titled “Add an app from the Community library (and the Daisy showcase)”

When someone asks for a ready-made app, the example app or the capabilities/design showcase, use the Community library, not examples followed by a newly authored manifest. MCP browse_library lists what exists (public, no account): template slug, name, one sentence, who made it, category and clone count, most cloned first, 24 per page (q searches name and description, category filters, cursor pages). MCP clone_template then copies a listing’s stored version, server-side compute source, referenced image assets and, with data:"sample", its fictional records into an independent app owned by the authenticated requester. It is immediately live and editable. No agent generation, deploy, manual seeding or separate publish is needed.

{"template":"daisy","data":"sample","requestId":"<unique-id-for-this-clone>"}

The signed-in connection identifies the account. data defaults to empty: no sample rows, and every computed value runs once. The server picks the destination id (daisy, then daisy-2, …) and never overwrites an app. Keep the request id unchanged when retrying; a repeated request returns the original clone. If a listing is missing or not clonable, surface the error; do not reconstruct a replacement. Then read_app on the returned id to keep shaping the copy. The older install_demo ({"demo":"daisy"}, sample data, destination <slug>-demo) still works for existing agents; prefer clone_template.

Cloning is deny-by-default. Neither tool is a general app-cloning API. The only sources are Community library listings Daiz approved: frozen D1 demo_snapshots rows marked installable=1, Daiz’s own templates and apps people shared with publish_to_library after review. browse_library and capabilities.templates.available list them. Being visible as a preview does not imply clonability: Studio is preview-only.

Ordinary account apps, including shared apps with viewer/editor access, cannot be selected as installation sources. app names a new destination, never the source. Caller-supplied owner ids, source app ids or approval flags are rejected. Never work around a denied clone by reading another account’s app and redeploying its manifest or records. There is no general clone API, including for owned apps. Future cross-account cloning requires a separately designed opt-in permission; visibility, sharing and app ownership must not silently enable it for everybody.

Approval is server-controlled metadata, not a manifest field, label or account preference. New rows default to non-installable; publication preserves the existing approval. Daiz approves a person’s listing on its admin page; Daiz’s own templates through the admin-key-protected PATCH /v0/admin/demos/:slug/installation with a boolean enabled, per slug. The install transaction rechecks it at commit. Revocation stops new copies, including in-flight copies; it does not remove existing owned copies. Replaying an already committed request only returns its receipt, without copying again.

The copy preserves relationships and design, attributes imported records to its new owner and starts with notifications muted. It does not copy real people, memberships, invitations, secrets, push tokens or another account’s preferences. Edits, deletion and later template publications do not change other copies. Public demo responses never include server compute code.

The canonical source is the existing Daisy row in D1 demo_snapshots, separate from owner apps. MCP examples and recipes also read D1 app_catalog, with no bundled or generated fallback. No app manifests, server compute sources or seeds are kept in Git. Today, Health, Food and Activity stay coherent; Explore exposes additional supported components and working flows. Native device capabilities, sharing and permission checks need their real host/account setup, not fake sample identities. This is a showcase of the implemented format, not a claim of external connectors or realtime. New components need the v0.5 host bundle; installing a demo does not update TestFlight.

These additions are optional. Existing v0/v0.2/v0.3 manifests still render. Rebuild the host bundle before trying a v0.4 draft; a Worker deployment cannot update the renderer on a phone.

theme.colors optionally declares brand, canvas, surface, soft, text, and muted. Each supplied role must contain both light and dark, each an opaque six-digit hex color. These are literal color values, not CSS. Unspecified roles keep system defaults.

"theme": {
"colors": {
"brand": { "light": "#D92959", "dark": "#FF8FA9" },
"canvas": { "light": "#FCF8F3", "dark": "#19171B" }
}
}

theme.appearance optionally selects light (default), dark or system (follow the iPhone). Apps are light unless the owner explicitly asks for dark or for following the system: leave it out or set light otherwise; prefer writing light, since host builds before 2026-10-16 follow the system when it is missing. Still give every color pair a dark value for owners who switch. The owner’s appearance preference wins. Daisy uses light with a white canvas. The owner’s accent replaces brand; their background and corners still win. Brand also becomes the default component accent when not overridden. style.foreground on text accepts text, muted, brand, or onBrand. The host derives onBrand as dark or white text for contrast. Use onBrand for text inside brand regions; foreground is not implicitly inherited through nested cards. Authors must check contrast for their custom text/surface pairs in both modes.

screen.layout is standard (existing padded layout) or edgeToEdge (no outer spacing). An edge-to-edge screen composes its own spacing using regions. Content stays within a centered 680-point maximum width on larger displays. screen.header is native (default) or hidden. Only start screens and primary tabs may hide the native header; detail screens retain back. The host still owns safe-area insets, scrolling and the fixed bottom navigation.

Component Fields Behavior
region background: color role, default surface; edge: flat/rounded/curve; children (1-30) Own background, padding and gap; edge shapes its bottom corners. Curve is a host-sized rounded end, not an arbitrary path. Owner corner choices override the shape radius
overlap exactly two children; anchor: leading/center/trailing; amount: small/regular/large The second child overlaps the bottom of the first by 20/32/48 points, remains in document flow and reserves its remaining height. The first child must leave enough space below its content
media asset: declared asset key; size: small/regular/large/hero; shape: none/rounded/circle; fit: contain/cover 48/88/152/230-point square frame, constrained by available width. Defaults: large, none, contain. Uses asset alt text and a labeled fallback if loading fails
meter label, value: binding; min (default 0), max; segments: 2-8 {until,tone,label}; optional caption: binding Ordered colored intervals and a clamped marker. Endpoints increase and end at max; max exceeds min. Missing or nonnumeric data shows “No data yet”, not zero. Accessible value includes number and segment label

Regions and overlaps use the normal child tree: bindings, record scope, menus, conditional visibility and form discovery work inside them. No absolute coordinates, arbitrary paths or code are accepted. A meter’s segment tones use the existing palette. Use textual labels as well as colors, and do not present a journal milestone as a medical assessment.

"assets": {
"companion": {
"source": "asset:509bc43f32b3465d2be4201b7a1f4826",
"alt": "An illustrated golden dog sitting calmly with a coral scarf"
}
}

assets maps keys to {source,alt}. Only asset: followed by a 32-character lowercase hex id is accepted; no remote URLs, inline SVG, data URLs or executable assets. alt is required. Upload with MCP upload_asset({data: <base64>, mime: "image/png"}), then use its id. The limit is 1.5 MB decoded. PNG preserves transparency; JPEG does not. The existing asset endpoint also accepts JPEG, WebP and HEIC. Prefer PNG for transparent illustrations and test device decoding. Asset ids are unguessable but publicly readable: never upload secrets or private documents. Keep the original in the repository and record its origin. Assets in a disposable account are removed when that account is removed; do not use those ids in durable examples.

Daisy’s guest illustration loads through its public asset id, just like the account app. The photo picker also has an internal preserveAlpha option; regular photo uploads keep their existing JPEG behavior. A visual asset-management editor is not implemented yet.

This is an additive revision of microapp/0, not a new executable format. New components and actions need a rebuilt native host. Deploying the Worker updates the MCP and server only, not an installed TestFlight build. Existing deployed manifests and the frozen v0 fixture remain valid.

Call the public MCP tools capabilities, recipes, and guide before authoring. recipes returns seven complete, valid starter manifests: tracker, workspace, directory, workflow, journal, booking, dashboard. They are starting points, not mandatory hero/stat/list layouts. Choose a recipe by the job, then compose the native catalog. theme.recipe records that intent; it does not generate screens or restrict composition.

theme.preset selects neutral, warm, vivid, or professional. Each has coordinated light and dark palettes. Existing theme properties and explicit color pairs still work. Additional semantic roles are elevated, onBrand, positive, warning, danger, info, border, focus. The renderer derives readable onBrand, and adjusts text/status colors for contrast after theme and owner preferences are combined. Owner appearance, accent, background, spacing and corners win.

The baseline is system type, comfortable spacing, clear content hierarchy, 44-point action targets, a maximum 680-point content column, meaningful empty states and one dominant action per region. Avoid deeply nested cards and long ungrouped forms. Charts offer a Show values numeric table, so color and shape are not the only way to read them. These defaults are not a substitute for testing Dynamic Type, VoiceOver, RTL, safe areas and keyboards on an iPhone.

For character (the default Daiz pastel palette, never the Daiz lime, specific symbols, a companion animal, original images and one interesting element per screen), read the public MCP tool design_guide (docs/DESIGN-PLAYBOOK.md). It adds art direction only; this file stays the format reference.

Run design_lint({manifest}) before deploy. It reports contrast errors, missing empty states, overloaded actions, nested cards and long forms, and layout mistakes: search built as a form with a submit button, a match collection that is empty until something is typed, sideways cards without columns, more than five sections repeating one collection, the start screen not being the first tab, and a filled delete button. Each finding has a path and a fix. validate also returns design findings. Structural validation blocks invalid manifests; design findings are guidance, not a blanket ban on custom composition.

email is a validated address; phone is text (not country-specific validation); money is a finite number in the app’s chosen currency units; duration is a nonnegative number in units the app must label consistently. multiEnum is a unique string array from values; refs is a unique array of record ids with to naming its collection. Single ref continues to use to. money and duration can feed sum/average bindings. Use currency:ILS or another supported currency formatter for display; use percent for percentage units (25 displays as 25%).

Fields accept required, optional, default, unique, min, max, minLength, maxLength. Fields are required unless optional; required:true and optional:true conflict. Defaults are validated. Numeric bounds apply to numbers, length bounds to strings/arrays. Empty required values, duplicate choices, invalid calendar dates and nonfinite numbers are rejected server-side.

Relations enforce target existence in the same app. onDelete is restrict (default), nullify, or cascade. A nullified single ref must be optional. Cascades and nullification obey the same permissions, command write allowlist and transaction limits as direct writes. unique is checked against the complete app snapshot, not only visible rows, before the atomic commit.

Display related data without duplicating it:

{"lookup":{"collection":"projects","record":{"field":"project"},"field":"name"}}

Lookups use already-authorized downloaded records; a missing or inaccessible target is empty.

Legacy where remains an AND of conditions. Add filter with nested all, any, or not (maximum depth 8). in and notIn also accept literal lists. contains supports text or array membership, startsWith supports text, and isEmpty takes a boolean. Text searches ignore case. orderBy accepts up to five [field,"asc"|"desc"] entries; it takes precedence over legacy sort. Record id breaks ties deterministically.

{"from":"tasks","filter":{"all":[["status","notIn",["done"]],{"any":[["title","contains","plan"],["due","in","today"]]}]},"orderBy":[["due","asc"],["title","asc"]],"pageSize":20}

HTTP POST /v0/apps/:app/query/:collection accepts the query fields without from. MCP read_data returns its legacy array unless pageSize or cursor is supplied, when it returns {rows,total,nextCursor}. Page size is 1-100. Pass nextCursor back unchanged with the same query. Cursors are keyset navigation data, not authorization; permissions are applied before paging. Changing filter/order invalidates the cursor. When supplied, limit is at most 500. Native collection pageSize pages its authorized synchronized data locally, after search/filter. It is not a server-streamed infinite dataset. Grouped chart rows belong in server compute.

Built-in roles are owner, editor, viewer. Declare additional role names in top-level roles. The owner can assign a declared role with MCP set_member_role. An owner always has full access; a viewer can never write. Other members use each collection’s optional access policies:

"access": {
"read": {"own":"createdBy"},
"create": {"roles":["editor","coordinator"]},
"update": {"own":"createdBy"},
"delete": {"roles":["coordinator"]}
}

An omitted read policy allows reads; omitted write policies allow writes for non-viewers. A policy combines its role allowlist and own field check. Use createdBy for server-controlled ownership. These rules apply on HTTP, MCP, commands, restore and sync, not only disabled controls. For restricted members, compute receives only readable records and cached owner aggregates are not reused. Computed reminders are suppressed for restricted members to avoid leaking owner-wide totals. A custom role removed from the manifest loses read/write access until the owner assigns a current role.

A workflow names an enum field and a set of allowed edges:

"workflow":{"field":"status","transitions":[
{"id":"complete","label":"Complete","from":["active"],"to":"done","roles":["editor","coordinator"]}
]}

{"transition":"complete"} acts on the current record. workflowActions renders allowed next actions. All writes, including ordinary updates and commands, check workflow edges and roles. Owners bypass role restrictions but still obey valid state edges. Members must insert workflow records in the enum field’s default state, or the first transition’s first from state when no default is declared. Owners may seed historical states. HTTP transitions use POST /v0/apps/:app/records/:collection/:id/transitions/:transition; MCP uses transition_record. Both return {record,requestId,revision,replayed}.

Declare a command and export a same-named server function from the compute file. Input uses the field schema; writes is the explicit collection allowlist; returns uses the compute return kinds. Optional roles restricts callers. Commands cannot share names with computed functions.

"commands":{"addProject":{
"input":{"name":{"type":"text","maxLength":120}},
"writes":["projects","tasks"],
"returns":{"project":"text","message":"text"}
}}
export async function addProject({data, input}) {
const project = await data.insert("projects", {name: input.name});
await data.insert("tasks", {title: "First task", project: project.id});
return {project: project.id, message: "Project created"};
}

Command context includes data, input, now, actor (id, role), and members. data.list/data.get are read operations; data.insert(collection,values), data.update(collection,id,values), and data.delete(collection,id) stage writes. Later reads see earlier staged writes. The entire command commits once or not at all, including activity and idempotency bookkeeping. Returning the wrong declared shape also rejects the transaction. Catching an invalid write inside command code does not allow a partially staged transaction to commit. Read-only computed functions cannot write. The sandbox has no network, secrets, Intl, filesystem or phone APIs.

Limits: 100 staged changes, 1 MiB transaction/audit payload, 128 KiB command result, 64 KiB source, 32 MiB sandbox memory and 2-second execution budget. Bulk SQL keeps the commit at six D1 statements rather than two per record. A client action sequence is still sequential and is NOT an atomic transaction. Use one command for operations that must succeed or fail together.

Call from a screen:

{"run":"addProject","with":{"name":{"state":"name"}},"into":"state.result"}

{"state":"result.message"} reads the result after success. HTTP is POST /v0/apps/:app/commands/:name; MCP is run_command. Supply input and a stable requestId. Same actor, app, request id and input replays the original result without repeating writes; reusing an id with different input, role or manifest version returns 409. For members with read restrictions, a changed app revision also blocks cached output replay to protect rows that may no longer be visible. A replay blocked by these changes, including a retired draft, returns details.committed:true when that request id already committed. This confirms the earlier operation, not a newly submitted payload; refresh instead of submitting it again under a new id. The ledger lasts until account deletion. Commands return {value,requestId,revision,replayed}. revision is the app-wide change counter, separate from a record’s version. Ordinary writes, workflow transitions and restore also support request ids. Record updates, deletes and transitions accept expectedVersion; a stale version returns 409. Host writes also carry manifestVersion to avoid accidentally applying a draft’s schema to a live screen. An owner may select the current live or draft version; members may select only live. Without it, writes use live when published, otherwise draft. Restore uses POST /v0/trash/:app/:collection/:id/restore and returns {restored,record,requestId,revision,replayed}. GET /v0/apps/:app/activity and MCP activity return authorized event summaries with id, actor, collection, record, operation, and at, without historical record contents.

automations schedules declared commands on the live version:

"automations":[{"id":"dailyRollup","command":"rollup","input":{},"schedule":"daily 21:00","enabled":true}]

Schedules use the existing daily/weekly format in the owner’s time zone and the five-minute Worker cron, running with the owner’s permissions. A schedule-slot request id prevents duplicate successful execution. Missed slots are eligible for 30 minutes; they are not replayed indefinitely after an outage. Failures are recorded in reports. Automations run on the live version; every MCP deploy makes its version live (since 2026-10-16), so a draft-only version exists only in older rows. These are server schedules, not exact-time phone alarms or webhooks.

Addition Contract
select, multiSelect label, bind, options pairs; multiSelect stores a string array. Both support defaults
recordPicker, multiRecordPicker label, bind, query, labelField, optional searchFields; return one id or an id array. Choices are limited to readable records
textField.input text, email, phone; email also validates locally
form.steps 2-10 {title,fields:["state.name",...]} groups. Every field appears exactly once. Continue validates the current step; submit validates the complete visible form
form.draft Enables account/app/screen-scoped persisted draft values in the native host. Failed writes keep entered values
collection.selectable Enables row selection. bulkActions:[{label,command}] invokes a command with ids:{selected:true}; input must accept refs
collection.persist Remembers search/filter/sort with a stable component id. pageSize is 1-100
calendar query, date field date, title field title, optional row action; month navigation plus a selected-day agenda
kanban query, enum field, title field title, columns:[{value,label}], optional action; explicit workflow actions, not drag-and-drop
timeline query, date, title, optional subtitle and action
gallery query, photo field photo, optional title field title and action
detail fields:[{label,value}] using ordinary bindings
activity optional limit; shows authorized recent record activity, not an owner-wide private log
workflowActions optional label; current record scope required
chart.seriesFields up to four {field,label,color?} measures for bar/line/area; donut uses only y. Negative ranges and missing observations remain distinct
share action {"share":{"title":B,"text":B}} opens the native share sheet; it does not send content automatically

Native host pending writes persist across app restarts and are account-scoped. A transport failure can retry using the same request id. Validation errors and version conflicts pause, with explicit retry/discard or keep-local/use-server choices. Pending is not server-confirmed success. A queued command does not run compute offline, and later success/navigation actions do not run early. Draft edits retain the record version from when editing began, so a background refresh cannot silently overwrite someone else’s update. Native Back and tab changes warn about unsaved edits. Scroll, selected sub-tabs and saved view state persist; independent native navigation stacks per primary tab are not implemented. The separate browser preview does not own the native offline queue.

The D1 example/workbench entry exercises the combined flow with fictional data; fetch its manifest and compute with MCP examples. Seed data stays private to the admin test catalog. scripts/platform-smoke.mjs checks the Worker/MCP in a disposable account; do not seed Dor’s data.

Arbitrary expressions, CSS, downloaded code, custom executable components, outbound network in compute, external connectors and secret provisioning, custom fonts, offline compute, private file attachments, field-level permissions, realtime subscriptions, drag-and-drop boards and custom widget layouts. Photo/design assets remain publicly readable opaque ids, not a private file store. These capabilities require separate security and native-runtime work; do not simulate them with unvalidated URLs or advertise them in generated apps.