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

publish — from personal use to publishing to the store

Audience: the person building a tool, and the AI working on their behalf. Both read this same page.

Goal

Take a package you use yourself and turn it into one other people can install from the store, understand at a glance, and not be puzzled by after installing — then ship it.

Publishing itself does not happen in the CLI: the CLI only gets the package into a shippable state (check --profile publish with zero errors). Signing and upload happen in the publish panel of the Workbench in the desktop app (Mac / Windows), which is where your login identity and signing capability live.

Before you start

  1. The package is already error-free on the personal profile (you have been through numable docs first-card).
  2. You are logged in to the desktop app — otherwise the publish panel just says you need to log in first.
  3. The package is the one you authored in your workspace folder, not someone else's package installed from the store.

Step 1 · Know what the two profiles differ on

numable check has two profiles. The personal profile skips nine sections, all of them things that do not affect whether the package runs, only whether other people can use it — left to good intentions, none of them ever get done, so only a check can enforce them.

Section What the publish profile additionally requires What happens if you skip it
G6 widget sizes ≥3 .xwidget files per package, including one at size 22 and one at 42 or 44 22 is the smallest home-screen widget size, so without it the package cannot go on the home screen; a package with only small widgets cannot fill a dashboard screen
G11 / G11b identity assets A logo.png at the package root, 512×512 square, full-bleed, with no rounded corners baked in Missing image falls back to an initial-letter icon; bake in your own corners and the host rounds it again, so the two arcs disagree and an opaque background leaks a ring of color at the corners
G19 store front manifest.subtitle is required and ≤22 characters; manifest.i18n["en-US"].title and .subtitle are required With no subtitle, the store row can only read "name + version"; with no English, English users see a column of Chinese
G24 add-widget entry (same section as G19) The add-widget entry is one button (pickWidgets), not a hand-copied widget catalog inside the page; do not write "Added" A hand-copied catalog is a second source of truth: forget to update the page after adding a widget and users can never add it; only the panel knows whether adding succeeded, so an "Added" label inside the package will lie
G20 English widget titles i18n["en-US"].title and .sub on every .xwidget In an English environment, dashboard widget titles, the widget panel, and the home-screen widget configuration list all show Chinese
G21 title width Display width of title ≤24 per locale (full-width counts 2, half-width 1), ≤16 recommended; manifest.i18n structurally valid, category using a platform enum key The tool grid tile title is always two lines, and the overflow is not hidden, it is eaten by an ellipsis, so users see half a name they cannot recognize
G8 / G8b bilingual content The text tables in the package (rc.i18n in .rcn, top-level i18n in .xpage / .af / .xform) have matching keys in both zh-CN and en-US; hard-coded Chinese in user-visible fields is lifted out into keys Chinese leaks onto widgets in an English environment; wherever a key is missing, the literal is rendered
G23 first-paint cache For packages with a detail page: the .df a page consumes needs three-state data.get / data.set caching (render from cache first, revalidate in the background, bypass on pull-to-refresh, never overwrite on failure); a cached page's root node needs events.onRefresh; cache write-back in the .df a widget consumes must be gated by an input parameter that defaults to off; no credentials in the cache Every first paint waits a full network round trip; pull-to-refresh hits the cache again, so refreshing changes nothing; on the widget side the whole point of a refresh is "skip the cache and fetch", and another cache layer underneath makes the entire refresh chain spin idle
G25 direct credential entry A package that declares a required: true credential needs an entry point in page/ going straight to numable://app/mine?section=credentials Users install it, see an empty widget, and have no idea where to bind a key; "add the widget first, then follow the hint on it" is a detour, not an entry point
G27 add-widget button styling The add-widget button uses the platform baseline styling (height, width, shape and color are not customized) Adding a widget is a platform action and users should recognize it instantly in any package; letting each package draw its own gives the same button six different looks

Beyond that, every check that runs on both profiles (structure, allowlist must match exactly, DSL silent failures, route and credential declarations) still applies. The complete code table is in numable docs lint-codes.


Step 2 · Run the full checks and fix them one by one

Command

numable check hn --profile publish

What "right" looks like: the goal is 0 error. Coming straight from the personal profile, it usually looks like this:

Static checks · profile publish (full store-standard set)

── HN Top Story (hn) ──
✗ [01M200QWNNX1RFPNTX5S7M8PGW] only 1 widget, the standard requires ≥3
✗ [01M200QWNNX1RFPNTX5S7M8PGW] no 42/44 widget (the standard requires at least one wide widget)
! [01M200QWNNX1RFPNTX5S7M8PGW] no logo.png — will fall back to an initial-letter icon
· [01M200QWNNX1RFPNTX5S7M8PGW] 1 widget · layout[22] · 5KB · net[hn.algolia.com]

✗ check finished: 2 error / 1 warn

A suggested order for fixing them:

  1. Add widgets (G6). Do not just resize the same widget — a wide widget should answer more (the top few entries, a trend bar), not be a small widget stretched out. After each new widget, go through run → render → the three-state renders again.
  2. Export a logo (G11). 512×512, content filling the whole canvas, no margin and no rounded corners of your own at the four corners.
  3. Fill in the store front (G19 / G21). subtitle says in one sentence what the package gives me, ≤22 characters; title short enough to read in one line. The English is not a machine translation of the Chinese, it is a fresh sentence an English user understands.
  4. Fill in the English (G20 / G8). One title / sub pair per .xwidget; matching keys in both locales in each .rcn's rc.i18n. How to do it: numable docs localize.
  5. First-paint cache (G23), credential entry (G25), add-widget button (G24 / G27) — only packages with a detail page / with credentials / with an add-widget entry will run into these.

