This commit is contained in:
2026-10-13 10:02:20 +02:00
parent a93f865e91
commit 80e595c02e
24 changed files with 2210 additions and 4310 deletions
+82 -119
View File
@@ -1,207 +1,170 @@
# Payload types
Every port declares a `dtype`, and every value that passes through it is
checked against that declaration.
Every port declares a `dtype`. Fluksio checks values against it and offers
compatible messages when you bind dashboard widgets.
This is not decoration. It is what lets the dashboard editor offer you only the
messages a gauge can actually draw, what lets the canvas refuse a binding before
anything runs, and what lets a downstream node know the shape of what it is
getting before the flow starts.
Messages carry JSON. Binary data lives in the artifact store; messages carry
references to it.
Everything on the wire is JSON. That is what lets the same value pass through
the state backend, the work queue and the worker protocol unchanged.
## The scalars
## Scalars
| `dtype` | Accepts |
|---|---|
| `float` | any number — `int` or `float`, but not `bool` |
| `int` | a whole number, not `bool` |
| `str` | a string |
| `bool` | exactly `true` or `false` |
| `float` | integer or floating-point number, excluding booleans |
| `int` | integer, excluding booleans |
| `str` | string |
| `bool` | `true` or `false` |
`bool` is an `int` subclass in Python but is not a number here, so a switch
cannot bind to a temperature.
`NaN` and infinity are rejected, including in structured payloads. For an
unavailable numeric result, omit that output or return `None` to publish
nothing. A numeric port does not accept `None` as its value.
`NaN` and infinity are refused, wherever they sit, including inside a `json`,
`record`, `series` or `list`. JSON cannot spell either, so one that travelled
would come back as a response nobody can parse and a row the database rejects,
a long way from the node that made it. An empty subset or a division with no
denominator is what usually produces one; publish `None` instead.
## The structured ones
These are *declared shapes* rather than "some JSON", which is what makes a
widget binding checkable.
## Structured payloads
### `record`
Flat named scalars.
An object with string keys and scalar or null values. Nested objects and
lists are not allowed.
```json
{"title": "Boiler", "body": "Pressure low", "severity": "warning"}
```
A record cannot contain a record, so its shape is readable from the declaration
alone.
Read by the **Notification** widget. It is also what an alert channel of kind
*dashboard* writes.
Notification and Player widgets read records. Dashboard alert channels also
publish them.
### `list`
Ordered items of one declared shape. The port also declares `item`:
An array with a declared item type:
| `item` | Meaning |
| `item` | Accepts |
|---|---|
| unset | `record` — what the agenda and forecast widgets read |
| `float`, `int`, `str`, `bool` | a list of scalars |
| `json` | anything |
| unset or `record` | flat records; used by Agenda and Forecast |
| `float`, `int`, `str`, `bool` | scalars of that type |
| `json` | arbitrary JSON values |
A list of lists, or a list of series, is refused. One declared level is the
point.
`list` and `series` cannot be declared as item types. Use `item: "json"` if
items need arbitrary nested structures.
### `series`
Labelled lines of `(timestamp, value)` pairs, which is what a chart draws.
Labelled lines of numeric `[timestamp, value]` pairs:
```json
{
"lines": [
{"label": "living", "points": [[1717000000, 21.4], [1717000060, 21.5]]}
],
"range": "-24h"
"range_s": 86400,
"interval_s": 60
}
```
Keys beside `lines` are carried through untouched, which is how a querying
chart puts the window and resolution it asked for on the request and reads them
back off the answer. That is what stops an answer to a *different* question
from overwriting the picture.
Additional top-level keys are preserved. A Query chart expects the
`range_s` and `interval_s` from its request in the response; this prevents
an answer for another window from replacing its chart.
`GET /runs/series/compare` answers in this shape, which is why comparing three
training curves is a widget binding rather than a screen of its own.
The run-series comparison endpoint also returns this shape.
### `artifact`
A reference to stored bytes.
A reference to bytes in the content-addressed artifact store:
```json
{"digest": "sha256:…", "size": 4194304, "media_type": "application/octet-stream", "name": "weights.pt"}
```
Binary payloads (tensors, checkpoints, images) never travel as a message. The
bytes go to a content-addressed store and the message carries this. A
thirty-megabyte checkpoint never sits in the state backend, and the reference
stays valid wherever the store is reachable from, including on another machine.
`digest` and `size` identify the content. `media_type` and `name` describe it.
The reference works wherever the store is reachable.
Node code produces one with `fluksio.save_artifact` and opens one with
`fluksio.load_artifact`. See [Writing node code](../code/nodes.md#bytes-artifacts).
Create one with `fluksio.save_artifact` and read its bytes with
`fluksio.load_artifact`. See [Artifacts](../code/nodes.md#bytes-artifacts).
### `image`, `audio`, `video`
The same reference, narrowed to a kind of media by its `media_type`.
Artifact references restricted to the matching `media_type` family:
```json
{"digest": "sha256:…", "size": 61344, "media_type": "image/jpeg", "name": "frame.jpg",
"meta": {"width": 1280, "height": 720, "seq": 41}}
{
"digest": "sha256:…",
"size": 61344,
"media_type": "image/jpeg",
"name": "frame.jpg",
"meta": {"width": 1280, "height": 720, "seq": 41}
}
```
An `audio` port takes `audio/*` and refuses anything else, so a speech
recogniser declares what it eats rather than taking any bytes at all and
finding out. An `artifact` port still accepts all three: media narrows
artifact, not the other way round.
An `audio` port accepts `audio/*`; `image` and `video` work the same way.
An `artifact` port accepts all three. Optional `meta` fields are for the
consumer; Fluksio does not interpret them.
`meta` is optional and nothing here reads it: sample rates, dimensions and
sequence numbers are for whoever consumes the media.
A media stream is a [streaming port](../concepts/flows.md#streaming-ports)
publishing one reference per frame or chunk. Saving with
[`volatile=True`](../code/nodes.md#media) keeps bytes in memory and lets local
websocket clients receive them with the value. Stored media and portal views
fetch the bytes separately.
Bytes still never travel as a message. A camera publishes one reference per
frame and a microphone one per chunk, which makes a media stream an ordinary
[streaming port](../concepts/flows.md#streaming-ports). What that costs
depends on how the bytes reach the screen:
| Rate | Where it works |
|---|---|
| A clip a second (speech) | anywhere, including through the portal |
| Ten frames a second (a camera worth watching) | on the local network, with `volatile=True` |
| A frame every second or two (a glance at a door) | anywhere, including through the portal |
| Full-rate video, 30 fps and up | not here — see below |
The difference is one flag. A frame saved with
[`volatile=True`](../code/nodes.md#media) is held in memory and pushed down the
websocket in front of the value naming it, so a screen draws it without asking
for anything. A stored one is fetched instead: a round trip per frame, which is
a glance rather than a view. Through a portal every frame is fetched, so a
remote panel is in the second row whatever the flag says.
Above that, video is not a message-plane problem — every frame would still be
an event. Point a media widget's **stream URL** at whatever the camera already
serves and the browser plays it from source; the messages then carry the
occasional still, and the flow reacts to those.
Use occasional snapshots through a portal. For continuous video, point a
Media widget's **Live stream** at a browser-compatible source URL reachable
by the screen.
### `json`
Anything JSON-serializable. The escape hatch, and the right answer when a
payload genuinely has no fixed shape.
Any JSON-serializable value. Use it for payloads without a fixed shape.
Prefer a specific type when one fits; it gives widgets and other authors a
clearer contract.
Reach for it last. A `json` port tells the canvas, the widget picker and the
next author nothing.
## Referencing a run's output
## Naming a run's output
A run parameter can name an existing value instead of repeating it:
Any *run parameter* is also accepted as text, since nobody wants to paste an
object into a shell. `@run:<id>.<output>` names what an earlier run produced,
whatever its type, an `artifact` reference or a `json` config alike. A bare
`sha256:…` digest names content in the artifact store. Both resolve before the
run starts, so the CLI, the run dialog and a python caller all mean the same
thing by the same string.
- `@run:<id>.<output>` resolves an earlier run's output.
- `sha256:…` resolves an artifact digest.
Both spellings are reserved on every input, `str` included: an input that has to
carry one of them literally is asking for a value this engine reads as a name.
These strings resolve before the run starts, regardless of the input's type.
Both prefixes are reserved even for `str` inputs; they cannot be passed as
literal text in run parameters.
## What a widget will bind to
## Widget bindings
| Widget | Accepts |
|---|---|
| Gauge, Chart, Slider, Bar | `float`, `int` |
| Gauge, live Chart, Slider, Bar | `float`, `int` |
| Query Chart | `series` response, `record` request |
| Switch | `bool` |
| Agenda, Forecast | `list` |
| Notification | `record` |
| Value | anything |
| Icon | weather strings, booleans and numbers alike |
| Notification, Player | `record` |
| Colour | `list` for HSV/RGB, `str` for hex |
| Media | `image`, `audio`, `video`, `artifact` |
| Clock, Text | nothing — they bind to no message |
| Value, Icon, Button, Buttons, Input, Selector | no fixed type restriction |
| Clock, Text, Website | no message binding |
Enforced on the server as well as in the editor.
Player transport commands publish to a separate `str` message.
Binding checks apply in the editor and on the server.
## Icon and colour names
A forecast is a `list` of records, one per column, each shaped
`{label, icon, value}` with an optional `color`. The flow decides what a
column is called and which glyph it gets; the widget only draws it.
Forecast reads a list of records with `label`, `icon` and `value`, plus
optional `color`:
```json
[{"label": "Today", "icon": "cloud-rain", "color": "primary", "value": "14°"}]
```
`icon` and `color` are names from a fixed vocabulary, the same one the icon
widget's picker offers:
The Icon widget uses the same vocabulary:
| | Names |
|---|---|
| Icons | `sun`, `moon`, `cloudy`, `cloud-rain`, `cloud-drizzle`, `cloud-snow`, `cloud-lightning`, `cloud-fog`, `wind`, `umbrella`, `snowflake`, `droplets`, `thermometer`, `thermometer-sun`, `flame`, `fan`, `sun-snow`, `door-open`, `house`, `bed`, `lightbulb`, `plug`, `music`, `volume-2`, `volume-x`, `zap`, `battery-charging`, `arrow-up`, `arrow-down`, `triangle-alert`, `check`, `circle-check` |
| Icons | `sun`, `moon`, `cloudy`, `cloud-rain`, `cloud-drizzle`, `cloud-snow`, `cloud-lightning`, `cloud-fog`, `wind`, `umbrella`, `snowflake`, `droplets`, `thermometer`, `thermometer-sun`, `flame`, `fan`, `sun-snow`, `door-open`, `house`, `bed`, `lightbulb`, `plug`, `cctv`, `music`, `volume-2`, `volume-x`, `zap`, `battery-charging`, `arrow-up`, `arrow-down`, `triangle-alert`, `check`, `circle-check` |
| Colours | `default`, `muted`, `primary`, `success`, `danger` |
An empty icon draws nothing. An unknown one draws a placeholder glyph and an
unknown colour draws as `default`; either warns once in the browser console,
listing the names it expected.
An empty icon draws nothing. Unknown icons use a placeholder; unknown colours
use `default`. Each warns once in the browser console.
## Type failures
A value that does not match its port's declaration raises on the node that
published it, naming the port and what arrived. It does not get published, and
it does not reach anything downstream. A wrong value stopping at its source is
easier to diagnose than one propagating.
A mismatched value raises an error on the node that produced it, naming the
port and received value. It is not published downstream.
## See also