Skip to Content
This documentation is provided with the HEAT environment and is relevant for this HEAT instance only.
RunnersSystem Utilssystem-arbex-js

system-arbex-js (platform, system-utils)

system-arbex-js is a platform-only transform node template on system-utils. It runs sandboxed JavaScript (Node.js) configured on the node instance, reads upstream data through typed input readers, and writes a JSON arbex envelope as its node output.

Typical pipeline:

upstream → system-arbex-js → downstream (reads envelope JSON)

Authoring types and examples live in the monorepo under core/api/Runner/Arbex/sdk/. External IDE corpora: tools/arbex/rag/.


Output envelope and node status

Every run produces:

FieldDescription
outputJSON-serialisable return value from run(ctx), or error object
consoleCaptured log / info / warn / error
statusSUCCESS or ERROR
metrics.executionTimeMsWall time
configurationSnapshotBase64Base64 (UTF-8) of the full node configuration JSON at execution time (script, parent, limits). Lets UIs show which code produced a historical output after the live config is edited.

When dataserviceOutput.persistence is dataservice-root, the stored file is the dataservice root plus a sibling $heat-arbex-run object with configurationSnapshotBase64, status, console, metrics, and profiler (envelope fields that would otherwise be dropped).

Runtime profiler (profiler)

Enabled by default. While run(ctx) executes, the Node host samples about every 50ms (configurable):

FieldSourceMeaning
profiler.samples[].heapUsedBytesV8JS heap used
profiler.samples[].rssBytesNodeProcess resident set
profiler.samples[].cpuUserMicros / cpuSystemMicrosNodeCPU time in the interval
profiler.samples[].locationStack (best effort)Top frame in arbex-user-script.js (line/column) when the event loop ticks
profiler.hotspots[]V8 CPU profile (end of run)Per line/function/column with selfTimeUs, totalTimeUs (inclusive stack); up to 50 entries
profiler.processSamples[]C# hostChild process working set and CPU% (includes bridge IPC)

Config (optional):

{ "profiler": { "enabled": true, "sampleIntervalMs": 50, "maxSamples": 600 } }

Limits: Samples run on the Node event loop. Long synchronous loops block ticks, so line/heap samples may be sparse until the script awaits or finishes. Use hotspots for aggregate CPU line cost; use samples for memory and async-time line hints. The Arbex IDE can chart atMs vs heapUsedBytes and scrub location.line.

statusNode LastState
SUCCESSProcessingSucceeded
ERRORProcessingFailed

Downstream nodes can inspect status even when the node is marked failed.

Host failures (no usable envelope from Node)

When the C# host cannot read or parse the child process envelope, status is ERROR and output includes structured diagnostics:

FieldDescription
output.messageShort summary (for example Node process exited unexpectedly)
output.reasonMachine-readable code: empty_stdout, deserialize_failed, no_envelope_line, timeout, memory_limit, host_not_found, …
output.detailsFull human-readable explanation
output.exitCodeNode process exit code when applicable
output.hostDiagnosticsrpcLineCount, stderrPreview, stdoutPreview, parse failure detail, …
console[]Same text as details, plus a separate stderr: entry when Node wrote stderr

The system-utils runner also logs a Warning with the summary and sets the node status message to system-arbex-js failed: … (not only ERROR). Check the persisted output JSON or agentic-cli output <id> first; pod logs are a secondary hint.


Node configuration

PropertyRequiredDefaultDescription
scriptyes,JS defining async function run(ctx) { ... }
parent.nodeInstanceNameyes,Parent node graph name
parent.modeyes,latest or history
parent.historyLimitno60Max outputs listed (metadata only)
parent.historyOrdernonewestFirstnewestFirst or oldestFirst
parent.readerHintnoautoauto | json | tabular | binary
parent.tabularOptionsno,{ delimiter, inputFormat } for CSV/TSV
timeoutMsno5000Kill Node child after this many ms
maxScriptCharsno131072Max script size
maxMemoryMbno512C# watchdog kills Node before pod OOM (max 640; system-utils pod limit 768Mi)

