Numable authoring docsthe same content is available in the terminal: `numable docs <topic>`中文

workflow — the authoring sequence and the rules you cannot break

Audience: the person building a tool, and the AI working on their behalf. Both read this same page. Read this chapter first — it defines the order in which a tool gets built and the lines you must not cross. For what is possible at all, see numable docs capabilities; for individual file formats, see the layout / xwidget / df / rcn chapters.

What a tool is

A tool (an XBundle package) is a folder. Everything in it is plain JSON / HTML — no build output, no compile step. The App treats the workspace folder as its root: write the files and the package shows up in the App; change one character and a re-render picks it up.

The smallest package looks like this (the starter produced by numable init):

my-source/
  manifest.json                # identity and metadata: id (ULID) / version / title / category / network / i18n
  xWidget/
    clock.xwidget              # one widget declaration: size, instance parameters, data binding, refresh cadence, click behavior
    rc/clock.rcn               # the drawing for that widget (geometry + color + text, rasterized to one bitmap)
    flow/clock.df              # the data flow for that widget (request + processing, emits the keys the .rcn uses)
  page/
    router.json                # page route table (optional: no pages means no page/ folder)
    html/home/index.html       # the detail page the widget opens
  .numable/                    # local fixtures and output, never shipped in the package (numable init adds it to .gitignore)

Four kinds of files, one job each, and they do not overlap:

File Owns Does not own
manifest.json package identity, store front, network allowlist, credential declarations, base language no business data at all
.xwidget ties drawing + data + size + refresh + click together, and gives the default instance parameters no drawing, no data logic
.df fetching and processing: request, parse, derive, then emit a set of keys through resultFilter never touches UI, never reads the theme (it may read ${@app.language} and carry its own i18n table, see numable docs i18n)
.rcn drawing: renders one widget from the keys the .df emitted never makes requests, never does heavy computation

Two more kinds are optional: detail pages under page/ (html / xpage / form page types, see numable docs page), and .af action flows (clicks, parameter write back, refresh, see numable docs af).

The authoring sequence

Seven steps. Every step has a command that verifies it — do not skip ahead. Jumping straight to drawing the widget and skipping step 2 is the single most common cause of rework: you finish the widget, then discover the value paths were wrong and the geometry and emptiness logic all have to be rewritten.

1. Clone to start

numable init my-source
numable init my-source --from ../an-existing-package

Clone from the template or from a package you already have; never hand-build a folder. The CLI generates the identity id (a 26-character ULID) and writes it into manifest.id / manifest.domain, and the folder layout and file extensions come out correct. ✓ 新包 … id=… means it worked.

2. Data first, widget second

Write the .df first and get the data flowing:

numable run my-source --flow clock --full

--full prints the complete output. The keys you get back are the entire set of variables available on the widget — including the flag you will use to test for emptiness, the time anchor you will display, and the fixed-length array that drives a line chart. The list of key names this step produces is the input to step 3.

✓ clock {...} with the key names you expected means it worked; on ✗, fix the flow before going further. run enforces the network allowlist and prints · 网络白名单已强制: [...] — a host missing here is a host blocked at exactly the same place on a real device.

3. Draw the widget

Write the .rcn and render it:

numable render my-source --widget clock

Output lands under my-source/.numable/render/: one PNG per widget × light / dark / empty state × language, plus an index.html contact sheet. render runs run first, and any widget whose fetch failed is rendered with empty data — so the empty-state image is not decoration, it is exactly what users see on the day the source goes down. When you are only tuning layout and do not want a round of network calls every time (save it for sources with a daily quota), add --no-run to reuse the data from the last run. Next to the images, <widget>.json records each image's size and its tappable regions (hits: which area fires which event; empty means no cell has its own tap action, so a tap anywhere goes to the .xwidget onClick) — handy for checking tap targets.

If the package has pages (page/, the layer a widget opens when tapped), render those too:

numable render my-source --page

Every route in router.json gets a light and a dark full-page screenshot at phone width, saved as page*.png in the same folder. Data fetched by the pages goes through the same allowlist and fixtures as run, and blocked requests, page script errors and bridge methods that are not simulated are all printed. form pages and remote pages are not supported yet; open them in the app. Details in numable docs page.

4. Declare

Write the .xwidget to bind rc / df / size / refresh cadence / click behavior together. Two things go wrong most often:

5. Static check

numable check my-source

Defaults to the personal profile and requires zero errors. It catches the class of problem that nobody finds unless a check finds it: an allowlist that does not match, a cell missing type, a wrong condition key in op:if, a concurrent result arriving one step late, a fetch failure with no error exit. The full code table is in numable docs lint-codes.

6. Show the renders for approval

Show the user the light / dark / empty images under .numable/render/. It is not done until the user says so — check and run cannot prove "it looks good" or "this is the widget they wanted".

7. Keep iterating

Whether the user edits by hand in the App's editor or asks the AI again, they are editing the same source files. After each change, only rerun the affected layer: run after changing a .df, render after a .rcn, check after a .xwidget or manifest.json.

Command reference

These are the commands the seven steps use, plus a few switches you will not reach for every day but that save time when you do:

