Manifest format
The idea in one paragraph
Section titled “The idea in one paragraph”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).
Principles
Section titled “Principles”- 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)) - 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.
- 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.
- Data outlives code. Collections are declared; changing a screen never touches data. Removing a field hides it, it does not delete it.
- 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.
File structure
Section titled “File structure”{ "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
Section titled “Collections”"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.
Bindings (the only way to show data)
Section titled “Bindings (the only way to show data)”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.
Components (v0 catalog, 16)
Section titled “Components (v0 catalog, 16)”| 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 |
Actions (closed set)
Section titled “Actions (closed set)”| 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 |
Compute (server only)
Section titled “Compute (server only)”<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 (push)
Section titled “Reminders (push)”"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.
What the validator checks
Section titled “What the validator checks”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.
v0.2 additions (2026-10-14)
Section titled “v0.2 additions (2026-10-14)”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.
App logo (2026-10-14)
Section titled “App logo (2026-10-14)”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.
iOS Home Screen app widget
Section titled “iOS Home Screen app widget”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.
Shared apps (2026-10-14)
Section titled “Shared apps (2026-10-14)”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.
v0.3: composition and interaction
Section titled “v0.3: composition and interaction”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.
Main navigation
Section titled “Main navigation”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.
Custom record collections
Section titled “Custom record collections”{ "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.
Record menus and forms
Section titled “Record menus and forms”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.
Guest demo
Section titled “Guest 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.
v0.4: visual foundations
Section titled “v0.4: visual foundations”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.
Semantic colors
Section titled “Semantic colors”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 composition
Section titled “Screen composition”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.
Design assets
Section titled “Design assets”"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.
v0.5 application platform (2026-10-14)
Section titled “v0.5 application platform (2026-10-14)”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.
Design defaults and agent workflow
Section titled “Design defaults and agent workflow”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.
Data types, validation and relations
Section titled “Data types, validation and relations”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.
Queries and paging
Section titled “Queries and paging”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.
Roles, row access and workflows
Section titled “Roles, row access and workflows”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}.
Commands, atomicity and scheduled writes
Section titled “Commands, atomicity and scheduled writes”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.
Native forms, views and interaction
Section titled “Native forms, views and interaction”| 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.
Deliberately outside the current format
Section titled “Deliberately outside the current format”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.