Example:

{ "script": "async function run(ctx) { return { ok: true }; }", "parent": { "nodeInstanceName": "metrics-upstream", "mode": "latest" }, "timeoutMs": 10000, "maxMemoryMb": 512 }

run(ctx) context

Scripting API (context, heat.dataservice, heat.layout, pipelines): Arbex API reference.

PropertyDescription
apiVersion"1.0"
sessionid, projectId, simulationName, template, configuration, nodes[] (no email/display name)
session.nodes[]{ instanceId, instanceName, templateName, lastState, statusDetails } for every node in the session
systemOperator triage helpers (on by default; see below)
nodeinstanceId, instanceName, templateName, taskId, configuration
parentResolved parent config block
parents{ name, templateName, instanceId }[]
inputs.latestSingle handle when mode: latest (or null)
inputs.historyIterable handles when mode: history

Input handles

MethodReturns
openJsonReader(){ kind: 'json', root }
openTabularReader(opts?){ kind: 'tabular', headers, readRow(), [asyncIterator] }
openBinaryReader(){ kind: 'binary', byteLength, base64, contentType }

detectedKind hints: json | tabular | binary. Only one reader open per handle at a time.

HEAT_ARBEX symbols and heat helpers

  • HEAT_ARBEX.isJsonReader(r) / isTabularReader / isBinaryReader / isInputHandle
  • heat.getPath(obj, dottedPath)
  • heat.coerceNumber(v, fallback?)
  • heat.parseIso8601(s)
  • heat.jmespath.search(expr, data) (also jmespath.search global)
  • heat.dataservice.createBuilder(options) , build schema-valid $heat-dataservice payloads for the Next dashboard (direct ingest)

Symbol keys: heat.arbex.reader.json, .reader.tabular, .reader.binary, .input.handle, etc.


system context (live cluster triage)

Platform operator use. On every system-arbex-js node, ctx.system is available by default with read-only helpers backed by the system-utils Kubernetes service account and in-process platform configuration. Set system.enabled: false to disable.

{ "script": "async function run(ctx) { ... }", "parent": { "nodeInstanceName": "upstream", "mode": "latest" }, "system": { "k8s": { "namespaces": ["heat"], "maxItems": 200 }, "configPrefixes": ["system."] } }
PropertyDescription
system.enabledDefault true. Set false to omit ctx.system and block system.* bridge ops
ctx.system.versionAlways present when system context is on: platform build metadata (version, buildUtc, commitSuffix, …), same shape as GET /version
system.k8s.namespacesNamespace allowlist for list calls. When omitted, defaults to the in-cluster pod namespace
system.k8s.maxItemsCap per list operation (default 200, max 2000)
system.configPrefixesAllowlisted platform config prefixes for ctx.system.config.getPrefix. Required for config reads

ctx.system.version

Structured platform build metadata from build-info/heat-platform-build.json (generated by deploy/build_all.sh). No bridge call; snapshot at run start.

const { version, commitSuffix, buildUtc } = ctx.system.version; return { platformVersion: version, commit: commitSuffix, builtAt: buildUtc };

ctx.system.k8s

Summaries use the same JSON shapes as cluster-report (not raw Kubernetes objects).

MethodDescription
listPods({ namespace?, labelSelector? })Pod summaries
listDeployments({ namespace? })Deployment summaries
listStatefulSets({ namespace? })StatefulSet summaries
listPersistentVolumeClaims({ namespace? })PVC summaries
listIngresses({ namespace? })Ingress summaries
listServices({ namespace? })Service summaries
listNamespaces()Namespace names and phases
listNodes()Cluster node summaries
getPodMetrics()Raw metrics.k8s.io pod metrics object

namespace must appear in system.k8s.namespaces when that allowlist is configured.

Example:

async function run(ctx) { if (!ctx.system) return { error: "system disabled (system.enabled: false)" }; const ns = ctx.system.k8sNamespaces[0]; const pods = await ctx.system.k8s.listPods({ namespace: ns }); const failing = pods.filter((p) => p.phase !== "Running"); return { failingCount: failing.length, failing }; }

ctx.system.k8s.exec (opt-in)

Pod exec is disabled by default. Enable explicitly on the node instance:

"system": { "k8s": { "namespaces": ["heat"], "exec": { "enabled": true, "allowClusterManager": true, "podNamePrefixes": ["cluster-manager-"], "timeoutMs": 30000, "maxOutputChars": 65536 } } }
MethodDescription
execClusterManager({ namespace?, container?, command[] })Finds a Running app=cluster-manager pod and runs argv (one-shot stdout/stderr)
exec({ namespace?, podName, container?, command[] })Exec into an allowlisted pod name prefix

command must be an argv array (not a single shell string), for example ["/bin/sh", "-c", "curl -s localhost:3000/health"].

Returns { podName, namespace, container, stdout, stderr, exitCode }. Requires system-utils RBAC pods/exec and in-cluster execution.

ctx.system.config

When system.configPrefixes is set, ctx.system.config.getPrefix(prefix) returns { name, valueType, value, description }[]. Values containing secret or connectionstring in the key name are redacted (**REDACTED**). Only prefixes listed in node config are allowed.


Optional: native $heat-dataservice output

Scripts can emit a dashboard v2 payload without a separate heat-system-dataservice-envelope node. The builder supports every direct-ingest channel shape (not only time series):

shapeBuilder methodUse case
seriesds.series(spec, points)Time-varying numeric or categorical playback
valueds.value(spec, payload)KPIs, bar chart data, arbitrary JSON under data.value
eventsds.events(spec, points)Instantaneous boolean events
timestampsds.timestamps(spec, points)Labelled instants
rangesds.ranges(spec, points)Phases / intervals

Per-channel realm (optional) maps to realms[].name. metadata on the channel spec is passed through to widgets (units, channelType, and so on).