Command What it does
numable workspace init [dir] turns a folder into an authoring workspace: writes a guide for the AI to read, after which you can simply state what you want to the AI from inside that folder
numable init <dir> [--from <package dir>|installed:<id>] creates a new package (regenerating the identity ULID). --from takes any local package folder, or a package installed in the desktop App (installed:<id>, Mac / Windows)
numable init --job <id> --kind static|once|cross|level|changed|task adds an alert / background job to the package in the current folder and raises manifest.minEngine if needed, see numable docs alerts
numable check [package…] [--profile personal|publish] the static gate
numable run [package…] [--flow a,b] [--file x.df] [--full] [--fixtures <dir>] really runs the data layer. --file runs any single .df, including a probe flow no widget is bound to
numable render [package…] [--widget a,b] [--states light,dark,empty] [--locales zh-CN,en-US] renders images
numable render [package…] --page [/route,…] [--locales zh-CN,en-US] renders pages (html / xpage, light + dark full-page screenshots)
numable docs [topic] [--toc] [--section word] [--search word] reads this documentation. For a long chapter, --toc shows the outline, --section reads one part, --search searches every chapter
numable doctor [package…] checks the environment, engine version and workspace

Three global switches and three environment variables:

What each of the three verification layers covers

The three layers (check / run / render) are not substitutes for one another. A green light in any one of them says nothing about the others.

Layer Command Catches Cannot catch
Static numable check structural and field validity, allowlist match, credential declaration match, known silent-failure patterns (cell missing type, doubled unit suffix, concurrent one step late, dirty parseDate pattern, a data flow with no error exit) wrong value paths, ugly layout, inverted emptiness logic
Data numable run whether the source is alive, whether the value paths are right, whether the derived values compute correctly, whether the allowlist blocks, whether the credential fixtures are complete a wrong branch in the render layer (the value is there but the widget took the fallback path)
Render numable render overflow and clipping, dark-mode readability, an empty state that is just a blank widget, an unevaluated $[...] literal drawn onto the widget glyph metrics and method differences on a phone (render uses a browser engine)

Differences that only show up on a phone — glyph widths, number formatting, date methods — have to be looked at on a phone.

Hard rules

Break any of these and the package is not finished. The "How it is caught" column tells you which layer catches it.

Rule How it is caught Symptom when broken Fix
Source files are the single source of truth: no generator scripts, no fixtures left behind check G1b / G1 fixtures ship with the package, or files sit where nothing loads them (written but never takes effect) put fixtures in .numable/params/; RCN under rc/, flows under flow/
The allowlist must match exactly: the set of hosts requested by the .df == manifest.network, no more and no less check G3 too few: silently blocked on device, the widget is stuck at --; too many: the install panel lists domains you never use and alarms the user align with the allowlist that run prints
Secrets never ship: not in params, not as a .df literal, not in data.* check G18 a plaintext secret is distributed to everyone with the package declare manifest.credentials and use the local fixture .numable/params/_credentials.json, see numable docs credentials
Never invent data: if the fetch fails, let the flow fail — do not paper over it with 0, an empty string, or a fake timestamp check G26 the flow reports success on a failed fetch, empty data overwrites the last good data, and a plausible-looking fake number appears on the widget test the load-bearing fields for emptiness → action:"error"; "the collection is empty" is a success, not a failure
Time anchor: a widget showing live data must show when the data is from manual review (look at the render output) the user cannot tell "unchanged" from "not refreshed for three days" pass the timestamp all the way through to the .rcn; test for emptiness before feeding formatDate, or an empty value renders as 1970
light|dark color pairs: every hex color field in the .rcn is written light|dark check G7 whole blocks invisible in dark mode, or white text on white "textColor": "#1A1A1A|#FFFFFF"
Write units as pt check G28 (doubled unit suffix) + visual inspection of the render output px: content shrinks into the top-left corner and type is too small; 14.0ptpt: the whole widget fails to render and nothing reports an error use pt for all geometry and font sizes
Copy goes through i18n: user-visible text is written ${@i18n.key} check G8 / G8b (publish profile) half the widget is in Chinese in an English environment put the text table in each asset's own i18n, see numable docs i18n
Test emptiness with an explicit flag: the sentinel form $[if::(eq::(findNotEmpty::(${x},__none__),__none__),0,1)] — never eq::(x,) / eq::(x,0), and never length:: (it always returns 0 for a number) the run layer (point the URL at a 404 and run again) the empty state renders self-contradictory output like a colored ▼ --% set a hasX flag in the .df and have the .rcn test only the flag

Personal use vs publishing

check has two profiles and defaults to personal:

personal (default) publish (--profile publish)
Purpose your own use / installed on the user's own machine publishing to the store
Which gates run structure, network, credentials, DSL silent failures, routes and interaction all of them
What is skipped store front (subtitle / name width / English coverage), logo, add-widget button layout, widget sizes (≥3 widgets, 22 plus 42/44 required), the bilingual gate, first-paint cache, direct entry point for credential binding nothing
numable check my-source --profile publish

A package for personal use does not have to satisfy the publishing requirements yet; run the publish profile when you want to ship, and fix whatever it reports — the process is in numable docs publish. Both are the same package with the same ULID: after you publish a package you were using yourself, a long-press on the widget the user already has upgrades it to the published version, data intact.

What to read next

What you want to do Read
Understand what is possible and how the platforms differ numable docs capabilities
Build your first widget end to end numable docs first-card
Add a detail page to a package numable docs add-page
Add clicks, parameter editing, forms numable docs add-interaction
Connect a source that needs a secret numable docs credentials
Ship in both Chinese and English numable docs localize
Add an alert or a background job numable docs alerts
Go from personal use to the store numable docs publish
Look up how to write a given file numable docs layout · xwidget · df · rcn · af · page · bridge · i18n · params
Build a declarative page (eight layouts, conditional visibility, input fields, infinite scroll) numable docs xpage
Look up node fields / methods / error codes numable docs rcn-nodes · methods · lint-codes
Look up which keys the builtins (@app / @device / @time / @contentInset …) expose numable docs builtins
The widget is empty, the click does nothing, the edit changed nothing numable docs pitfalls