Docs/Reference/Flowlet catalog
Reference

Flowlet catalog

The complete flowlet definition — every component, action, chart shape, expression, watch and source, plus the guarantees the system fills in on its own.

This is the exhaustive reference for a flowlet definition: the declarative document the agent authors for a live mini-screen. For the concepts and how to ask for one, see the Flowlets feature guide. You never write this by hand — it's here so you can see exactly what a flowlet can express.

Current catalog version: 3. A definition carries a catalog number; a client renders anything it understands and gracefully placeholders the rest, so a higher catalog is always forward-compatible.

Definition shape

{
  "catalog": 3,
  "name": "Water",
  "icon": "droplet",
  "accent": "#00A6C8",
  "state":    { "...": "..." },
  "series":   { "...": "..." },
  "computed": { "...": "..." },
  "layout":   [ "...components..." ],
  "screens":  { "...": "..." },
  "watches":  [ "..." ],
  "sources":  [ "..." ]
}
KeyRequiredPurpose
catalogyesCatalog version (integer, ≤ 3).
nameyesTitle, ≤ 100 chars.
iconnoIcon name for the tile.
accentnoHex colour (#00A6C8 or #0AC).
stateno{key: spec} — what the flowlet remembers.
seriesno{name: {unit?}} — append-only event logs.
computedno{key: spec} — derived values.
layoutyesArray of components — the screen.
screensno{id: {title?, layout}} — drill-down pages (≤ 6).
watchesnoReactive reminders (≤ 20).
sourcesnoLive data bindings (≤ 8).

State

state maps a key to a typed cell with a default.

TypeValueNotes
numbera numberOptional min / max clamp.
stringtextOptional maxLength. A date is stored as a "YYYY-MM-DD" string.
booltrue/false
timer{running, elapsed}A live stopwatch; toggled with timer_toggle.
listarray of rowsThe living rows of a repeater.

(There is no top-level date state type — a stored date is a string. date is a list item-field type and a date input component.)

A list declares an item schema{field: type} with up to 8 fields of string / number / bool / date / image (the id field is reserved and auto-assigned). A list caps at 200 rows ("max": N to lower it). A list marked "source": true is owned by a data source and is read-only in the UI.

"state": {
  "goal_ml":   { "type": "number", "default": 2000 },
  "expenses":  { "type": "list", "max": 200, "item": {
      "title": "string", "amount": "number", "category": "string",
      "date": "date", "receipt": "image" } }
}

Series

An append-only event log. Charts and windowed aggregates read from it; you add to it with the log action.

"series": { "water": { "unit": "ml" } }

Computed

Derived values, resolved on every render in dependency order. Exactly one form per key:

Aggregate a series — over a time window:

"today_ml": { "series": "water", "agg": "sum", "window": "today" }

Aggregate a list — with an optional per-row filter:

"month_total": { "list": "expenses", "agg": "sum", "field": "amount",
                 "where": "days_since(date) < 30" }

Expression — safe arithmetic / comparison over other values:

"remaining": { "expr": "max(0, monthly_budget - month_total)" }

Conditional text — first matching when wins:

"status": { "cases": [
    { "when": "month_total > monthly_budget", "text": "Over by {over} ₺" },
    { "when": "month_total == 0", "text": "No spending yet" } ],
  "else": "{remaining} ₺ left this month" }
  • aggsum count avg min max last for a series; a list aggregate excludes last (sum/count/avg/min/max).
  • windowtoday 7d 30d 90d all.

Components

49 component types in four groups. Every component may carry an id (required when it has an action, auto-assigned if you forget) and a visibleWhen expression.

Layout (8)

TypePropsNotes
cardchildrenThe app's card surface.
rowchildrenHorizontal.
columnchildrenVertical.
gridchildren, columnsEqual columns; a chart-bearing grid is forced full-width.
listchildrenDivided rows.
dividerA hairline.
spacersizeVertical gap.
repeatersource, item, empty?, navigate?, where?, sortBy?One item per row of a list. See Dynamic lists.

Display (23)

TypeProps
headertext, subtitle?
texttext (interpolates {key})
badgetext
iconname
statvalue (bind), label?
metricvalue, unit?, label?, delta?, deltaLabel?, invert?
progressvalue, max
ringvalue, max, label?
gaugevalue, min, max, label?
chartdata, kind — see Charts
sparklinedata
heatmapdata
tablestatic rows, or a data-bound source + columns — see Tables
statustext, tone? (ok / warn / bad / neutral)
keyvaluerows: [{label, value}]
timelineevents: [{title, time?, tone?}] (tone done / now / wait)
callouttext, tone? (info / success / warn / bad), icon?
codetext, language?
linktext, url
imagesrc (a URL, a data: URI, a $.field ref, or a state key), height?, alt?
clockseconds?
countdowntarget (epoch ms or "YYYY-MM-DD[THH:MM]", read in local time)
timervalue (a timer state key)

A value/max/min bind is a number literal or the name of a state/computed key. An icon (on icon / icon_button / checklist items) is a name from a vetted set; an unknown name degrades to a sensible fallback.

Input (15)

TypePropsFires
buttontext, style? (primary/secondary/ghost/destructive), actionits action
icon_buttonicon, actionits action
inputaction (set/item_update/item_add), value?, placeholder?, label?, maxLength?on Enter / add button
number_inputas input, plus min? / max?as input
textareaas input, multiline, rows?on submit / add
selectoptions, action, placeholder?on pick
dateactionon pick
segmentedoptions, actionon pick
togglevalue, action, label?on flip
checklistitems: [{key, label?, icon?}]toggles the tapped item's key
steppervalue, action (increment), label?, min? / max?on +/−
slidermin, max, step?, value, action, label?on release
ratingmax?, value, actionon tap
searchtarget (a repeater/table id), fields?, placeholder?filters client-side
photoaction (vision)captures an image → a new row

options (on select / segmented) is an array of strings, or of {value, label} objects. Interactive controls reflect a tap optimistically (the UI updates the instant you tap and reconciles when the authoritative value lands), so nothing feels laggy on a slow link.

Composites (3)

Catalog-3 semantic components. The agent states intent; the system expands each to primitives before rendering, and owns the layout and wiring. Old clients render the expansion unchanged.

list_row — a repeater's item. Every prop but title is optional; a bare $.field interpolates, a {$.field} unit template is kept, a plain word is a literal.

{ "type": "list_row",
  "thumb": "$.receipt", "title": "$.title", "subtitle": "$.merchant",
  "badge": "$.category", "value": "{$.amount} ₺" }

form — a multi-field entry card that adds a row into a list. Each field names a field of the into list; the control type is derived (string→input, number→number_input, date→date, bool→toggle, options→a picker). A date defaulting to "today" pre-fills today. Submit adds the row and clears the form.

{ "type": "form", "id": "addExpense", "into": "expenses", "title": "Add expense",
  "fields": [
    { "field": "title", "label": "What" },
    { "field": "amount", "label": "Amount" },
    { "field": "category", "options": ["Food", "Bills", "Other"] },
    { "field": "date", "default": "today" } ],
  "submit": { "label": "Add" } }

tracker_card — a headline metric + a list-backed chart. field (a number field) and agg (default sum) drive the metric; chart (bar/line/area/pie/donut) draws the same data; window scopes both. Pie/donut groups by a category-like field (or an explicit by).

{ "type": "tracker_card", "id": "spend", "list": "expenses", "field": "amount",
  "title": "This month", "window": "30d", "chart": "bar" }

Icons

An icon (on icon / icon_button, and checklist items) is a name from this vetted set — an unknown name degrades to a sensible fallback, so these always render:

activity · arrow-down · arrow-up · bed · bell · book · brain · calendar · camera · cart · check · clock · cloud · coffee · cup · droplet · dumbbell · flame · heart · leaf · mail · minus · moon · music · pen · phone · pill · plus · run · smile · sparkles · star · sun · target · trash · trophy · undo · walk · wallet · zap

Actions

Every interactive component declares one action object with an op. Any action may also carry "once": "day" | "week" | true — a server-side latch that lets the action fire at most once in that window; a repeat tap is a silent no-op.

OpShapeEffect
set{op, key}Set a state key to the supplied value.
increment / decrement{op, key, by?}Step a number by by (default 1). A stepper supplies only the sign.
toggle{op, key}Flip a bool.
log{op, series, value?, category?}Append an event.
remove_last{op, series}Undo the last logged event.
reset{op, key} or {op, series}Reset a key to default / clear a series.
timer_toggle{op, key}Start/pause a timer state key.
item_add{op, key, fields?} or {op, key, item?}Add a row. fields = a {field: template} map; item = a fixed {field: value} map.
item_update{op, key, field} or {op, key, fields}Edit one field (or several) of the tapped row.
item_toggle{op, key, field}Flip a bool field of the tapped row.
item_remove{op, key}Delete the tapped row.
item_move{op, key}Reorder the tapped row.
batch{op, ops: [...]}Up to 20 ops at once; not nested.
vision{op, prompt, into}Read a captured photo into a new list row.
agent{op, message}Hand a message (≤ 2000 chars, templated with {value}/{key}) to the model — the only op that reaches it.

item_add field templates

item_add with a fields map builds one row from several inputs. {value} is the tapped input's value; {state_key} pulls a live value; a lone {token} keeps its type (a number stays numeric); today on a date field becomes the current date. (A form composite generates all of this for you.)

Dynamic lists

A list state key holds living rows; a repeater renders one item template per row.

{ "type": "repeater", "source": "tasks", "empty": "No tasks yet",
  "navigate": "taskDetail",
  "item": { "type": "list_row", "title": "$.title" } }
PropPurpose
sourcethe list state key (required)
itemthe row template — a list_row, or any component tree (required)
emptytext shown when the list is empty
navigatea screens id — tapping a row opens that detail screen, scoped to the row (screens are one level deep — a screen can't itself navigate)
wherea per-row filter expression (over the row's fields)
sortBy{field, dir}dir is asc / desc

Inside the item, $.field binds a prop to the current row and {$.field} interpolates it. Row actions — item_update / item_toggle / item_remove / item_move — live inside the template; item_add can sit anywhere. Every user-owned list row is guaranteed editable: if you author no detail screen, one with the right edit control per field is synthesized. Rows are swipe-to-delete.

Tables

A table is either static rows or a data-bound list.

{ "type": "table", "source": "prs",
  "columns": [ { "field": "title", "label": "Title" },
               { "field": "n", "label": "#", "align": "right", "width": 48 } ],
  "sortBy": { "field": "n", "dir": "desc" }, "navigate": "prDetail" }
  • Static: rows — an array of cell arrays.
  • Data-bound: source (a list key) + columns (≤ 6), each {field, label?, align?, width?} (align = left/center/right). Optional sortBy: {field, dir}, where, navigate, empty. Tapping a header re-sorts client-side.

Charts

A chart's data object has one of these shapes; kind picks the drawing.

Shapedatakind
Single time series{series, agg?, bucket?, window?}bar / line / area
Multi-series overlay{series: [{key, label?, color?}], stacked?}bar / line / area
Category breakdown{series, by: "category", agg?}pie (+ donut: true)
List-backed time{list, field?, date?, bucket?, window?}bar / line / area
List-backed category{list, by: "<field>", field?, agg?}pie / donut
Scatter{list, x, y}scatter
  • buckethour day week; windowtoday 7d 30d 90d all.
  • A category breakdown's agg is sum or count only.
  • A list-backed chart aggregates the list's rows directly, so it stays correct through every add, edit, delete and photo capture — no parallel series to drift. Prefer it (or a tracker_card) whenever the data lives in a list.
  • sparkline and heatmap take the single-series data shape.

Expressions

Used in computed.expr, visibleWhen, cases[].when, and a list where. A tiny, safe grammar — no attribute access, no calls outside the whitelist, no names outside the resolved values.

  • Operators: + - * / % ** //, comparisons < <= > >= == !=, and and / or / not. Comparisons evaluate to 1.0 / 0.0.
  • Functions: min max abs round floor ceil; and date helpers now(), weekday() (0=Mon), days_until("YYYY-MM-DD"), days_since("YYYY-MM-DD") (a literal or a date key).
  • Names resolve against state + computed keys; inside a list where, against the row's fields.

Interpolation & binding

FormWhereResolves to
{key}any display text/labelthe value of a state/computed key
$.fielda prop inside a repeater itemthe current row's field (literal)
{$.field}text inside a repeater itemthe current row's field, formatted

Watches

watches is an array of reactive reminders the flowlet evaluates itself — no scheduled model turn. Each rule has a stable id, a trigger (one of four kinds), a notify, and an optional cooldown.

"watches": [ {
  "id": "drink_reminder", "trigger": "condition",
  "when": "today_ml < goal_ml", "after": "18:00",
  "cooldownMinutes": 120,
  "notify": { "title": "Water", "body": "{today_ml}/{goal_ml} ml today" }
} ]
triggerFires whenFields
schedulea time of day / an interval / given daysat: "21:00", everyMinutes, days: ["mon",…]
conditiona when expression goes true (edge-triggered)when, optional after: "HH:MM" (only fire past that time of day)
goala when first becomes true (once)when
stalenothing changed for a whileidleMinutes
  • notify = { title, body?, compose? }. Set compose: true to have the agent write the reminder text from the live numbers at fire time.
  • cooldownMinutes (optional) throttles re-fires; once: true (optional) fires the whole watch at most once ever; days: [...] may narrow any trigger.
  • also = { op: "agent", message } — the only side-effect a watch may trigger beyond a notification; runs an agent turn on fire (heavily throttled).

Sources

sources is an object {name: {…}} binding a list to live external data. The agent fetches on a schedule (or on pull-to-refresh) and writes the result into the list.

"sources": {
  "commits": { "kind": "agent",
               "prompt": "the last 10 commits to {repo} in the past hour",
               "into": "commits", "refresh": "1h", "limit": 10 }
}
  • kind — currently "agent": a normal turn of yours (with your tools) runs as a self-prompt on a schedule, isolated from chat.
  • prompt — what to fetch, templated with {key} live values.
  • into — the source-owned list. A source-owned list is read-only in the UI (the snapshot is replaced each refresh, not edited).
  • refresh? — a cadence like "15m" / "1h" (omit for manual / pull-to-refresh); minimum 10 minutes.
  • limit? — cap the rows fetched. prompt is capped at 1000 characters.

Serve-time guarantees

Some correctness is filled in by the system when a flowlet is served, never left to whether the agent remembered it. These transforms are deterministic, idempotent, and never change what's stored:

  • Forgotten ids are assigned — a control that needs an id gets a deterministic one, so nothing is rejected and no chart silently renders empty.
  • Composites expand to primitives, so every client (old or new) renders the same layout.
  • Every user-owned list row is editable — if the agent authored no edit screen, one is synthesized with the right controls per field.
  • Every stored photo is shown — an image field gets a row thumbnail and a full photo on its detail screen.
  • A chart-bearing multi-column grid is forced full-width, because charts don't fit side by side on a phone.

The flowlet tool

The agent authors and maintains flowlets through one tool. You never call it — you ask in plain language — but these are the operations behind the scenes:

ActionDoes
createBuild a flowlet from a definition.
updateReplace a flowlet's definition (versioned), or set pinned.
getThe definition and current live values (answers "how much today?").
listEvery flowlet with its live values.
deleteRemove a flowlet.
logAppend an event to a series (from chat: "I drank 500 ml").
set_stateChange a state value ("make my goal 3 liters").
queryOne aggregated number from a series, without dumping the flowlet.
notifyFire a one-off reminder now (recurring/conditional → use a watch).

On create/update the tool returns a review — deterministic lint findings plus a preview resolved against sample rows — which the agent reads and acts on, so a generated flowlet tends to come out right the first time.

A complete example

A daily-habit tracker — checklist, a once-per-day "complete" button, a weekly total, a chart, and a 9pm reminder — showing how the pieces fit:

{
  "catalog": 3, "name": "Habits", "icon": "check", "accent": "#7C6FF0",
  "state": {
    "read_done": { "type": "bool", "default": false },
    "code_done": { "type": "bool", "default": false }
  },
  "series": { "days": {} },
  "computed": {
    "both_done":    { "expr": "read_done > 0 and code_done > 0" },
    "weekly_total": { "series": "days", "agg": "sum", "window": "7d" }
  },
  "layout": [
    { "id": "habits", "type": "checklist", "items": [
      { "key": "read_done", "label": "Read 1 hour", "icon": "book" },
      { "key": "code_done", "label": "Code 5 hours", "icon": "zap" } ] },
    { "id": "complete_day", "type": "button", "text": "Complete the day",
      "style": "primary", "visibleWhen": "both_done > 0",
      "action": { "op": "batch", "once": "day", "ops": [
        { "op": "log", "series": "days", "value": 1 },
        { "op": "reset", "key": "read_done" },
        { "op": "reset", "key": "code_done" } ] } },
    { "type": "metric", "value": "weekly_total", "label": "This week" },
    { "id": "trend", "type": "chart", "kind": "bar",
      "data": { "series": "days", "agg": "sum", "bucket": "day", "window": "7d" } }
  ],
  "watches": [ {
    "id": "evening_nudge", "trigger": "schedule", "at": "21:00",
    "notify": { "title": "Habits", "body": "Did you read and code today?" }
  } ]
}

Limits

BoundValue
Definition size64 KB
Components200
Nesting depth8
State keys / computed50 each
Series20
List rows200
Item fields8
Drill screens6
Chart series (overlay)4 · pie slices 8
Table columns6
Watches20 · Sources 8
Photo capturesrate-limited per flowlet + globally
agent tapsrate-limited per flowlet + globally

See also