Warnings (!) do not block publishing, but read every one of them. Several of them mean "it works today, but English users / small-screen users are not seeing what you think they are".

After fixing, re-verify on two layers:

numable run hn
numable render hn --locales zh-CN,en-US

If the English column still renders Chinese, the text table is not wired up; if a literal ${@i18n.xxx} appears on a widget, that key is missing in that locale.

Before you publish · Read your on-screen text once more

check tells you whether things run, not whether the words are any good. Before handing the package to other people, go through every line users can see — in widgets, pages and reminders — against these points:


Step 3 · Publish from the Workbench in the app

The CLI does not publish. Go to the desktop app (Mac / Windows) → Workbench → find the package → Publish.

The panel has three things to look at:

Item Notes
Version "Publish as v(next)" is checked by default. Each (id, version) can be published only once, and re-uploading one is rejected. If the content changed, let it go up by one
Distribution region Read-only, set by the platform: the first publish goes overseas (outside mainland China); availability in mainland China is set by the platform after review; every later version keeps the region of the version currently live. Publishing from mainland China is not available yet, and the panel says "Publishing isn't available in mainland China yet"
Checks Hitting Publish runs the static checks first, using exactly the --profile publish rule set. Any error blocks the upload and is listed line by line

After a successful upload the panel shows one of two outcomes:

Publishing is an outbound action: every byte in the package is signed and distributed to every user. Before you ship, confirm the package contains no keys, no local fixtures, and no private data — the .numable/ folder never enters the package by design, and credentials are declared through manifest.credentials rather than written into files (see numable docs credentials).


Step 4 · After publishing

How the package reaches users

At upload time the package is sealed into a signed .xbundle. The user taps install in the store → the client downloads it → verifies the signature and file hashes → unpacks it to disk → renders locally. Data is always fetched by the user's own device; it never passes through the server.

Updates ride on version +1

The app compares the version in the store with the version installed and only offers an update when the former is higher. Re-publishing without bumping manifest.version is the same as not publishing: the update is never noticed and installed users stay on the old build forever. Before shipping a new version, re-verify your changes on both the run and render layers — users install all of the package, not just the few files you touched.

Your source folder and the published version are two copies

The copy in your workspace folder is the source; it is never overwritten by the published version and never appears in the update badge (updates only apply to packages installed from the store). So:

A widget's file name is its identity

Each widget on a user's dashboard records "which package + which .xwidget file name". If a new version deletes or renames a .xwidget, the widgets that point at it show "Widget removed" and are cleared when the package updates. So once a widget file is named, keep the name; to replace a widget, add a new file, and delete the old one only if users can do without it.

A version that widens access is not installed automatically

"Auto-update tools" is on by default, but if a new version reaches more than the installed one — more websites (manifest.network), more credentials (a new credentials[].id, or an existing credential sent to a new website), or new background jobs — it is not updated silently: the user taps update and confirms each one, with the install panel saying "New in this version: …". A version that only narrows access installs automatically. So ship a change that widens access as its own version, not bundled with a fix users are waiting for.

Version history and going back

In the Workbench, the package row's ··· → Version history lists every version you published and its state. A version that was replaced by a newer one, or that you withdrew yourself, offers Go back to this one: it republishes that version's content under a new version number and replaces the version currently live (the app only accepts a higher version number, so going back is also a step forward, not a renumbering). A version the platform took down cannot be restored this way; appeal instead.

What you can still do after shipping

The Workbench lets you take down your own published packages; if a package is taken down or reported, you can appeal. Both live on the package list in the Workbench.


Common mistakes

Symptom Cause Fix
Hitting Publish immediately shows a list of red items and nothing uploads The static checks found errors Fix them as listed; running numable check <package> --profile publish in the terminal uses the same rule set and iterates faster
A message says the version is taken The version bump was unchecked, so the same (id, version) was re-uploaded Check "Publish as v(next)", or bump manifest.version by hand first
A new version shipped but users get no update prompt manifest.version was not bumped Bump it and publish again
The store shows half a name plus an ellipsis The title exceeds what two lines of the grid tile can hold (G21) Shorten title and put the full name in subtitle
Store row / widget titles are Chinese in an English environment manifest.i18n["en-US"] (G19) or the .xwidget's i18n["en-US"] (G20) is missing Fill them in; numable docs localize
A white ring at the icon's corners logo.png has rounded corners baked in over an opaque background, and the host rounds it again so the backing leaks (G11b) Go full-bleed and fill the corners with the backing color
After installing there is one empty widget and users do not know a key is needed A required credential is declared but the page has no direct entry point (G25) Put numbered steps plus a direct button on the home page
A message saying the account is restricted from publishing The account is on the publisher denylist Contact the platform through in-app feedback
"Publishing isn't available in mainland China yet" You are publishing from mainland China Not available yet; publish from outside mainland China
A new version shipped but users are slow to get it automatically It reaches more websites, credentials or background jobs than the installed version, so users must confirm each one Expected; ship access-widening changes as their own version
After a new release, a widget on the user's dashboard shows "Widget removed" That .xwidget was deleted or renamed Never rename a widget file; add a new file to replace a widget

Next