SDK changelog
Beta: see what changed in the App Studio SDK and what to change in an app when you next edit it, such as moving from entities to plant calls.
- Runtime
- runtime-v1
- Latest change
- 2026-11: a value-stream stage says when it was not scheduled
App Studio is in release 3.0 as Beta: a paid add-on that no edition includes by default. A licence or a trial licence that includes the app_studio feature switches it on. The module is on by default, so the licence decides. Names and steps may change while it is in Beta.
@maestrohub/sdk is compiled into the pinned runtime an app is built against (runtime-v1 today), so a published app keeps working as it was built. What changes is the surface a new build compiles against; this page says what, when, and what to change in an app's source when it is next edited.
2026-11: a value-stream stage says when it was not scheduled
An addition to @maestrohub/scene, shipped inside runtime-v1. Code an app already has keeps compiling.
- New in the scene spec: a value-stream stage's
bindtakesscheduled, a topic whose value isfalsewhen the stage had no planned production in the window. Uptime (availability) is undefined then, so a pipeline publishes none; withscheduledbound, the stage's cycle time, uptime and OEE (in its box, on the lead-time ladder and on the 3D overlay card) read Not scheduled, with the reason on hover, instead of a blank or the last value still on their topics, and a stale worst-stage topic no longer puts the kaizen burst on it. A missing value reads–as before: no data yet. - New in the scene spec: a stage's
bindalso takesoeeComputed, a topic whose value isfalsewhen the stage was scheduled but its OEE could not be computed in the window (too few counter readings). No OEE is published then; withoeeComputedbound, OEE reads–with the reason on hover instead of the last OEE still on its topic. When both apply, Not scheduled wins. - New in the kit:
stageRow(key, bind, values), the text of one stage row (cycle,uptimeoroee) as the value-stream sheet and the overlay cards show it, for an app's own figures. - The demo plant's value stream publishes
<site>/vsm/<line>/<stage>/scheduled,<site>/vsm/<line>/<stage>/oee_computedand<site>/vsm/blending/scheduled, and the Trabzon 3D template binds them. An app installed from the template before this change keeps its own copy ofsrc/scene.jsonandsrc/App.tsx: install the template again (or add thescheduledandoeeComputedbinds to its stages) to get it.
What to do: nothing is required. When you next edit an app with a value stream, bind each stage's scheduled and oeeComputed topics if its pipeline publishes them.
2026-10: every app gets the platform look
A change to how an existing app looks, shipped inside runtime-v1. Code an app already has keeps compiling.
- Changed: the app's page now has a base stylesheet. A plain element the kit does not draw (a
<textarea>, a file input, a<table>, a heading) takes the shell's font, colours and corner radius in every theme, instead of the browser's defaults (monospace text areas, grey bevelled buttons). The page is padded from the frame's edges. Every rule has zero specificity, so any inline style or class the app writes still wins. - Changed:
Cardtakessubtitleandactionsbesidetitle. Inside a kit layout (Page,Section,Stack,Grid,Row) cards are spaced by the layout's gap, so the gaps between cards are even.Pageno longer adds its own padding; the page does. - New in the kit:
FileField(a file picker that also takes dropped files),Attachments(a stored record's photos and PDFs: thumbnails that open, a picker, the refusal when the platform will not take a file),FeedStatus(a live subscription's state in words: Live, Connecting, Feed error, Disconnected) andProgressBar. - New in the SDK:
BASE_CSS, the stylesheet above, andFALLBACK_COLOURS, the colours an app shows when the shell sent no theme. The CSS variables--mh-bg,--mh-surface,--mh-ink,--mh-muted,--mh-border,--mh-subtle,--mh-primary,--mh-primary-ink,--mh-dangerand--mh-radiuscarry the current theme for an app's own styles. - Validate warns on a hand-written
<input>,<textarea>,<select>,<button>or<table>and names the kit component to use instead. It is a warning: the app still builds and publishes.
What to do: nothing is required. When you next edit an app, replace any hand-written control the warning names with its kit component, and frame the app in Page.
2026-10: numbers past 2^53 keep every digit
A change to what an existing app reads, shipped inside runtime-v1. Code an app already has keeps compiling.
The platform answers a number with every digit, but a JavaScript number (a 64-bit float) holds only about 15 to 17 significant digits. A 19-digit counter such as 1729600000000000001, an unsigned 64-bit value such as 18446744073709551615, or a decimal with more digits than that used to reach your app rounded (1729600000000000000).
- Changed: every read the SDK makes (
useTopic,useTopics, thedata.*reads,apiFetch, stored documents) gives a number anumbercannot hold as anExactNumber. Every other number is still a plainnumber, so most values are unchanged. - What works on an
ExactNumber:String(x),`${x}`andx.textgive its digits as received.- Arithmetic and comparisons (
x * 2,x > 5) use its nearestnumber. x.toFixed(n)andx.toLocaleString(locale, options)format its digits, never through anumber.fmt(x)and the kit'sTable,MetricCardandKeyValueListshow its digits.- Writing it back (
data.publish, a stored document) sends the number with every digit.
- What does not:
typeof x === 'number'andNumber.isFinite(x)arefalse, because it is an object.- Detect one with
isExactNumber(x), or accept both kinds withisNumberish(x). Both are exported from@maestrohub/sdk. - Get a plain
number(for a chart, a threshold or arithmetic) withNumber(x), or withnum(x). This is the nearestnumber, so the extra digits are lost. JSON.stringify(x)writes its digits as a string.stringifyJson(x)(exported) writes a number.
- Detect one with
- Added in
@maestrohub/sdk:ExactNumber,isExactNumber,isNumberish,formatNumberish,parseJson,stringifyJsonand theNumberishtype.
2026-10: status colours in every shell theme
Additions, and changes to what an existing app draws. Code an app already has keeps compiling.
-
Added in
@maestrohub/app-ui:toneColor,ToneMarkandTONE_WORDPalette.statusand theStatusColourtypePalette.backgroundandPalette.dangerInk- the
ResolvedPalettetype, whichusePalettenow returns mixColorsandcontrastRatio
-
Added:
formatNumbertakes{ pad, percent }, andfractionDigitsis exported from@maestrohub/sdk. -
Changed: status colours (ok, warn, critical, neutral) are the kit's own in every shell theme.
toneColorandBadgeboth readPalette.status, so they always match.- Each status colour reads at 4.5:1 or more on the card and the subtle fill.
- In the shell's dark themes a critical value used to take the shell's button red, which read at about 1.7:1.
-
Changed:
Palette.dangeris now only the danger button's fill, withPalette.dangerInkas its text colour.- Text you wrote in
p.dangershould move totoneColor(p, 'critical'). That works for any palette, including one typed asPalette. - If you want the status colours directly, use
p.status?.critical.text.statusis optional onPalette, sop.status.critical.textdoes not compile understrictunless the helper takes aResolvedPalette, which is whatusePalettereturns. - The old
dangerfield still exists, but in a shell dark theme it is a deep red that cannot be read as text.
- Text you wrote in
-
Unchanged for app code: the three new
Palettefields (background,dangerInk,status) are optional onPalette.- A palette object your app builds itself still type-checks.
toneColorstill answers for it, from the palette'sbadgecolours and then itsok,warn,dangerandmuted.
-
Changed: these now also show a shape for the tone (a disc for ok, a triangle for warn, a diamond for critical, a ring for neutral):
- StatusDot and Timeline
- a toned MetricCard value
- a Gauge below its threshold
- a Table
thresholdcell - toned bars, and toned series' points and legend
The marks are named by the tone's word.
- Charts list each bar, series and band, with its unit and its tone's word, in their accessible description. After the first 20 entries they give a count of the rest per tone.
- A Timeline entry or StatusDot with no tone is not announced, and a StatusDot whose label is already the tone's word is not announced twice.
-
A toned
Badge's text must name the status ("Faulted", "Torque high"), not only a number or a name: colour alone is not enough for colour-blind users. This also applies to the values shown by the Table'sbadgeandcategory-colorformatters. -
Changed: a delta (a
comparecell, a MetricCarddelta):- keeps its padded decimals (
▲ 2.00) - writes a percent in the locale's own form (
8,8 %,%8,8) - is a percentage of the baseline's size, so -3 against -2 reads
▼ 50.0% - shows
•when it rounds to zero
A
digitsvalue outside 0 to 20 is clamped instead of throwing. A numeric string such as"2"counts as its number. The same rule is exported asfractionDigitsfrom@maestrohub/sdk. Withdigitsset, acomparecell's own value is padded to it too (7.00 (▲ 2.00)). - keeps its padded decimals (
-
Changed: the 3D
Sceneroom follows all six shell themes, not only light and dark. Its label chips are opaque.
2026-10: the plant's clock, and kiosks offline
Additions, and one change to what an existing app sees.
useClock(),formatDateTime,formatNumber,dayWindow,sinceClock: the workspace's time zone (or the viewer's when none is set) and the viewer's locale. Times stay UTC on the wire.useConnection()andstaleonuseTopic/useTopics: a value kept after the connection dropped says so.usePendingWrites(),pendingWrites,CODE_OFFLINE, andConnectionNoticein@maestrohub/app-ui.- Fixed: a topic path written in its portable form (
mHv1.0/{workspace}/…, whatfind_topicsreturns and manifests declare) is filled with the workspace's own path byuseTopic,useTopics,data.publishand everydata.*read. It was sent as typed, so the subscription was refused as a topic that does not exist (the Line Operator Console template's station lights never lit). - Changed: while the connection is down, a storage save or delete,
plant.recordandnotifications.notifyno longer reject: they wait and resolve once sent. Every other write that does not get through now rejects withCODE_OFFLINEinstead of a raw network error.plant.recordnow sends each event's time as the moment it was recorded.
2026-10: writing to the plant
Additions only; nothing an existing app uses changed.
plant.record({ note, events }): events a person records in the plant's log (reject, lab result, fill, changeover, work order, calibration, proof test, shift), underentities:recordand a declaredplant.recordaction. They land at once.plant.update({ note, facts }): facts set on things, underproposals:writeand a declaredplant.updateaction. A change set proposed as the viewer, applied at once when the app also hasproposals:decideand the viewer may decide proposals; otherwiseresult.stateisproposedandresult.waitingsays why.plant.declare(thing, mode, { note }): what a thing is doing, underentities:declareand a declaredplant.declareaction.- Every one waits for another person where a part is held to another person's approval.
- The
plantbinding kind, andSCOPE_PLANT_RECORD,SCOPE_PLANT_PROPOSE,SCOPE_PLANT_DECIDE,SCOPE_PLANT_DECLARE. See the builder guide.
2026-10: people, print and labels
Additions only; nothing an existing app uses changed.
PersonFieldandAvatarin@maestrohub/app-ui: a people picker.people.search(),usePeople,usePersonPicture,labelOf,initialsOf,subjectOf: the people who can open the app, by name. See People, paper and labels.notifications.notifytakesto: named people and groups who can open the app.print(),PRINT_CSSand themh-no-print/mh-print-onlyclasses: the app prints itself, on paper or as a PDF.zpl.label(...): a Zebra label, sent through a TCP connector function.
2026-09: entities.* becomes plant.* (#6017)
The knowledge model moved out of the UNS module into the knowledge module, and the SDK's entity reads moved with it. The old entities namespace read the retired entity tree over /api/v1/uns/entities…; the new plant namespace asks the knowledge module's own tools over POST /api/v1/knowledge/tools/<tool>, the same tools an agent calls as plant_* and the Things pages read. There is no compatibility layer: entities is not exported by the runtime any more, and an app that names it fails its next build with the import error naming the missing export.
| Removed | Use instead |
|---|---|
entities.list({ typeLineageId, lifecycle, parentId, under }) | plant.whatCanIAsk() for the sites, then plant.walk({ from: <site or place>, tree: 'physical', direction: 'down', type?: [...] }) for what is under a place; plant.find({ name }) for a thing by name |
entities.get(id) / entities.byUri(uri) | plant.describe(id): the thing, its facts, places, identities |
entities.bindings(id) (topic_path) | plant.describe(id).result.bindings: each series.ref is the topic path when series.kind is "uns" |
entities.computedStatus(id) | No equivalent: computed properties were the old model's. plant.describe(id, { include: ['values'] }).result.readings carries each reading's quality, stale and flat |
entities.fleetMetrics({ propertyId, under, descending, limit }) | plant.numbers({ set: { under }, property, measure: 'latest', order: 'desc', top }), or plant.walk({ from, tree: 'physical', direction: 'down', values: [property] }) and rank in the app |
SCOPE_ENTITIES_READ | SCOPE_PLANT_READ: the scope string is still entities:read, so manifests need no change |
Types Entity, ListEntitiesOptions, EntityBinding, EntityComputedStatus, FleetMetricRow, FleetMetricsOptions | Envelope<T>, Thing, Described, Candidate, WalkedThing, Binding, Reading, Vocabulary, TimelineEntry, and the *Options of each call; see the reference |
Every plant.* answer is an Envelope<T>: read result, and show what the planner says about the answer: coverage.missing (what the plant does not record that the answer needed), hidden (rows the viewer may not see), computed (the answer's own sentence). Errors arrive as ApiError with the module's code and message, as every SDK call's do.
The builder agent's prompt pack and the Equipment Maintenance Log template were moved with the surface; the lint that says which scope a call needs names the plant.* calls now.