async function run(ctx) { const ds = heat.dataservice.createBuilder({ defaultRealm: "default" }); ds.addGroup("ops", "Operations"); ds.value({ id: "kpis", name: "KPIs", groupId: "ops" }, { podCount: 4 }); ds.series({ id: "cpu", name: "CPU", groupId: "ops", realm: "run-a" }, [ { timeMs: 1000, value: 1.2 }, ]); return ds.buildRoot(); // { $heat-dataservice, dashboard_users: [] } }

Node configuration: dataserviceOutput

PropertyDefaultDescription
enabledfalseValidate script output when it contains $heat-dataservice
persistencearbex-envelopearbex-envelope | dataservice-root | dataservice-in-envelope
dashboardUsers[]Applied when the script omits dashboard_users
layoutOutput.enabledfalseWhen true, validate suggestedLayoutConfiguration on the script return (v2 layout)
layoutOutput.requirefalseWhen true with layoutOutput.enabled, the script must include suggestedLayoutConfiguration
  • dataservice-root: node output file is the root object (for heat-system-next-dimension / Next dimension downstream).
  • arbex-envelope / dataservice-in-envelope: file is the arbex envelope; validated dataservice lives in output.

C# reuses the same normalization as heat-system-dataservice-envelope. See dashboard v2 contract and Next data service (UI).

Example: sdk/examples/dataservice-multi-shape.js


Layout builder (heat.layout)

heat.layout builds v2 layoutConfiguration JSON for heat-system-next-dimension (Next dashboard grid). Canonical sources live in ui/lib/heat-layout-builder/ and are synced into the arbex node host before core-api image builds (deploy/sync-arbex-layout-lib.sh).

APIDescription
heat.layout.createBuilder(options?)Start a layout; options include version, realms, configuration
.addRow(opts?)Append a grid row
.addComposableChart(spec)Fluent ComposableChart column with slice helpers (addAreaSlice, addEventsLaneSlice, …)
.addColumn(componentId, config)Generic v2 column (BarChart, MapDisplay, …) with widget keys such as barChartItem
.build()Returns layout JSON matching the layout schema

Emitting layout with data (experimental loop)

The platform already supports suggestedLayoutConfiguration on dataservice-root outputs (same key as system-tabular-to-dataservice with emitSuggestedLayout). Pair with heat-system-next-dimension layoutFromParentOutput: true so one arbex run drives both channels and grid layout.

  1. Node config:
{ "dataserviceOutput": { "enabled": true, "persistence": "dataservice-root", "layoutOutput": { "enabled": true, "require": true } } }
  1. Script return:
const layout = heat.layout.createBuilder({ version: "1.0.0", realms: ["default"] }) .addRow() .addComposableChart({ name: "c", titleContent: "Chart", channels: ["demo-metric"], colspan: 12 }) .addAreaSlice({ order: 1, height: 220 }) .addSeries({ id: "s1", label: "Series", channel: "demo-metric" }) .endComposableChart(); return ds.buildRoot({ suggestedLayoutConfiguration: layout });
  1. Dimension node: layoutFromParentOutput: true and a minimal fallback layoutConfiguration stub (see system-tabular-dashboard-autodetect-next).

Static layoutConfiguration on the dimension node remains the preferred production path; layoutOutput is for iteration without editing the dimension config each time.

Example: sdk/examples/layout-composable-from-arbex.js


Sample session template

Shipped preset sample-arbex-composable-dashboard (Name: sample-arbex-composable-dashboard):

sample-input (input-node, any upload) → sample-arbex (system-arbex-js, dataserviceOutput.persistence: dataservice-root) → sample-dashboard (heat-system-next-dimension, layoutFromParentOutput)
  1. Upload any file to sample-input (JSON is parsed when detected; other types contribute descriptor metadata only).
  2. sample-arbex reads the latest parent output, writes channel input-summary (shape: value), synthesizes random series on channel synthetic-metric (seeded from session id), and returns suggestedLayoutConfiguration (ComposableChart via heat.layout).
  3. sample-dashboard opens a Next dimension using layoutFromParentOutput: true (fallback layoutConfiguration stub is empty rows).

Script source (synced with the template): sdk/examples/sample-arbex-composable-dashboard.js.


Cookbook

Scripts live in sdk/examples/:

  • A: latest JSON + JMESPath
  • B: latest tabular row aggregation
  • C: history over many CSV uploads
  • D: node config field extract
  • E: binary metadata
  • F: history with type guards
  • G: native $heat-dataservice (all channel shapes)
  • H: sample-arbex-composable-dashboard (upload + synthetic series + heat.layout + layoutFromParentOutput)
  • I: v2 layout builder (heat.layout) with ComposableChart slices

Use async function run(ctx) because readers are async over the C# bridge.


Editing outside HEAT

  1. Start with the Arbex API reference, then use types from the Arbex SDK (heat-arbex.d.ts) and examples under tools/arbex/rag/.
  2. Add /// <reference path="heat-arbex.d.ts" /> in your script when using an external IDE.
  3. Paste into the node script configuration after testing logic locally.

Scripts run in a sandboxed Node host inside the platform runner deployment.


Security and limits

  • User script runs in a separate Node process with stripped env (no DB/S3 credentials).
  • vm context whitelist: no require, process, fs, or network.
  • C# memory watchdog terminates the child and writes an ERROR envelope before Kubernetes OOM kills the pod.
  • Template authors with config edit rights are trusted; this is not a multi-tenant isolation boundary.

See also


system-json-field-to-binary (planned): extract a dotted path from a JSON parent (for example output.csv from an arbex envelope) and register a binary node output for tabular or file consumers.


Changelog

apiVersion 1.0

  • Initial system-arbex-js with Node host, bridge readers, envelope contract, default maxMemoryMb 512.