Message Bus New in 1.0.8
The message bus carries named messages between the three places logic runs in QUBIQ: pipelines, Designer views, and scripts. A button on a screen can start a pipeline; a pipeline can update every screen that is listening; a script can ask a pipeline a question and wait for the answer. None of them needs to know where the others are.
It replaces the 1.0.7 arrangement, in which a view's Send message stayed inside the browser tab
and a separate system.message.send pushed from the gateway to browsers. There is now one bus and
one call shape everywhere.
Subjects
A message is addressed by a dotted subject: line1.motor.start, batch.released,
report.ready.
| Rule | Detail |
|---|---|
| Segments | Letters, digits, _ and -. No spaces, no other punctuation. |
| Length | At most 200 characters and 16 segments. |
| Sending | An exact subject — no wildcards. |
| Receiving | A pattern: * matches exactly one segment, > matches one or more trailing segments and must come last. |
| Pattern | Hears |
|---|---|
line1.motor.start | Exactly that subject |
line1.*.start | line1.motor.start, line1.pump.start — not line1.motor.a.start |
line1.> | Everything under line1 |
Design subjects like tag paths: general to specific, so a receiver can take a whole branch with
one >.
Scope
Every message belongs to the project it was sent from. The server fills the project in from the
sender's verified identity — the pipeline's own project, the operator's session, the script's run —
so nothing a browser or a script writes can address another project, and a receiver's wildcard can
never reach past its own project, not even with >.
| Scope | Reaches |
|---|---|
All projects (all) — the default | Receivers in every project that listen for broadcasts |
This project (project) | Receivers in the sender's project only |
A view can also send locally — to one component, this view, or this browser tab — and a local message never leaves the browser. → Send message
all is the default for every sender, so a message sent without a scope reaches every project on
the server and every pipeline listening on that subject. Pass This project (project) unless
the message genuinely concerns the whole site.
Send, and request/reply
A sender either sends — fire and forget, returning as soon as the message is on the bus — or requests: it waits for the first reply.
| Default | Range | |
|---|---|---|
| Request timeout | 5000 ms | 100–60000 ms |
A request fails rather than hanging:
- Nobody is listening — it fails at once with "no receiver is listening for this request", without waiting out its timeout.
- The receiving pipeline finished without replying — it fails at once, saying why: the run failed (with the error), or no Message Reply was reached. A pipeline that holds a Timer node is the exception, since a deferred branch may still reply; then the request waits for its timeout.
- The receiver rejected it — the error it sent back, plus its details (see Errors that travel back).
- No reply in time — "request timed out waiting for a reply".
Only a pipeline answers a request. A view's On message handlers hear sent messages, never requests, so a request whose subject only views listen on fails with no receiver.
Pipelines
Three nodes in the Message Bus category of the palette:
| Node | Does |
|---|---|
| Message In | Starts the pipeline when a message arrives on its Subject pattern. The payload is the message's payload; msg.topic is the exact subject that matched. |
| Message Out | Sends the incoming msg.payload on a Subject — or, in Request mode, waits for a reply and outputs it. |
| Message Reply | Answers the request that started this run, with msg.payload or with an error. |
Message In settings:
| Setting | Default | Meaning |
|---|---|---|
| Subject | — (required) | The pattern to listen on. |
| Also receive messages sent to all projects | On | Off: only this project's messages. |
| Load-sharing group | Empty | Empty: every receiver gets its own copy. Set: receivers in this project with the same group share the messages, each handled once. |
Message Out settings:
| Setting | Default | Meaning |
|---|---|---|
| Subject | Empty | An exact subject. Empty reuses the incoming msg.topic — handy downstream of a Message In. |
| Send to | All projects | Or This project. |
| Mode | Send — fire and forget | The input passes through. Request — wait for reply: the reply becomes the output, and a timeout, no receiver or an error reply fails the node, which a Catch can handle. |
| Timeout (ms) | 5000 | Request mode only. |
A Message Out in Request mode is never retried: a request that timed out may still have been acted on.
Message Reply answers the sender of the message that started the run. If that sender did a plain send there is nothing to answer, and the message passes through. In a run that a Message In did not start, the node fails.
Message In ("motor.status") → SQL Query (latest state) → Message Reply (OK)
const status = await context.message.request("motor.status", { id: "m1" }, { scope: "project" });
When the receiving pipeline is too busy to take another message, a requester is told "receiving pipeline is busy" straight away.
Errors that travel back
Set Message Reply's Reply with to Error — reject the request and wire a Catch node into it, and the error the Catch caught goes back to the sender: its message (unless you type one in Error message) plus the details — what failed and its traceback. Without a Catch, the sender gets the Error message text, or "request rejected".
Message In ("calc.run") → Python Script → Message Reply (OK)
Catch ──────────────────────────────────→ Message Reply (Error)
try {
await system.message.request("calc.run", { x: 1 });
} catch (err) {
console.log(err.message, err.details?.source?.name); // the node that failed
}
In Python the exception is a SystemCallError, and the same details are on its details
attribute.
Views
A Designer Send message action sends on the bus with a subject, a scope, and optionally request mode with a Reply to client variable that keeps the answer for a widget to bind to. An On message handler on any widget listens on a pattern. → Send message · On message
A browser registers the patterns its handlers listen on, and the server forwards each one only the messages it handles — a busy pipeline does not flood every open screen. A network message reaches every open browser in scope, so a handler fires once per screen that has it open.
Scripts
The same (subject, payload, opts) shape in every runtime:
| Call | Where | Does |
|---|---|---|
system.message.send(subject, payload?, opts?) | Python and JavaScript | Send on the bus. opts.scope: all (default) or project. |
system.message.request(subject, payload?, opts?) | Python and JavaScript | Send and wait for the reply. opts.timeoutMs as above. |
context.message.send(subject, payload?, opts?) | Browser | As system.message.send, plus the local scopes tab, view and component. |
context.message.request(...) | Browser | As system.message.request. |
context.message.onMessage(pattern, cb) | Browser | Listen from a script. |
→ system.message · context.message
Limits and safeguards
| Limit | Value | Why |
|---|---|---|
| Payload | 256 KB, JSON | The bus carries events, not files — save a file and send its URL. |
| Hops | 8 | A message sent because one was received carries a hop count. The ninth re-send is dropped, so two receivers answering each other cannot loop forever. |
| Browser send rate | 50 per second, bursts of 100 | Bounds a view-side loop, which no call-depth guard can see. |
Who may send and listen:
- Browsers send through their own session. An operator needs the Operate write level, the same as any other action that can change something — a message can start a pipeline that writes tags. An anonymous visitor of a public project can neither send nor listen.
- Scripts need the
script:executescope. Python scripts do not publish on the bus themselves; they ask the message service in QBQ-RealtimeGateway, which resolves the project from the database.
What changed from 1.0.7
Views are migrated the first time they are opened: a Send message action's type becomes its subject
with every character a subject cannot hold turned into _ (Motor Start → Motor_Start),
handlers are renamed the same way, and the old in-browser scopes become This browser tab only.
Script code is not rewritten:
- A subject with a space,
:or/is refused.system.message.send(...)and a networkcontext.message.send(...)reject with "may only contain letters, digits, '_' and '-'"; a localcontext.message.send(..., {scope: "tab"})orcontext.message.onMessage(...)only logs that warning to the browser console. Use the subject the migrated handler now has. scope: "session"or"page"incontext.message.sendis not delivered — use"tab".system.message.send(..., {"scope": "user"})fails: the bus cannot deliver to one user. Send withprojectand put the user in the payload for the receiver to check.- A script that sends with no scope now broadcasts.
context.message.send("reload", p)stayed in the tab in 1.0.7; it now reaches every project. Add{scope: "tab"}to keep it local. - The
projectId,userIdandfromoptions of the oldsystem.message.sendare gone. The project is always the sender's own.
The Link node is confined to its own project as well: a Link-in that names a Link-out from another project hears nothing.