App Studio is coming soon: it is in development and testing and is not part of release 3.0. This reference describes the SDK as it is being built and may change before release.
Generated from the SDK sources by scripts/appstudio-sdk-docs.cjs for runtime-v1 — do not edit; run npm run appstudio:docs:sdk.
@maestrohub/sdk
Runtime, data, live-topic, action and storage APIs available to App Studio apps.
Runtime: runtime-v1. 119 exports (47 values, 72 types). Import with:
import { ... } from '@maestrohub/sdk';
Hooks
useAlerts
function useAlerts(limit?: number): AlertsState
useAlerts renders the organization's alarm transitions as they happen
— fires and clears — newest first, at most limit (default 200). Pair
it with alerts.recent() for the alarms that were already active when
the app opened. Needs the uns:read scope.
Example
const a = useAlerts();
return a.error ? <ErrorNotice error={a.error} /> : <Timeline items={a.events.map(e => ({ at: e.firedAt, title: e.topicPath, tone: e.cleared ? 'ok' : 'critical' }))} />;
useAppContext
function useAppContext(): AppContextState
useAppContext returns user/org/theme (throws outside AppProvider).
useExecutionStream
function useExecutionStream(pipelineId: string, limit?: number): ExecutionStreamState
useExecutionStream renders one pipeline's live run events as they
happen — execution start and finish plus per-node progress, newest
first, at most limit (default 200). Needs pipelines:read
(pipelines:execute carries it). Pair it with the executions read API
for history: only manual and API-submitted runs stream live;
scheduled and message-triggered runs don't emit these events. An
empty pipelineId renders an idle stream ('closed', no error) so a
not-yet-bound template stays honest.
Example
const run = useExecutionStream(pipelineId);
return run.error ? <ErrorNotice error={run.error} /> : <Timeline items={run.events.map((e) => ({ at: e.endedAt ?? e.startedAt ?? '', title: e.type, tone: e.type === 'execution.failed' ? 'critical' : 'ok' }))} />;
usePermission
function usePermission(scope: string): { granted: boolean; reason?: string; }
usePermission reports whether the current VIEWER holds a scope the app's manifest requested — the FR-29/42 data every bound actionable control renders its state from. Unknown scope → not granted, with a reason (fail closed, and the UI still explains itself).
useTopic
function useTopic(topic: string): TopicState
useTopic renders the latest value on a topic; status/error explain the gaps.
Example
const t = useTopic('plant/line1/oee');
return t.error ? <ErrorNotice error={t.error} /> : <MetricCard label="OEE" value={t.value ?? '—'} />;
Components
AppProvider
function AppProvider({ children }: { children: ReactNode; }): JSX.Element
AppProvider connects the React tree to the bridge state. When the shell disables the app mid-session (kill switch, FR-21) it also covers the whole frame: a read-only wall dashboard must not keep showing stale numbers behind a token that no longer exists. Plain inline styles — the SDK does not depend on @maestrohub/app-ui.
Functions
ago
function ago(iso?: string | null): string
ago renders how long ago an ISO-8601 instant was — "12s ago", "3m ago", "2h ago" — and "—" when there is no instant.
apiFetch
function apiFetch<T = unknown>(path: string, init?: RequestInit): Promise<T>
apiFetch calls a platform API path (e.g. "/api/v1/uns/topics") with the current app token. Throws ApiError on refusals — including the case the UI should treat as "render disabled-with-reason", a 403 INSUFFICIENT_SCOPE.
fmt
function fmt(n: unknown, digits?: number): string
fmt renders a number for a tile or a cell: integers as they are,
anything else with digits decimals (default 1), and "—" for
anything that is not a finite number.
isConfirmationRequired
function isConfirmationRequired(err: unknown): err is ApiError
isConfirmationRequired: the platform wants the viewer's confirmation first.
isRateLimited
function isRateLimited(err: unknown): err is ApiError & { status: 429; }
isRateLimited: the platform refused because this app (all of its viewers
together) exceeded its request budget. Back off for retryAfterSeconds;
prefer useTopic/useTopics for live values instead of polling REST.
mountApp
function mountApp(App: React.ComponentType): void
mountApp is the boot contract: the served document loads boot.mjs, which imports the app's default component and calls this. The bridge handshake completes BEFORE first render, so hooks never see a half-connected state; uncaught errors report to the shell (NR-3/FR-8).
num
function num(value: unknown, field?: string): number | null
num reads the numeric reading out of a topic value — a number, a
numeric string, or an object carrying one under field (default
"value") — the shapes a UNS record's value takes depending on how
the connector published it. null when there is no number.
parsePayload
function parsePayload(raw: string): Pick<TopicMessage, "value" | "timestamp" | "quality" | "source">
parsePayload mirrors the shell's data explorer: enriched envelope → value; JSON → parsed; else raw.
subscribeTopic
function subscribeTopic(topic: string, onMessage: (m: TopicMessage) => void, onError?: (e: LiveError) => void): () => void
subscribeTopic delivers every value published on topic until the
returned function is called. Refusals (missing scope, topic outside
the viewer's access, app disabled) arrive on onError with a reason
and a remediation — never as silence.
Classes
ApiError
class ApiError extends Error
new ApiError(status: number, code: string, message: string, details?: unknown, retryAfterSeconds?: number, remediation?: string): ApiError
ApiError carries the platform's flat {code, message, details?} refusal
shape. details is what a refusal hands back for the caller to act on
— a storage 409 carries the winning document there.
Members
readonly code:stringreadonly details?:unknownreadonly remediation?:string | undefined— The platform's own "what to do instead" — every refusal body carries one (rule 36).readonly retryAfterSeconds?:number | undefined— Seconds the platform asked us to wait (429 APP_RATE_LIMITED, Retry-After).readonly status:number
ConflictError
class ConflictError<T = unknown> extends Error
new ConflictError<T = unknown>(current: StoredDocument<T> | null): ConflictError<T>
Thrown when a CAS write cannot converge; carries the winner's document.
Members
readonly current:StoredDocument<T> | null
DownloadRefusedError
class DownloadRefusedError extends Error
new DownloadRefusedError(reason: string): DownloadRefusedError
DownloadRefusedError is what bridge.download rejects with when the
file could not be handed to the viewer — locally (over
MAX_DOWNLOAD_CHARS) or by the shell (download_refused). .reason is
the human-readable explanation the app should show (rule 36).
Members
readonly reason:string
Constants
ALERTS_FEED
const ALERTS_FEED: "alerts"
The pseudo-topic the alarm feed reports under in LiveError.topic.
CODE_ACTION_NOT_DECLARED
const CODE_ACTION_NOT_DECLARED: "ACTION_NOT_DECLARED"
ApiError.code when an app-token request hits one of the three write
doors (publish, connector execute, pipeline submit) without a matching
declared action in the app's manifest. The fix is a manifest change
and a republish, not a retry.
CODE_APPROVAL_REQUIRED
const CODE_APPROVAL_REQUIRED: "APPROVAL_REQUIRED"
ApiError.code when the matched manifest action is on the "approval"
rung: the write guard refuses direct calls to it — such an action only
runs through a pipeline with an approval gate.
CODE_CONFIRMATION_REQUIRED
const CODE_CONFIRMATION_REQUIRED: "CONFIRMATION_REQUIRED"
ApiError.code when the platform's write guard requires the viewer's
confirmation for a rung-"confirm" action and the request carried no
confirmed-at assertion. Retry the same call with { confirmed: true }
from the confirmed click; isConfirmationRequired checks for it.
CODE_RATE_LIMITED
const CODE_RATE_LIMITED: "APP_RATE_LIMITED"
CODE_RATE_LIMITED: the app's per-org fair-use budget is spent (429).
MAX_DOWNLOAD_CHARS
const MAX_DOWNLOAD_CHARS: 20000000
The size cap (in characters) on a download message's content.
bridge.download / bridge.requestDownload refuse locally above it,
and the shell (AppFrameHost) answers download_refused with the same
limit for anything that slips through.
PROTOCOL_VERSION
const PROTOCOL_VERSION: 1
The bridge schema version stamped as v on every message in both
directions. The app-side bridge ignores any shell message whose v
differs, and the shell does the same for app messages.
SCOPE_ALERTS_ACK
const SCOPE_ALERTS_ACK: "alerts:ack"
The scope alerts.ack needs; guard its button with usePermission(SCOPE_ALERTS_ACK).
SCOPE_APPROVALS_DECIDE
const SCOPE_APPROVALS_DECIDE: "approvals:decide"
The scope approvals.approve / approvals.reject need; guard their buttons with it.
SCOPE_APPROVALS_READ
const SCOPE_APPROVALS_READ: "approvals:read"
The scope the approvals inbox reads need (approvals:decide carries it too).
SCOPE_FUNCTIONS_DISCOVER
const SCOPE_FUNCTIONS_DISCOVER: "functions:discover"
The scope connectors.execute needs for a discover-effect function (browse namespaces, enumerate operations).
SCOPE_FUNCTIONS_READ
const SCOPE_FUNCTIONS_READ: "functions:read"
Per-effect function scopes: guard a button with the one matching the function's effect.
SCOPE_FUNCTIONS_STREAM
const SCOPE_FUNCTIONS_STREAM: "functions:stream"
The scope connectors.execute needs for a stream-effect function (subscribe, tail, watch).
SCOPE_FUNCTIONS_WRITE
const SCOPE_FUNCTIONS_WRITE: "functions:write"
The scope connectors.execute needs for a write-effect function (commands, tag writes, publishes).
SCOPE_PIPELINES_EXECUTE
const SCOPE_PIPELINES_EXECUTE: "pipelines:execute"
The scope pipelines.submit needs; guard its button with usePermission(SCOPE_PIPELINES_EXECUTE).
SCOPE_PIPELINES_READ
const SCOPE_PIPELINES_READ: "pipelines:read"
The scope the execution stream needs — pipelines:execute carries
read, so an app that submits runs can also watch them land.
SCOPE_PLANT_READ
const SCOPE_PLANT_READ: "entities:read"
The scope every plant.* read needs; guard plant panels with usePermission(SCOPE_PLANT_READ). The scope string stays entities:read — manifests already declare it.
SCOPE_UNS_PUBLISH
const SCOPE_UNS_PUBLISH: "uns:publish"
Scope names the actions need — exported so apps guard with the same strings.
SCOPE_UNS_READ
const SCOPE_UNS_READ: "uns:read"
The scope live subscriptions need. live.subscribe checks the viewer's
grant for it before opening a socket and reports scope_missing on
onError when it is absent; guard live panels with usePermission(SCOPE_UNS_READ).
alerts
const alerts: { recent(options?: RecentAlertsOptions): Promise<AlertItem[]>; history(options?: AlertHistoryOptions): Promise<AlertHistoryItem[]>; subscribe: (sub: Parameters<typeof live.subscribeAlerts>[0]) => () => void; ack(input: AckAlertInput, options?: ActionOptions): Promise<AckAlertResult>; }
The alarm surface: recent and history read the platform's alert
feed (needs uns:read — alert visibility inherits topic read), subscribe
streams transitions live (see useAlerts), and ack acknowledges one
alarm through the write ladder — a declared alerts.ack action bound
to an alerts binding, with its rung — and needs alerts:ack. The
viewer's own alert:ack authority per topic still bounds it: an app
never silences an alarm its viewer could not.
Members
ack:(input: AckAlertInput, options?: ActionOptions) => Promise<AckAlertResult>— Acknowledge one alarm with the viewer's authority. The answer says what it did:not_foundwhen no active alarm carries that key.history:(options?: AlertHistoryOptions) => Promise<AlertHistoryItem[]>— One topic's (or every topic's) transitions, newest first.recent:(options?: RecentAlertsOptions) => Promise<AlertItem[]>— Alarms currently known to the platform (active, cleared-recently, shelved), newest first.subscribe:(sub: Parameters<typeof live.subscribeAlerts>[0]) => () => void— Live transitions over the app's scoped socket (needs uns:read). See useAlerts.
approvals
const approvals: { list(options?: ListApprovalsOptions): Promise<ListApprovalsResponse>; get(id: string): Promise<ApprovalDecision>; approve(input: DecideApprovalInput, options?: ActionOptions): Promise<ApprovalDecision>; reject(input: DecideApprovalInput, options?: ActionOptions): Promise<ApprovalDecision>; }
The approvals surface: the human decision inbox for pipeline gates.
list / get read it (approvals:read; approvals:decide carries read);
approve / reject decide one — the pipeline then continues with the
held payload, or stops — through the write ladder (a declared
approvals.decide action bound to an approvals binding, with its
rung) and with the viewer's own approval:decide authority. There is no
live stream for approvals; poll list gently (the inbox changes at
human speed) or lean on the deadline the row carries.
Members
approve:(input: DecideApprovalInput, options?: ActionOptions) => Promise<ApprovalDecision>— Approve: the pipeline continues with the held payload, as the viewer.get:(id: string) => Promise<ApprovalDecision>— One decision, with its payload.list:(options?: ListApprovalsOptions) => Promise<ListApprovalsResponse>— The inbox, newest first; state defaults to "pending" server-side.reject:(input: DecideApprovalInput, options?: ActionOptions) => Promise<ApprovalDecision>— Reject: the pipeline continues with decision=rejected, as the viewer.
bridge
const bridge: Bridge
The module-singleton bridge — one iframe, one bridge.
Members
currentState:() => AppContextState | nullcurrentToken:() => string | null— currentToken returns the live bearer value or null (disabled/expired-out).download:(name: string, mime: string, content: string) => Promise<void>— download is the promise form of requestDownload: it settles once the shell has had its say. Rejects with a DownloadRefusedError (carrying.reason) when the content is over MAX_DOWNLOAD_CHARS or the shell answersdownload_refused; resolves after DOWNLOAD_REFUSAL_WINDOW_MS without a refusal. Refusals are only received after start() has run (the listener is the handshake's) — mountApp always does that.post:(msg: AppToShellMessage) => voidreportError:(message: string, stack?: string) => void— reportError sends an uncaught error to the shell (NR-3 / FR-8).reportLiveError:(e: { code: string; reason: string; remediation?: string; topic?: string; }) => void— reportLiveError tells the shell a live subscription failed. The builder shows it next to the preview with the remediation; the Run and Kiosk hosts ignore it (the app's own UI already told the viewer).reportTelemetry:(method: string, path: string, status: number, durationMs: number) => void— reportTelemetry sends one API call's timing to the shell (G7).requestDownload:(name: string, mime: string, content: string) => boolean— requestDownload asks the shell to hand the viewer a file (the sandbox cannot start downloads itself). Text content only; refused (returns false) above MAX_DOWNLOAD_CHARS. Synchronous, fire-and-forget: a refusal the SHELL decides on later is logged, not returned — callers that need to react to it usedownload.start:() => Promise<AppContextState>— start runs the handshake once; subsequent calls return the same promise.subscribe:(fn: Listener) => () => void
connectors
const connectors: { execute(input: ExecuteFunctionInput, options?: ActionOptions): Promise<ExecuteFunctionResult>; }
The connector-function surface: execute runs a saved function on a
connection with the viewer's authority (see the member docs). Rides the
write ladder and needs the functions:<effect> scope of that function.
Members
execute:(input: ExecuteFunctionInput, options?: ActionOptions) => Promise<ExecuteFunctionResult>— Execute a saved connector function (an OPC UA write, a DB query, a command) with the viewer's authority. The platform resolves the function's effect and demands the matching functions:<effect> scope.
data
const data: { subscribe: (topic: string, onMessage: (m: TopicMessage) => void, onError?: (e: LiveError) => void) => () => void; publish(input: PublishInput, options?: ActionOptions): Promise<PublishResult>; }
The UNS data surface: subscribe for live values (see subscribeTopic)
and publish for one write to an existing topic (see the member docs).
publish goes through the write ladder — declared action, idempotency
key, in-flight lock — and needs uns:publish.
Members
publish:(input: PublishInput, options?: ActionOptions) => Promise<PublishResult>— Publish one value to an existing UNS topic.subscribe:(topic: string, onMessage: (m: TopicMessage) => void, onError?: (e: LiveError) => void) => () => void— Live values on a topic over the app's scoped socket (needs uns:read). See subscribeTopic.
live
const live: LiveConnection
The app's single live-data connection (one socket per runtime), shared
by useTopic, subscribeTopic and data.subscribe. Call
live.subscribe(topic, subscriber) directly only when you need the
onStatus callback; see the LiveConnection member docs.
Members
alertSubscriberCount:() => number— Test seam: number of alarm-feed subscribers.pipelineCount:() => number— Test seam: number of pipelines with live watchers.subscribe:(topic: string, sub: TopicSubscriber) => () => voidsubscribeAlerts:(sub: AlertSubscriber) => () => void— subscribeAlerts delivers every alarm transition in the organization the viewer may see (alert visibility inherits topic read) until the returned function is called. Needs uns:read like a topic; refusals arrive ononErrorwith a reason and a remediation.subscribePipeline:(pipelineId: string, sub: PipelineSubscriber) => () => void— subscribePipeline delivers ONE pipeline's live execution events — execution.started/completed/failed/cancelled plus per-node progress — until the returned function is called. Gated exactly like the HTTP pipeline reads: the pipelines:read scope (pipelines:execute carries it) plus the viewer's own access to that pipeline; refusals arrive ononErrorwith a reason and a remediation. Only manual and API-submitted runs emit events (mode 'live' | 'sandbox'); scheduled and message-triggered runs appear in the executions API.topicCount:() => number— Test seam: number of topics with live subscribers.
notifications
const notifications: { notify(input: NotifyInput, options?: ActionOptions): Promise<NotifyResult>; }
The notification surface: notify posts one notice to the person
feed of THE APP'S OWN PEOPLE — its owner plus every viewer who has
opened it. The platform stamps the rest: the producer, the app-name
prefix on the title, and the audience — an app can neither
impersonate the platform nor reach beyond its users. A declared
manifest action (kind notifications.notify, a notifications
binding, a rung) — no scope needed. Org channels (Slack, Teams)
hear app notices only when an operator explicitly subscribes a
channel to them.
Members
notify:(input: NotifyInput, options?: ActionOptions) => Promise<NotifyResult>— Post one notice to the app's people.
pipelines
const pipelines: { submit(input: SubmitPipelineInput, options?: ActionOptions): Promise<SubmitPipelineResult>; }
The pipeline surface: submit fires a pipeline's manual-trigger node
with the viewer's authority (see the member docs). Rides the write
ladder and needs pipelines:execute.
Members
submit:(input: SubmitPipelineInput, options?: ActionOptions) => Promise<SubmitPipelineResult>— Fire a pipeline's manual trigger with the viewer's authority.
plant
const plant: { whatCanIAsk(): Promise<Envelope<Vocabulary>>; find(options: FindOptions): Promise<Envelope<Candidate[]>>; describe(thing: string, options?: DescribeOptions): Promise<Envelope<Described>>; walk(options: WalkOptions): Promise<Envelope<WalkedThing[]>>; numbers<T = unknown>(options: NumbersOptions): Promise<Envelope<T>>; timeline(options: TimelineOptions): Promise<Envelope<TimelineEntry[]>>; }
The plant — read-only. Everything needs the entities:read scope
(SCOPE_PLANT_READ); the viewer's own access bounds every row, as
always. Each call is one of the knowledge module's tools, answered as
an Envelope: read result, and say what coverage and hidden say.
Key per-machine app state by the thing's id — a topic path renames.
Nothing here writes: a change to the plant is a proposal a person
decides under Review, on the platform's own pages.
Members
describe:(thing: string, options?: DescribeOptions) => Promise<Envelope<Described>>— One thing in full: its facts, places, identities, bindings (the topics), and with "values" what it reads now.find:(options: FindOptions) => Promise<Envelope<Candidate[]>>— Things by name, by a number another system issued, or by the topic(s) they are readings of.numbers:<T = unknown>(options: NumbersOptions) => Promise<Envelope<T>>— A figure for one thing or hundreds at once — the latest value, an average, time in a state, OEE — ordered whenorderis set: the "N machines, worst first" question. The result's shape follows the measure.timeline:(options: TimelineOptions) => Promise<Envelope<TimelineEntry[]>>— What happened to a thing, or what is happening now under a place: alarms, trips, work orders, shifts, calibrations.walk:(options: WalkOptions) => Promise<Envelope<WalkedThing[]>>— Everything reached from a thing — down a tree (the machines under a line) or along links (what a pump feeds) — with their readings whenvaluesis set.whatCanIAsk:() => Promise<Envelope<Vocabulary>>— The plant's vocabulary: its sites (the roots of every tree), the trees it keeps, its types, what it measures. Ask it first.
storage
const storage: { shared: { collection: <T>(name: string) => Collection<T>; }; user: { collection: <T>(name: string) => Collection<T>; }; }
The app's own document store: shared (one copy per org, CAS by
default) and user (one copy per viewer) — both expose
collection(name) returning a Collection. Not for plant data; see
the Collection member docs for get/put/delete/list/usage.
Example
const prefs = storage.user.collection<{ unit: string }>('prefs');
await prefs.put('display', { unit: 'bar' }, (current, mine) => ({ ...current, ...mine }));
Members
shared:{ collection: <T>(name: string) => Collection<T>; }— App-wide documents (the manifest'sscope: "shared"collections).user:{ collection: <T>(name: string) => Collection<T>; }— Per-viewer documents (the manifest'sscope: "user"collections).
Types
AckAlertInput
interface AckAlertInput
Input to alerts.ack — the alarm's identity as the feed returns it
(topicId + level, plus propertyId / direction when the feed
carries them). The property is named by id: the platform keys an
incident by id so a rename never splits it, and a name is not a key.
Mirrors uns AcknowledgeAlertRequest verbatim.
Members
direction?:string | undefinedlevel:stringpropertyId?:string | undefinedtopicId:string
ActionOptions
interface ActionOptions
Options every write action accepts.
Members
confirmed?:boolean | undefined— The viewer confirmed this specific invocation (rung "confirm").idempotencyKey?:string | undefined— Reuse a key to retry the SAME request; omit for a fresh invocation.
AlarmState
interface AlarmState
A live alarm's state, as plant.timeline reports it with state set.
Members
acknowledged:booleanacknowledged_at?:string | undefinedacknowledged_by_name?:string | undefinedactive:booleanactive_hours:numbershelved:booleanshelved_until?:string | undefinedstanding:booleanto_act_on:boolean
AlertEvent
interface AlertEvent
One alarm transition from the platform's rule engine — mirrors the
uns contract SchemaAlertFiredEvent (shared/contracts/uns) the socket
relays as an alert.fired frame. cleared false = the alarm fired,
true = it cleared.
Members
attributePath?:string | undefined— The property's name at the transition: a label, never a key.cleared:booleandirection:string— "low" | "high".firedAt:string— RFC3339.level:string— "warning" | "critical" — the threshold that tripped.message?:string | undefinedpropertyId?:string | undefined— The alarm key's property, by id. Empty for value schemas.receivedAt:numberrecordTimestamp:stringschemaId:stringschemaName:stringseverity:string— Operator-facing "info" | "warning" | "critical" from the alert config.threshold:numbertopicId:stringtopicPath:stringunit?:string | undefinedvalue:number
AlertHistoryOptions
interface AlertHistoryOptions
Options for alerts.history: one topic's transitions, newest first, before an instant.
Members
before?:string | undefined— RFC3339 — only events before this instant.limit?:number | undefinedtopicId?:string | undefined
AlertItem
interface AlertItem
One alarm as the platform's feed returns it — mirrors uns CurrentAlertDTO verbatim.
Members
acknowledgedAt?:string | undefinedattributePath?:string | undefined— The property's name at the last fire: a label, never a key.clearedAt?:string | undefineddirection?:string | undefined— "low" | "high".fireCount:numberfirstFiredAt:string— RFC3339.lastClearedAt?:string | undefinedlastFiredAt:stringlastRecordTs?:string | undefinedlevel:string— "warning" | "critical" — the threshold that tripped.message?:string | undefinedorgId:stringpropertyId?:string | undefined— The alarm key's property, by id (empty for a value schema).schemaId:stringschemaMajor:numberschemaMinor:numberschemaName:stringseverity:string— Operator-facing "info" | "warning" | "critical".shelvedAt?:string | undefinedshelvedBy?:string | undefinedshelvedUntil?:string | undefinedthreshold:numbertopicId:stringtopicPath:stringunit?:string | undefinedvalue:number
AlertSubscriber
interface AlertSubscriber
Callbacks handed to live.subscribeAlerts; useAlerts builds one for you.
Members
onError?:((e: LiveError) => void) | undefinedonEvent:(e: AlertEvent) => voidonStatus?:((s: LiveStatus) => void) | undefined
AlertsState
interface AlertsState
What useAlerts returns: the latest transitions (newest first) plus the feed's status.
Members
error:LiveError | nullevents:AlertEvent[]status:LiveStatus
AppContextState
interface AppContextState
What the shell told the app about its viewer, as read by useAppContext
and bridge.currentState. Built from the context message; theme
follows later theme pushes and disabled is set by a disable push.
Members
canEditApp:boolean— The viewer may edit this app — gate curation UI on it (writers: "editors" collections).disabled?:string | undefined— Set once the shell disabled the app; carries the notice to show. The token is dropped at the same time.org:{ id: string; slug: string; }— The organisation the app runs in.permissions:Record<string, { granted: boolean; reason?: string; }>— Per-scope grant results;usePermissionreads these.theme:ThemeState— The shell's current theme mode.user:{ id: string; displayName: string; }— The viewer's id and display name.
AppToShellMessage
type AppToShellMessage = HelloMessage | ReadyMessage | ErrorMessage | TelemetryMessage | DownloadMessage | LiveErrorMessage;
Every message the app posts to the shell: hello (handshake start),
ready (handshake done), error (uncaught error), telemetry (one
API call's timing), download (hand the viewer a file) and
live_error (a live-data subscription failed, for the builder).
Discriminated on type.
ApprovalDecision
interface ApprovalDecision
One pending or decided approval — mirrors the engine's DecisionResponse verbatim.
Members
createdAt:stringdeadline:string— RFC3339 — the gate expires by itself after this.decidedAt?:string | undefineddecidedBy?:string | undefineddecisionNote?:string | undefinedfreshUntil?:string | undefinedid:stringkind:string— Apps see "pipeline_gate" only — the agent-writes surface is not exposed.nodeId:stringpayload?:unknown— The held upstream data — exactly what continues on approve.pipelineId:stringpipelineName?:string | undefinedreason?:string | undefinedrequestedBy?:string | undefinedsignature:stringsourceExecutionId?:string | undefinedstate:string— "pending" | "approved" | "rejected" | "expired" | "executed".title:string
Binding
interface Binding
A binding: which series carries one property of a thing.
Members
id:stringproperty_id:stringseries:{ kind: string; ref: string; org_id?: string; }—kind"uns" andrefthe topic path — THE way from a thing to its live data.thing_id:stringunit?:string | undefined
Candidate
interface Candidate
plant.find: one thing that answers.
Members
name:stringphysical_place?:string | undefined— Where it sits in the physical tree, in words.property?:string | undefinedreading?:Reading | undefinedsite:stringthing:stringtopic?:string | undefined— Withtopic/topics: the topic the row answers for, and the property it is bound to.type?:string | undefined
Collection
interface Collection<T = unknown>
A handle on one declared storage collection, from
storage.shared.collection(name) or storage.user.collection(name).
Every method is one or more apiFetch calls to
/api/v1/appstudio/storage/<collection>; an undeclared collection is a 404.
Members
delete:(key: string) => Promise<void>— Removes a document; resolves even if it was already gone.get:(key: string) => Promise<StoredDocument<T> | null>— Returns null when absent.list:(opts?: ListOptions) => Promise<ListPage<T>>— One page of documents; follownext_cursorfor more.put:(key: string, value: T, merge?: (current: T | null, mine: T) => T) => Promise<StoredDocument<T>>— Writes a value. On a CAS collection: merges onto the current document viamerge(default: replace) and retries until it lands or MAX_CAS_ATTEMPTS is hit. On a last-write-wins collection: one write.usage:() => Promise<UsageInfo>— Document count, bytes used and the cap for this collection's scope.
ContextMessage
interface ContextMessage
Shell → app: identity, per-scope grant results, theme, and the token.
Members
canEditApp?:boolean | undefined— The viewer holds app:update on this app (a "keeper" — App Data Authority R1): editor-only affordances render enabled from it. The platform re-checks server-side on every write.org:{ id: string; slug: string; }permissions:Record<string, { granted: boolean; reason?: string; }>— Per-scope grant results from the token exchange (granted + denied_scopes) — what usePermission renders disabled-with-reason from (FR-29/42).theme:ThemeStatetoken:TokenPayloadtype:"context"user:{ id: string; displayName: string; }v:1
DecideApprovalInput
interface DecideApprovalInput
Input to approvals.approve / approvals.reject.
Members
id:string— The decision's id, from the inbox.note?:string | undefined— Recorded beside the verdict; the decider is always the viewer.
DescribeOptions
interface DescribeOptions
What plant.describe may bring beyond the thing itself.
Members
as_of?:string | undefined— The thing as it was at an instant, RFC3339.include?:("values" | "links" | "ended" | "missing" | "running" | "brief")[] | undefined— "values" for what it reads now, "links" for its links, "missing" for recorded absences.property?:string | undefined— Narrow the readings to one property.
Described
interface Described
plant.describe: one thing in full.
Members
bindings?:Binding[] | undefined— The series bound to it —series.refis the topic path for live values.declared?:InForce | undefineddocuments?:string[] | undefinedfacts?:Record<string, FactValue> | undefinedidentities?:Identity[] | undefinedlinks?:DescribedLink[] | undefinedparts?:string[] | undefinedplaces?:Record<string, string> | undefined— Where it sits, in every tree that holds it, keyed by the tree kind (physical, location, floc, …).readings?:Reading[] | undefined— With include "values": what it reads now.thing:Thingtype_chain?:string[] | undefined— Its type and every type it extends, nearest first.
DescribedLink
interface DescribedLink
A link on a thing, named from the thing's side.
Members
link:stringname:stringother:stringother_name:stringother_site?:string | undefinedsince:stringuntil?:string | undefined
DisableMessage
interface DisableMessage
Shell → app: kill switch / unshare — drop the token, show the notice.
Members
message:stringtype:"disable"v:1
DownloadMessage
interface DownloadMessage
App → shell: hand the viewer a file (G1 export). The sandbox has no allow-downloads, so a download started inside the iframe is inert; the shell materialises it. Text content only (CSV/JSON/text), size-capped.
Members
content:stringid?:string | undefined— Correlates adownload_refusedreply with the request that caused it.mime:stringname:stringtype:"download"v:1
DownloadRefusedMessage
interface DownloadRefusedMessage
Shell → app: the shell refused a download (over MAX_DOWNLOAD_CHARS,
non-text content). id echoes the request's id so the SDK can settle
the right caller's promise; a refusal is never silent (rule 36).
Members
id?:string | undefinedreason:stringtype:"download_refused"v:1
Envelope
interface Envelope<T>
Every plant answer: the result, and what the planner says about it —
what it could not read (coverage), how many rows the viewer may not see
(hidden), the sentence it computed, and the calls that would say more.
Members
ambiguous?:boolean | undefined— More than one thing answered to the name asked.computed?:string | undefined— The answer's own sentence, in words.coverage?:{ missing?: string[]; without_reading?: string[]; } | undefined— What the answer needed and the plant does not record, or did not read.cut_short?:{ limit: string; } | undefined— The answer was cut short at this limit.hidden?:number | undefined— Rows the viewer's access hides — counted, never shown.next?:{ tool: string; why: string; }[] | undefined— What to ask next, and why.readings_as_of?:string | undefined— The instant the readings in the answer are as of, RFC3339.result:T
ErrorMessage
interface ErrorMessage
App → shell: an uncaught error (NR-3 + FR-8's send-to-agent hook).
Members
message:stringstack?:string | undefinedtype:"error"v:1
ExecuteFunctionInput
interface ExecuteFunctionInput
Input to connectors.execute.
Members
connectionId:string— The connection the saved function belongs to.functionId:string— A SAVED function on that connection (apps cannot run ad-hoc function bodies).params?:Record<string, unknown> | undefined— Function parameters; sent as{}when omitted.
ExecuteFunctionResult
interface ExecuteFunctionResult
Mirrors the platform's FunctionResultResponse.
Members
data?:Record<string, unknown> | undefineddurationMs:numbererror?:string | undefinedmetadata?:Record<string, unknown> | undefinedsuccess:booleantimestamp:string
ExecutionEvent
interface ExecutionEvent
One live event from a pipeline's execution stream — the platform's
unified live-exec family (shared/contracts/pipeline live_events.go),
delivered as WS frames typed execution.* / node.*. Field names
mirror the contract's json tags verbatim (camelCase); a full-boot Go
test pins them. Only manual and API-submitted runs emit these events
(mode separates real runs from sandbox test runs); scheduled and
message-triggered runs do not — read those from the executions API.
Members
data:Record<string, unknown>— The frame's full payload, verbatim, for fields not lifted above.durationMs?:number | undefinedendedAt?:string | undefinederror?:string | undefined— A node event's error, or execution.failed's errorSummary.executionId:stringmode:string— 'live' for a real run, 'sandbox' for a test run.nodeId?:string | undefined— Node events only.nodeName?:string | undefinednodeType?:string | undefinedpipelineId:stringreceivedAt:numberstartedAt?:string | undefinedtype:string— 'execution.started' | 'execution.completed' | 'execution.failed' | 'execution.cancelled' | 'node.started' | 'node.progress' | 'node.completed' | 'node.failed' | 'node.skipped' | 'node.recovered' | 'node.prompt'.node.progressis a loop body node's coalesced progress: cumulativecompleted/failedcounts and the latest iteration'soutput.
ExecutionStreamState
interface ExecutionStreamState
What useExecutionStream returns: the run events (newest first) plus the stream's status.
Members
error:LiveError | nullevents:ExecutionEvent[]status:LiveStatus
FactValue
interface FactValue
A fact a thing carries, with its unit when it has one.
Members
unit?:string | undefinedvalue:unknown
FindOptions
interface FindOptions
What plant.find takes: a name, a number another system issued, a topic, or many topics at once.
Members
as_of?:string | undefined— The plant as it was at an instant, RFC3339.identifier?:string | undefined— A number another system issued, withsystemsaying which.name?:string | undefined— A name or part of one, in any language the plant records.system?:string | undefinedtopic?:string | undefined— A uns topic path: answered with the thing it is a reading of and the property.topics?:string[] | undefined— Many topic paths in one call — a screen's whole list; nevertopicin a loop.
HelloMessage
interface HelloMessage
App → shell: announce readiness to receive context.
Members
app:{ slug: string; }type:"hello"v:1
HostPalette
interface HostPalette
The host's design tokens, resolved by the shell to plain CSS colours
(hsl(220 14% 10%), #fff) so an app renders in the shell's exact
theme — every variant, not a hard-coded light/dark pair. Optional in
every message: a shell that cannot resolve them (no stylesheet yet)
omits the field and the UI kit falls back to its own palette.
Members
background:string— Page background (--background).border:string— Borders and rules (--border).danger:string— Critical / destructive (--destructive).font:string— The shell's font stack (--font-sans).ink:string— Primary text (--foreground).muted:string— Secondary text (--muted-foreground).primary:string— Accent — primary buttons, active tab, default chart series (--primary).primaryInk:string— Text on the accent (--primary-foreground).radius:string— The shell's corner radius (--radius), a CSS length.subtle:string— Subtle fill — table header, neutral button (--muted).surface:string— Card / control surface (--card).
Identity
interface Identity
A number another system knows the thing by: the SAP equipment, the functional location, the OPC UA node.
Members
id:stringidentifier:stringkind?:string | undefinedsystem:string
InForce
interface InForce
A declaration in force on a thing — its own or the nearest above it: maintenance, decommissioned, …
Members
by?:string | undefinedmode:stringnote?:string | undefinedon:string— The thing the declaration was made on.on_name?:string | undefinedsince:string
ListApprovalsOptions
interface ListApprovalsOptions
Filters for approvals.list — mirrors the engine's query names verbatim.
Members
limit?:number | undefinedoffset?:number | undefinedpipelineId?:string | undefinedstate?:string | undefined— "pending" (the default) | "approved" | "rejected" | "expired" | "executed" | "all".
ListApprovalsResponse
interface ListApprovalsResponse
The inbox page shape.
Members
items:ApprovalDecision[]total:number
ListOptions
interface ListOptions
Query options for Collection.list; each one becomes a query parameter only when set.
Members
cursor?:string | undefined— Thenext_cursorof the previous page.limit?:number | undefined— Maximum documents per page.since?:string | undefined— RFC3339 — only documents updated at or after this instant.
ListPage
interface ListPage<T = unknown>
One page of Collection.list.
Members
items:StoredDocument<T>[]— The documents on this page.next_cursor?:string | undefined— Pass asListOptions.cursorto fetch the next page; absent on the last page.
LiveError
interface LiveError
A refusal or failure on the live path — always carries a next step.
Members
code:stringreason:stringremediation:stringtopic?:string | undefined
LiveErrorMessage
interface LiveErrorMessage
App → shell: a live-data subscription was refused or failed (a missing
scope, a topic the platform would not serve, an auth failure). The
SDK already tells the app's own code through onError; this copy is
for the BUILDER, which otherwise sees only "Status: error" inside the
preview and cannot say why. Carries the same code / reason /
remediation the app got, so the builder can teach the fix.
Members
code:stringreason:stringremediation?:string | undefinedtopic?:string | undefinedtype:"live_error"v:1
LiveStatus
type LiveStatus = 'connecting' | 'live' | 'error' | 'closed';
Connection state a subscriber sees: connecting until the server acks
the subscription (also while reconnecting), live once it has, error
after a refusal or failure. closed is part of the type but the
current LiveConnection never emits it.
NumbersOptions
interface NumbersOptions
What plant.numbers takes: a set of things, a property, a measure, a window.
Members
as_of?:string | undefined— The plant and its readings as they were at an instant, RFC3339.measure?:string | undefined— Which number: "latest", "avg", "sum", "min", "max", "delta", "count", "time_in_state", "oee", …order?:"asc" | "desc" | undefined— Order the rows: "asc" or "desc".property?:string | undefined— The property to read or rank, e.g. "mh:vibration".set:SetOptionstop?:number | undefined— How many rows to keep; the rest are counted, not dropped.window?:string | undefined— The period: "now", "current_shift", "yesterday", "last_7d", "this_month", a date, a month.
PipelineSubscriber
interface PipelineSubscriber
Callbacks handed to live.subscribePipeline; useExecutionStream builds one for you.
Members
onError?:((e: LiveError) => void) | undefinedonEvent:(e: ExecutionEvent) => voidonStatus?:((s: LiveStatus) => void) | undefined
PlantEvent
interface PlantEvent
An event the plant recorded or worked out: alarm, trip, work_order, mode_change, …
Members
at:stringcleared_at?:string | undefinedid:stringkind:stringname?:string | undefinedpayload?:Record<string, unknown> | undefinedseverity?:"info" | "warning" | "critical" | undefinedsite_id:stringsource?:string | undefinedthing_id:string
PublishInput
interface PublishInput
Input to data.publish.
Members
source?:string | undefined— Optional source label recorded with the point.topic:string— Existing UNS topic path, e.g. "factory/line1/reset".value:unknown— JSON-serialisable value the topic's schema accepts.
PublishResult
interface PublishResult
What POST /api/v1/uns/data/publish returns on success.
Members
message:string— The platform's human-readable acknowledgement.
Reading
interface Reading
A reading of a thing now, as the planner words it: the value, when, how old, how far it can be trusted.
Members
age:string— How old it is, in words.at:string— When it was read, RFC3339.flat?:boolean | undefinedname?:string | undefinedproperty:string— The property id, e.g. "mh:vibration".quality:"good" | "uncertain" | "bad"ref:string— The series it comes from — for kind "uns", the topic path: whatuseTopictakes.source?:string | undefinedstale?:boolean | undefinedtext?:string | undefinedunit?:string | undefinedvalue?:number | undefined
ReadyMessage
interface ReadyMessage
App → shell: handshake complete.
Members
type:"ready"v:1
RecentAlertsOptions
interface RecentAlertsOptions
Options for alerts.recent: activeOnly keeps only alarms still firing; shelved only shelved ones.
Members
activeOnly?:boolean | undefinedlimit?:number | undefinedshelved?:boolean | undefined
SetOptions
interface SetOptions
Which things a plant.numbers or plant.timeline set covers, in match's words, or a plain list of ids.
Members
include_subtypes?:boolean | undefinedthings?:string[] | undefinedtype?:string | undefinedunder?:string | undefined— Everything below this thing in the physical tree.
ShellToAppMessage
type ShellToAppMessage = ContextMessage | TokenMessage | ThemeMessage | DisableMessage | DownloadRefusedMessage;
Every message the shell may post into the app iframe: the initial
context, token rotations, theme flips, disable (kill switch /
unshare) and download_refused. Discriminated on type.
SiteClock
interface SiteClock
A site's clock.
Members
name:stringnow:stringsite:stringzone:string
StoredDocument
interface StoredDocument<T = unknown>
One document as the storage API returns it.
Members
key:string— The document key within its collection.revision:number— Server-assigned revision;putsends it back for compare-and-set writes.updated_at:string— RFC3339 instant of the last write.updated_by:string— The user who last wrote the document.value:T— The stored JSON value.
SubmitPipelineInput
interface SubmitPipelineInput
Input to pipelines.submit.
Members
data?:Record<string, unknown> | undefined— Payload delivered to the run as its trigger data.nodeId:string— The manual-trigger node's id inside that pipeline.pipelineId:string— The pipeline that owns the manual-trigger node.
SubmitPipelineResult
interface SubmitPipelineResult
What the scheduler's trigger endpoint returns on success.
Members
status:string— The scheduler's status word for the accepted trigger.
TelemetryMessage
interface TelemetryMessage
App → shell: one API call's timing (G7). The builder aggregates these into "slow data sources" so latency is a build-time signal, not a production surprise. Sent for every apiFetch; cheap, fire-and-forget.
Members
durationMs:numbermethod:stringpath:stringstatus:numbertype:"telemetry"v:1
ThemeMessage
interface ThemeMessage
Shell → app: a live theme switch (FR-17). The context message carries the initial mode; this one follows every flip the viewer makes in the shell, so an app never has to reload to match its host.
Members
theme:ThemeStatetype:"theme"v:1
ThemeState
interface ThemeState
The theme as the shell sends it: the mode, and the host tokens when the shell could resolve them.
Members
mode:"light" | "dark"palette?:HostPalette | undefined
Thing
interface Thing
One thing the plant knows: a machine, a line, an area, a site.
Members
facts?:Record<string, FactValue> | undefinedid:stringlabels?:Record<string, string> | undefinedname:stringorg_id?:string | undefinedsite_id:stringtype_id?:string | undefined— Its type, e.g. "company:centrifugal_pump".validity?:{ valid_from?: string; valid_to?: string; } | undefined
TimelineEntry
interface TimelineEntry
One entry of plant.timeline: the event and the thing it happened to.
Members
alarm?:AlarmState | undefinedevent:PlantEventsite_name?:string | undefinedthing_name:stringwhy?:string | undefined— Withrank: why it comes first.
TimelineOptions
interface TimelineOptions
What plant.timeline takes: whose events, of which kinds, over which period or state.
Members
as_of?:string | undefined— The model as it was at an instant, RFC3339.kinds?:string[] | undefined— Which kinds of event: "alarm", "trip", "work_order", "mode_change", "due", …; leave it out for all.limit?:number | undefinedrank?:boolean | undefined— Order by what to look at first — severity, then how much sits downstream — rather than by time.state?:"active" | "to_act_on" | "acknowledged" | "shelved" | "standing" | undefined— Live alarms ("active"), or only those to act on, acknowledged, shelved, standing.things?:string[] | undefined— The things to ask about, by id or by name.under?:string | undefined— Everything below this thing in the physical tree.window?:string | undefined— The period: "now", "current_shift", "yesterday", "last_7d", "last_24h", a date, a month.
TokenMessage
interface TokenMessage
Shell → app: a rotated token (silent re-issue, DS4).
Members
token:TokenPayloadtype:"token"v:1
TokenPayload
interface TokenPayload
The scoped app token the shell hands over in context and re-issues in token pushes.
Members
expiresAt:string— RFC3339 — the shell re-issues before this; the app never refreshes itself.value:string— The bearer valueapiFetchand the live socket authenticate with.
TopicMessage
interface TopicMessage
One value delivered on a topic.
Members
quality?:number | undefinedraw:string— The payload exactly as the server sent it.receivedAt:numberretain:booleansource?:string | undefinedtimestamp?:string | undefined— Server-side fields of the enriched envelope (application.MQTTPayload) when present.topic:stringvalue:unknown— The parsed value: the enriched envelope'svalue, else parsed JSON, else the raw string.
TopicState
interface TopicState
What useTopic returns; status and error explain why value may be null.
Members
error:LiveError | null— The last refusal or failure; cleared by the next message.message:TopicMessage | null— The full last message (raw payload, retain flag, envelope fields), or null before the first.status:LiveStatus— Current connection state for this topic.value:unknown— The latest value (the enriched envelope'svalue), or null before the first message.
TopicSubscriber
interface TopicSubscriber
Callbacks handed to live.subscribe; useTopic and subscribeTopic build one for you.
Members
onError?:((e: LiveError) => void) | undefined— Refusals and failures (missing scope, refused topic, auth error, reconnecting, app disabled).onMessage:(m: TopicMessage) => void— Every value delivered on the topic.onStatus?:((s: LiveStatus) => void) | undefined— Connection-state transitions for this subscription.
TreeView
interface TreeView
A tree the plant keeps: a legal value of plant.walk's tree.
Members
default?:boolean | undefinedid:stringlink_types?:string[] | undefinedname:stringtree_kind:string
TypeCount
interface TypeCount
How many things are of a type.
Members
subtypes?:string[] | undefinedthings:numbertype:stringwith_subtypes?:number | undefined
UsageInfo
interface UsageInfo
What Collection.usage returns: how full the collection's scope is.
Members
cap_bytes:number— The quota; a write that would exceed it is refused with a 413.doc_count:number— Number of documents stored.total_bytes:number— Bytes used.
Vocabulary
interface Vocabulary
plant.whatCanIAsk: the plant's vocabulary — its sites, trees, types, and what it measures.
Members
event_kinds?:string[] | undefinedlink_types?:string[] | undefinedmeasured_properties:string[]now:stringsites:SiteClock[]trees:TreeView[]types:TypeCount[]
WalkOptions
interface WalkOptions
What plant.walk takes: a start, and either a tree or the link types to follow.
Members
as_of?:string | undefined— The plant as it was at an instant, RFC3339.depth?:number | undefined— How many hops; leave it out to go as far as it goes.direction?:"downstream" | "upstream" | "both" | "up" | "down" | undefined— Along links: "upstream" / "downstream" / "both"; in a tree: "up" / "down".from:string— The thing to start at, by id or by name.tree?:string | undefined— Follow a whole tree: "physical", "location", "floc", "electrical", "cost_centre" —whatCanIAsklists this plant's.type?:string[] | undefined— Return only these types, having walked through the rest.values?:string[] | undefined— Properties to read for everything reached, in one read — the fleet's values, ranked by you.via?:string[] | undefined— The link types to follow instead of a tree, e.g. ["feeds"].
WalkedThing
interface WalkedThing
One thing a plant.walk reached.
Members
depth:numbername?:string | undefinedreadings?:Reading[] | undefined— Withvalues: the properties asked, read now.site:stringstub?:boolean | undefinedthing:stringtype?:string | undefinedvia?:string[] | undefined— The link types followed to reach it.