Skip to main content
Version: 3.0

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
Beta

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 bind takes scheduled, a topic whose value is false when the stage had no planned production in the window. Uptime (availability) is undefined then, so a pipeline publishes none; with scheduled bound, 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 bind also takes oeeComputed, a topic whose value is false when the stage was scheduled but its OEE could not be computed in the window (too few counter readings). No OEE is published then; with oeeComputed bound, 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, uptime or oee) 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_computed and <site>/vsm/blending/scheduled, and the Trabzon 3D template binds them. An app installed from the template before this change keeps its own copy of src/scene.json and src/App.tsx: install the template again (or add the scheduled and oeeComputed binds 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: Card takes subtitle and actions beside title. Inside a kit layout (Page, Section, Stack, Grid, Row) cards are spaced by the layout's gap, so the gaps between cards are even. Page no 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) and ProgressBar.
  • New in the SDK: BASE_CSS, the stylesheet above, and FALLBACK_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-danger and --mh-radius carry 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, the data.* reads, apiFetch, stored documents) gives a number a number cannot hold as an ExactNumber. Every other number is still a plain number, so most values are unchanged.
  • What works on an ExactNumber:
    • String(x), `${x}` and x.text give its digits as received.
    • Arithmetic and comparisons (x * 2, x > 5) use its nearest number.
    • x.toFixed(n) and x.toLocaleString(locale, options) format its digits, never through a number.
    • fmt(x) and the kit's Table, MetricCard and KeyValueList show its digits.
    • Writing it back (data.publish, a stored document) sends the number with every digit.
  • What does not: typeof x === 'number' and Number.isFinite(x) are false, because it is an object.
    • Detect one with isExactNumber(x), or accept both kinds with isNumberish(x). Both are exported from @maestrohub/sdk.
    • Get a plain number (for a chart, a threshold or arithmetic) with Number(x), or with num(x). This is the nearest number, so the extra digits are lost.
    • JSON.stringify(x) writes its digits as a string. stringifyJson(x) (exported) writes a number.
  • Added in @maestrohub/sdk: ExactNumber, isExactNumber, isNumberish, formatNumberish, parseJson, stringifyJson and the Numberish type.

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, ToneMark and TONE_WORD
    • Palette.status and the StatusColour type
    • Palette.background and Palette.dangerInk
    • the ResolvedPalette type, which usePalette now returns
    • mixColors and contrastRatio
  • Added: formatNumber takes { pad, percent }, and fractionDigits is exported from @maestrohub/sdk.

  • Changed: status colours (ok, warn, critical, neutral) are the kit's own in every shell theme.

    • toneColor and Badge both read Palette.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.danger is now only the danger button's fill, with Palette.dangerInk as its text colour.

    • Text you wrote in p.danger should move to toneColor(p, 'critical'). That works for any palette, including one typed as Palette.
    • If you want the status colours directly, use p.status?.critical.text. status is optional on Palette, so p.status.critical.text does not compile under strict unless the helper takes a ResolvedPalette, which is what usePalette returns.
    • The old danger field still exists, but in a shell dark theme it is a deep red that cannot be read as text.
  • Unchanged for app code: the three new Palette fields (background, dangerInk, status) are optional on Palette.

    • A palette object your app builds itself still type-checks.
    • toneColor still answers for it, from the palette's badge colours and then its ok, warn, danger and muted.
  • 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 threshold cell
    • 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's badge and category-color formatters.

  • Changed: a delta (a compare cell, a MetricCard delta):

    • 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 digits value outside 0 to 20 is clamped instead of throwing. A numeric string such as "2" counts as its number. The same rule is exported as fractionDigits from @maestrohub/sdk. With digits set, a compare cell's own value is padded to it too (7.00 (▲ 2.00)).

  • Changed: the 3D Scene room 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() and stale on useTopic / useTopics: a value kept after the connection dropped says so.
  • usePendingWrites(), pendingWrites, CODE_OFFLINE, and ConnectionNotice in @maestrohub/app-ui.
  • Fixed: a topic path written in its portable form (mHv1.0/{workspace}/…, what find_topics returns and manifests declare) is filled with the workspace's own path by useTopic, useTopics, data.publish and every data.* 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.record and notifications.notify no longer reject: they wait and resolve once sent. Every other write that does not get through now rejects with CODE_OFFLINE instead of a raw network error. plant.record now 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), under entities:record and a declared plant.record action. They land at once.
  • plant.update({ note, facts }): facts set on things, under proposals:write and a declared plant.update action. A change set proposed as the viewer, applied at once when the app also has proposals:decide and the viewer may decide proposals; otherwise result.state is proposed and result.waiting says why.
  • plant.declare(thing, mode, { note }): what a thing is doing, under entities:declare and a declared plant.declare action.
  • Every one waits for another person where a part is held to another person's approval.
  • The plant binding kind, and SCOPE_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.

  • PersonField and Avatar in @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.notify takes to: named people and groups who can open the app.
  • print(), PRINT_CSS and the mh-no-print / mh-print-only classes: 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.

RemovedUse 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_READSCOPE_PLANT_READ: the scope string is still entities:read, so manifests need no change
Types Entity, ListEntitiesOptions, EntityBinding, EntityComputedStatus, FleetMetricRow, FleetMetricsOptionsEnvelope<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.