Unconnected mandatory input
The Operation requires an Input, but nothing is wired to it. The compiler flags missing Connections on Mandatory Ports, but the runtime checks again — a task loaded in an unexpected state still fails cleanly.
Every Operation in Nonbl runs as one Node of an Execution Path. When it can’t produce a valid result, you decide whether the Path stops, continues with its previous output, or emits a fallback value. You also decide, per Input, whether the Operation waits for upstream data to be ready before it runs at all.
This guide covers every mechanism that controls what happens when things don’t go right: the failure categories, the per-Operation On Error verb, the error output every Operation that can fail carries, the per-Input Validation dropdown, and the Gate Input that explicitly stops a Path.
Path and branch are the two words everything below is said in; Automation Model defines them.
A Node passes, stops, or fails. A failure is turned into a stop or a continue by the Node’s On Error, and it is what the Node’s error output carries — see The error output.
Inside an Operation’s logic there are exactly three ways a run can fail:
Unconnected mandatory input
The Operation requires an Input, but nothing is wired to it. The compiler flags missing Connections on Mandatory Ports, but the runtime checks again — a task loaded in an unexpected state still fails cleanly.
Input type mismatch
The Input carries a value the Operation can’t work with — for example, Math received a Nil on a Data Port, or JSON Parser received non-UTF-8 binary.
Operation-level runtime error
The Operation’s own logic can’t finish — division by zero in Math, a JSON parse failure in JSON Parser, a SQL error in SQLite Storage, and so on.
All three are failures and funnel into the same answer: the On Error verb you picked for that Operation resolves it (see On Error: three verbs).
The per-Input Validation rules and the Port type masks are checked before the Operation runs. A rule that is not satisfied stops the Node: it waits, its dependents do not run this firing, and nothing is written. A type the Node cannot read fails it, and the Node’s On Error verb resolves that exactly as it resolves a failure the Operation reports itself. See Per-Port Input Validation.
Separately, any Operation that takes a Gate Input can be stopped by closing that Gate — this is not a failure, just an explicit “don’t run this time”. See Gate Inputs.
Many Operations accept an optional Gate Input, marked with a > label ahead of the Operation’s own Inputs. Each Operation’s own page says whether it has one. Wiring a Gate has one meaning: stop this Execution Path when the Gate value is false or Nil.
If you don’t want that, don’t connect a Gate. There is no per-Operation toggle to change this behaviour.
A Gate closure is not a failure:
Typical uses:
Gate op sits in the middle of a path to turn the rest of the path on/off from a condition upstream.Compare op’s Gate lets you evaluate the comparison only when some prerequisite Boolean is true.MQTT Action’s Gate publishes only when some condition holds.Almost every Operation carries an On Error property, chosen from three verbs. The default is Stop this branch. Change it on any Operation whose failure should not take the rest of its branch down with it.
An Operation that can never fail — one with no Input to check and no completion that can fail — shows no On Error control at all, because the verb would never be consulted. The Operation’s own page says so.
The verb combines with the per-Output Fallback value (described below). Together they give you three ways to answer a failure:
Stop this branch (default)
This Node and its dependents stop; each Output keeps its value and reads as a failed update. Nodes in the same run that do not depend on this one still run, and other Paths in the Task are unaffected — so this stops a branch, never the Task. Pick it when a failure means whatever reads from this Operation would be acting on bad data.
Continue with the last value
Dependents run on the value each Output already holds, not stamped as new; it reads as a failed update. Pick this when the last good value is useful and a one-off glitch should not interrupt the branch.
Continue with the fallback
Dependents run on each Output’s per-output Fallback (see Per-Output Fallback Value below), stamped as this run’s value; it reads as a failed update. The Editor refuses a Task where the Fallback is unset. Pick this when a sensible substitute lets the branch keep flowing.
Whichever verb resolves the failure, the Operation’s last attempt reads as failed — a consumer on another Path asking whether the last update succeeded is told the truth, and one asking whether it failed is admitted. See The verb meets downstream Validation below for the full interaction, including the compile-time hint that flags the gap between a Node that continues after a failure and a downstream Input that is not checking.
Each Output Port carries its own Fallback value, always editable in the Node tab’s Outputs section — the editor is shown unconditionally, regardless of which On Error verb you’ve picked. Default is empty (Nil).
You pick the type — Boolean, Integer, Float, or String — and enter the value. The chosen MessagePack value is what the Operation writes to that Output whenever the verb resolves to Continue with the fallback. A Node set to Continue with the fallback must have a Fallback on every Output it would write; the Editor refuses the Task otherwise ({} continues with the fallback after a failure, but output {} has no Fallback set. Set one on that output, or give the node a different On Error verb.).
Nil.The On Error verb is consulted only for the three failure categories described above. Closing a Gate stops that branch immediately, with no verb to ask and no Fallback write.
An Operation’s own schema declares which of the three verbs it offers, and the On Error dropdown in the Editor shows only that subset. A sink, and an Operation whose failure has nothing downstream to carry a value to (Throttle), offers only Stop this branch and Continue with the last value.
Logger and MQTT Action are sinks: they write no Output, so there is nothing for a fallback to stand in for. Throttle, Delay and Boost are the second shape — their Outputs belong to the Path their completion fires, not to the Path that reaches them, so a failed arm attempt has no Output of this Path’s to substitute. REST API Call and Webhook carry all three verbs. The Operation’s own page says which it offers.
For JavaScript, the verb answers an uncaught script exception: Nonbl emits a status and then resolves the failure exactly as it resolves any other Operation’s, so an unhandled exception stops the branch under the default. The script can call pathHalt(true) to stop the branch whatever the verb says, or pathHalt(false) (or simply not call it) to leave the decision to the verb. An uncaught throw is the script’s one way to say it could not produce, and it fails the Node.
Each Input Port has its own Validation dropdown. This controls whether the Operation runs at all, based on whether the Input is ready.
Any value (default)
The Operation runs with whatever value is on the Port, including the initial empty state. Use when the Operation already handles missing or default values safely.
A value that exists
Stops the Node until an upstream Node has written to this Input at least once since the Task started. After the first write the value persists and later firings proceed normally.
A value not used yet
Admits the Input only when its source holds a value this Input has not run on yet. Works across Triggers: a report Path reading a sensor Path runs once per new reading.
A value whose last update succeeded
Stops the Node unless the Operation feeding this Input last attempted a run and succeeded — anywhere in the Task, even on a different Execution Path. See Cross-Path failure signalling below.
A value whose last update failed
The inverse: runs the Operation only when the Operation feeding this Input’s last attempt failed. This is how a branch reacts to a failure — a log entry, an alert, a fallback action. Behind Stop this branch the producing Operation’s last attempt still reads as failed, but it stops its own dependents, so a consumer on the same Path never gets its turn; give that Node a continuing verb when the reaction sits directly downstream of it. A consumer that a different Trigger reaches is not one of those dependents, and there the rule passes behind any verb.
The Operation feeding an Input has a last attempt in one of three states, not a plain succeeded-or-failed: it may never have been attempted at all. The engine keeps that one state per Node, and A value whose last update succeeded and A value whose last update failed each read it; an Operation that has never been attempted fails both readings — an Input that has never been produced is not ready under either one. A value that exists is the rule that asks that question instead.
| The Operation’s last attempt | A value whose last update succeeded | A value whose last update failed |
|---|---|---|
| Never attempted | Fails | Fails |
| Succeeded | Passes | Fails |
| Failed | Fails | Passes |
A rule that is not satisfied stops the Node: it waits, at Information level in the log, and its Outputs are untouched. The Node’s On Error verb is not consulted — a wait is not a failure.
The Validation setting is stored on the Port, not the Operation — you can configure it independently for each Input on a Node.
An Operation’s On Error verb decides two things every time it resolves a failure: does the branch continue, and does the Output’s write stamp advance. Those two questions matter separately to whoever reads that Output downstream, and Stale versus fresh lays the three verbs side by side on both.
If a connected Input’s Validation is Any value and the source Operation’s On Error verb is not Stop this branch, Nonbl’s compiler flags it at build time with an informational status: {} reads from {}, which continues after errors; it may see old or substitute data. Consider a Validation rule on this input. It shows up in the Editor’s problems list — it does not block compiling or deploying the Task, it’s a nudge. The hint fires for Continue with the last value and Continue with the fallback alike, because both let the source Operation’s branch continue past its own failure; only the downstream Input’s Validation decides whether that matters.
A second hint fires when an Input carries A value whose last update failed and the Node feeding it stops its branch on a failure: that Node never runs its own dependents, so on its own Path the rule can never pass. The hint reads the two settings rather than the Path structure, so it also names a consumer that a different Trigger reaches — where the rule does pass. Informational either way; give the producing Node a continuing verb if the reaction is meant to sit directly downstream of it.
A third finding is not a hint and blocks: a Node set to Continue with the fallback with an Output whose Fallback is unset does not check out, and the Editor refuses the Task — {} continues with the fallback after a failure, but output {} has no Fallback set. Set one on that output, or give the node a different On Error verb., an Error, one per unset Output, naming the Node and the Output. An Output the Operation declares but does not publish is passed over — the Editor draws no port for it, so there is nowhere to put a Fallback.
Trigger ─► ParseTemp ─┬─► Average ─┐ └─► Peak ────┴─► ReportParseTemp parses an incoming JSON reading. It forks into two Operations fed directly from its Output: Average (a rolling mean) and Peak (a running max). Downstream, Report joins the two branches — one Input wired to Average’s Output, the other to Peak’s Output.
Average’s On Error verb is Continue with the last value — a bad reading shouldn’t reset the running average.Peak’s On Error verb is Continue with the fallback, Fallback 0.Report’s avg Input Validation is A value not used yet. Its peak Input Validation is Any value.Trace a run where the incoming reading is malformed, so both Average and Peak hit their own runtime failure:
Average resolves the failure under Continue with the last value: nothing is written, and its Output’s stamp stays at whatever run last wrote it.Peak resolves the failure under Continue with the fallback: it writes 0 to its Output and stamps it with this run’s pathRunId.Average nor Peak stopped its branch, so Report still runs this round:
avg Input (A value not used yet) finds the same value it already ran on, and stops Report — correctly, no report is built from stale data.peak Input (Any value) checks nothing, so it silently accepts Peak’s fallback 0. Because Peak’s verb continues and this Input’s Validation is Any value, the compiler flags Report’s peak Input with the hint above — this is exactly the gap it’s designed to catch.If Average’s verb were Continue with the fallback instead, its Output would be written and stamped this run, and Report’s avg Input would pass and run on the fallback average — a different, equally deliberate choice, not a bug, but one worth making on purpose rather than by omission.
Some Operations play two roles inside the same Task. Most Operations are a single leaf: they run once, in one Execution Path, when their turn comes. A hybrid trigger-action is both a leaf in the Path that reaches it and, later, the Trigger of its own separate Execution Path. Delay is the model for this shape.
Path A: Trigger ─► ... ─► Delay (action role — Delay is a leaf here)
Delay ─► ... ─► (fire role — Delay triggers its own Path, later)Action role — Delay as a leaf in Path A. When Path A reaches Delay, it behaves like any other Operation. A Gate that is off, or an Input whose rule is not satisfied, stops it: the timer is not armed and its branch ends there. A value on an Input it cannot read fails it, and Delay’s own On Error verb answers that exactly as it answers Math’s or Compare’s. In this role Delay’s downstream Outputs aren’t meaningful yet (they belong to a firing that hasn’t happened), so the verb here only decides whether Path A continues past the failed arm attempt — it never makes the timer fire early or fire wrong.
Fire role — Delay as the trigger of its own Path. When the timer actually elapses, Delay becomes the root of a brand-new Execution Path: a fresh path run, Delay’s own downstream Nodes.
Delay, Throttle, Boost, REST API Call and Webhook are a leaf on the Path that dispatches them and the root of the Path their completion fires. Every completion fires. A completion that failed fires a Path whose first Node fails: under Stop this branch that Path stops there, with the failure in its own log; under either continue its dependents run on the last or the fallback values.
The takeaway: the Path a completion roots always runs. The Node is that Path’s root, so its branch is the whole of it, and Stop this branch on a failed completion means the Path fires and ends at the Node itself. The completion event (in Delay’s case, the timer) decides when the Path runs; the On Error verb decides what the rest of the Path sees.
A follow-on Path always runs on a later turn of the engine, never inside the Path that caused it — whatever fires it. Delay’s timer, an outbound call’s answer, an MQTT message a Path itself publishes: none of them run their triggered Path from within the Path that armed, sent or published. The dispatching Path finishes first; the follow-on Path starts afterwards, as its own, separate run. You see this in Monitoring: the dispatching Path’s status batch completes before the follow-on Path’s batch opens, never interleaved with it.
One consequence worth naming: a Node that both Paths pass through runs once for each Path, not once shared between them. There is no “already ran this round” shortcut across the boundary between a Path and the one it triggers.
A hybrid trigger-action’s own Path can, directly or through several hops, fire the very Trigger that started it. Two MQTT nodes are the common shape: an MQTT Action on one Path publishes to a topic, and an MQTT Trigger subscribed to that topic starts a Path that eventually publishes back to it, closing the cycle.
Nonbl bounds how many follow-on Paths one firing can set off before treating the sequence as a cycle rather than a working design: the engine.maxFiringDepth setting, default 16 (0 disables the cap). A trigger that legitimately fires once per external event, indefinitely, is not what this counts — the cap is on how many follow-on Paths one firing sets off, not on how often the Trigger fires over time.
When a firing crosses the cap, the Task is retired with Trigger {} exceeded the firing-depth cap of {}; the task was retired, a Warning naming the Trigger that closed the loop and the cap it crossed. Retiring the Task stops it rather than leaving it firing forever; redeploy it once you’ve broken the cycle, typically by adding a condition that stops the loop from feeding back on itself.
The cap sees a cause that closes through the engine: a firing asked for from inside a walk, or from the timer or the completion of a Node that walk ran. It cannot see a cause that crossed an external system, a real broker or a third-party service, and nothing can. Two settings stand beside engine.maxFiringDepth for those: engine.maxPostedPerRunee (default 65536) retires a Task whose Trigger has that many firings waiting, and engine.maxFiringsPerSecond (default 1000) retires a Task that asks for more firings than that in one second. Each retirement leaves a record: the rate cap names the Trigger, the firings it asked for and the cap (Trigger {} asked for {} firings in {} ms, above the cap of {}; the task was retired), and the pending cap is answered as a Task failure naming the Trigger’s own queue and the cap it rose above. Either setting at 0 turns that bound off.
An Operation whose answer arrives later — an outbound call, a delivery — has the hybrid two-role shape, and its On Error verb is the one every Node carries. There is no second knob. What differs is where the verb acts.
The follow-on Path this Operation fires always runs on a later turn of the engine, never inside the Path that dispatched the call — see When the follow-on Path runs above.
In the Path that dispatched the call the Operation is the last Node. Its Outputs belong to the answer, not to this Path, so anything that keeps the call from being issued — a closed Gate, an Input whose rule is not satisfied, an exhausted budget, an unusable destination — is reported, the call is not issued, and no completion arrives to fire the follow-on Path that round, whichever verb the Node carries. Nothing else in the dispatching Path depends on this Node, so nothing else is affected.
In the Path the answer fires the Operation is the root, and the verb decides everything, exactly as the Delay section above describes:
| On Error | The follow-on Path |
|---|---|
| Stop this branch (default) | Fires, and stops at its first Node. The Outputs keep their values, and the last attempt reads as failed. |
| Continue with the last value | Fires. The Outputs keep their values, not stamped as new; the last attempt reads as failed. |
| Continue with the fallback | Fires. Each Output carries its Fallback, stamped as this run’s value; the last attempt reads as failed. |
The failure is recorded as a status in every row, at the Task’s Status Level, so choosing a continuing verb never hides it.
Where an Operation offers a retry it is a number in its own settings, spent before the completion fires: only when the retries are exhausted does the failure meet the verb. The Operation’s own page says whether it has one and what it may be combined with.
A default Fallback makes the continuing choice one click: an Operation whose only answer to a failure is a Boolean ships that Output with a Fallback of false, so Continue with the fallback is “fire and report the failure”. Where no default ships, the Editor refuses the Task until you set one. The Operation’s page says which Outputs carry a default.
Every Operation that can fail has an error output. It carries the failure, and it feeds whatever is wired to it only in a firing where the Operation failed. The Outputs are the try; the error output is the catch, and the catch receives the exception.
The error output is a ring of solid ink at the Node’s bottom-right corner, hanging fully below the Node’s bottom edge with its top edge tangent to it — filled in every state, wired or not — and its wire is dashed. Every Node that can fail has one; a Node whose Operation cannot fail (Constant, Scheduler, MQTT Trigger) has none — exactly as it shows no On Error control: the Operation declares it, and the Editor draws neither on such a Node.
The dashed wire is tellable from a value wire at a glance. It connects to any Input that accepts a Map — that is what a failure is on the wire — and it is copied, undone and deleted like any other Connection. The Node tab lists no Fallback for the error output: it is not an Output the Operation writes, and no verb substitutes for it.
The catch runs in the firing the failure happened in, after the Node that failed, as part of the same Path run — no second firing, no later turn. A failure is one of the three the Overview lists: an unconnected mandatory Input, a value the Node cannot read, or the Operation’s own logic not finishing. Whatever verb resolves it, the failure reaches the error output first.
Three things look like a failure and are not, and none of them fires the catch:
Nor does the catch fire for a failure that happened somewhere else. It answers this Node failing in this firing. An Operation that failed three firings ago and was not reached this time fires nothing; a catch wired to an Operation that this firing’s Path does not include is skipped, because to this firing that Operation did not fail. The cross-Path signal — a consumer stopping or running because some Operation’s last attempt failed, whenever the consumer’s own Trigger fires — is Validation, not the error output.
The On Error verb governs the Node’s Outputs and their dependents; the error output is governed by nothing but the failure. The two compose: the verb is what you choose for the branch, the catch is what you draw for the failure.
| On Error | The Outputs’ dependents | The catch |
|---|---|---|
| Stop this branch | Skipped | Runs |
| Continue with the last value | Run, on the value each Output holds | Runs |
| Continue with the fallback | Run, on each Output’s Fallback | Runs |
Under Stop this branch the failure ends the branch and the catch is the only thing that runs after it — the reaction shape:
Trigger ─► Parse ─┬─► Store (skipped: Parse failed) └─ Error ─► Logger (runs, with the failure)Under Continue with the fallback the branch carries on with the substitute and the catch runs with the reason — “give me the fallback, and tell someone”:
Trigger ─► Fetch ─┬─► Publish (runs, on Fetch's Fallback) └─ Error ─► Logger (runs, with the failure)Both branches run in the same firing, once each. A Node reading the Outputs sees the substitute; the Node on the error output sees why.
The value on the error output is the failure record: a Map with six keys, the same for every failure whoever reported it.
| Key | Type | What it holds |
|---|---|---|
extension | String | The extension that published the status, by its full name — nonbl.core for a core operation, nonbl.tasks for the engine’s own path failures |
code | Integer | The status code, as that extension’s documentation lists it |
severity | Integer | The severity the extension declared for it: 10 Critical, 20 Error, 30 Warning, 40 Information, 50 Debug, 60 Trace — smaller is more severe |
node | Integer | The id of the Node the failure is about — the number in node 3 "Thermostat" |
message | String | The Operation’s own sentence, rendered from its published format: Convert: cannot convert input to requested format |
details | Array | The arguments the status carried, for a reader that wants the pieces |
A catch reads it with a JSON Parser: give an Output /message for the sentence, /code or /severity for a Condition to branch on, /node to know where. A Logger shows the whole record; an MQTT Action or a Webhook takes it straight on its Payload and sends it as JSON — nothing needs to make text of it first. To keep the JSON text in the graph, Convert’s JSON text target makes a String of any Map.
A missing key is an answer: a failure the engine could not resolve against a published definition carries no severity and no message, and a failure addressed to something other than a Node carries no node. A JSON Parser pointer at a missing key writes an empty (Nil) value on that Output for this firing, which a Condition reads as “no”.
The catch is a branch like any other. Whatever you would put after an Output you can put after the error output: a Counter to count failures, a Compare and a Gate to escalate after so many of them, an MQTT Action to raise an alert, a second attempt at the same work. Everything after it is subject to what everything else is subject to — Gates, Validation rules, its own verb.
A catch can itself fail, and it has its own error output and its own catch. There is no limit on how deep that goes, and no special machinery either: a Path is one ordered pass through its Nodes, each run at most once, and a catch is only more Nodes in it. A catch that fails and has no catch of its own is resolved by its verb, like every Node.
An Operation waits for all of its Inputs, and the two branches never both arrive.
That is the trap, in one sentence. A Node fed, directly or through others, by an Output and by the error output of one Node needs that Node to have both delivered and failed in the same firing — and under Stop this branch a failure means nothing was delivered, so the Node can never run. The Editor says so as a finding (below). Under either continuing verb the same shape is live — it runs on a failure only, with the substitute and the reason — which is the case above, not a join.
To run one step after either outcome, route the two branches through a Funnel. Its Inputs do not wait for their producers — they are loose — and each firing it passes to its Output the one Input that arrived, stopping when none did. Put the step after the Funnel’s Output and it runs once, whichever branch ran:
Trigger ─► Send ─┬─► (ok) ─────────────┬─► Funnel ─► Acknowledge └─ Error ─► Logger ───┘Acknowledge is drawn once, with one set of settings and one state, instead of once per branch. A step with state — a Counter, a Throttle — is exactly the step you do not want duplicated to drift.
A Funnel is for joining branches that produce a value. It is not for carrying on past a failure with a stand-in value: that is Continue with the fallback, and the verb does it without a Funnel.
The same Funnel joins a second source to a first: a Primary on the Trigger, a Secondary on the Primary’s error output, both value Outputs into the Funnel, and the processing after it. The processing receives the answer from whichever source produced one — the Primary’s when it succeeded, the Secondary’s when it did not — and it is drawn once. Without the Funnel the two sources would need two copies of everything that follows.
Trigger ─► Primary ─┬─► Out ──────────────────┬─► Funnel ─► Process └─ Error ─► Secondary ────┘When several Inputs of a Funnel arrive in one firing the lowest-numbered wins, which makes port order a priority; the Funnel’s page has the rest. Everything after a Funnel must accept everything that can reach it: its Output is Any, and a downstream Node sees, over time, every type the Funnel’s Inputs can carry.
The compiler knows the shapes above, and it tells you in the problems list while you draw:
| What you drew | Finding | What it says |
|---|---|---|
| A Node fed by an Output and by the error output of one Node that stops its branch | An Error on the Node — the Task will not deploy while it stands | {} can never run: it needs {} to have both failed and delivered in one firing, and that node stops its branch on a failure. Feed it from one branch only, join the two branches in an operation that takes a value and an error input, or change the On Error verb on that node. |
| A Node fed by a Node that can never run | A note | {} can never run: it is fed by {}, which can never run — the remedy is on the producer’s own finding |
| An error output on a Node that cannot fail | An Error on the Input | {} is fed by the error output of {}, which cannot fail. Remove that edge: a node that cannot fail has no failure to pass on. |
| A value whose last update succeeded on an Input fed by an error output | A Warning on the Input | {} admits its input only after a success, but it is fed by an error output, whose producer has just failed; the rule can never pass |
| Two error outputs into one Node | A note | {} runs only when {} and {} both fail in the same firing — a Funnel is “either” |
| A Validation rule on a loose Input, such as a Funnel’s | A Warning on the Input | {} does not wait for its producer, so a Validation rule on it would refuse a value the node was never going to read; the rule is ignored |
The two consequence findings above still ask you to change nothing — a Node fed by a Node that can never run, and two error outputs into one Node, are both notes, and each disappears once the shape upstream is resolved. A Node that can never run is not one of them: it is an Error, and the Task will not deploy while it stands, until you feed it from one branch only, join the two branches in a Funnel, or change the producer’s On Error verb. The Editor withholds the Validation dropdown on a loose Input for the reason the last row gives — the Operation reads only what arrived this firing, so a rule there has nothing to check.
A value whose last update failed on an Input fed by an error output is true by construction, and is neither flagged nor needed.
A hybrid trigger-action — a Delay, an outbound call — can fail in either of its roles: the Path that reaches it can fail to arm or dispatch it, and the completion that fires its own Path can arrive failed. One error output answers both. A catch on a hybrid runs in whichever firing the failure happened in — the one that tried to start it, or the one its completion produced; it never waits for a path that may not come. A Logger on an outbound call’s error output therefore logs a call that could not be issued and a call that was answered with a failure alike, each in the firing it belonged to.
The verb still governs only the value side: under Stop this branch a failed completion fires a Path that ends at the Node itself and its catch, exactly as Operations that answer later describes for the Outputs.
A Delay, a Throttle or an outbound call wired to an error output is armed or dispatched by the failure, in the failing firing, and what follows is subject to what a Delay is subject to: its own Path runs on a later turn of the engine, never inside the firing that failed. A retry after a pause is that shape — error output into a Delay, Delay into the work again — and the firing-depth cap counts each round of it, so a retry that always fails is a loop the engine retires rather than runs forever. Put a Counter and a Gate ahead of the Delay to bound it yourself.
details carry the specifics of one occurrence — the value refused, the count exceeded — so an OnChange on an error output sees a change nearly every time. To alert once per kind of failure, take the failure through a JSON Parser and put the OnChange on its /code Output; the codes compare equal where the details do not.Shape. Device state arrives on MQTT; a Scheduler republishes the most recent payload every minute so downstream consumers always see the current state even if the device is quiet.
Without validation. When the Task starts, the Scheduler fires before any MQTT message has arrived. The MQTT Action publishes the empty default Payload — a blank message.
With validation. Set the Payload Input on MQTT Action to A value that exists. The first few Scheduler fires after startup stop at MQTT Action because Payload has not been written yet. Once the MQTT Trigger’s path runs and populates Payload, subsequent Scheduler fires proceed and republish the latest value.
Shape. Two numeric sensor values feed a Math Division; the result flows into downstream analytics.
With Stop this branch. If the divisor is ever zero, this Operation and everything reading from it stop. Analytics don’t update until the next valid reading.
With Continue with the fallback, Fallback NaN. The Path keeps flowing. Downstream consumers see NaN and can branch on it — for example, a Compare checking for NaN routes to a Logger. Other Paths in the Task are undisturbed.
With Continue with the last value. Downstream sees the last good division result. Useful when the sensor produces brief glitches that shouldn’t distort a running display.
Shape. Path A computes a current setpoint; Path B periodically publishes that setpoint to a remote service.
Without cross-path signalling. If Path A errors out, Path B still publishes whatever stale setpoint remains. The remote service sees no update, but also no error.
With cross-path signalling. Set Path B’s Input Validation to A value whose last update succeeded. The moment Path A’s computing Operation errors out, the next Path B fire stops at that Input — the stale value never reaches the remote service. The Task automatically resumes as soon as Path A’s Operation runs again successfully.
Paths in a Task run independently — different Triggers, different timings. Sometimes you want a failure in one Path to block a consumer in another. Use the A value whose last update succeeded Validation on the consumer’s Input.
How it works from the user’s perspective:
Two important properties:
Reacting rather than blocking. Put A value whose last update failed on an Input instead, and the consumer runs because the upstream failed, whenever the consumer’s own Trigger fires. On the failing Node’s own Path the reaction is its error output: whatever is wired to it runs in the firing the Node failed in, with the failure, whatever the verb.
Gate closure never makes a last attempt read as failed — a closed Gate is not a failure.
The Operation does not run (or is the one that stopped the Path), and downstream Nodes that depend on it are skipped for that path run. Other Execution Paths in the same Task are unaffected.
A stopped path is silent to the outside world by design: nothing is published, logged, or reported unless you wire a Node that does so, or you check the Task’s recorded statuses (below). The next time a Trigger fires, the same checks run again, and the path proceeds once the underlying condition is satisfied.
Nonbl records what happened during a path run as a stream of status entries, each with a severity and a plain-language message. The statuses that report a Path stop:
| What happened | Severity | Message template |
|---|---|---|
| Stopped on purpose | Information | Each Operation says so in its own words — OnChange: input unchanged, Hysteresis: input {value} within band [{low}, {high}]; JavaScript’s pathHalt(true) answers Stop. |
| Operation failed | Error | Also the Operation’s own message — Math: input is not numeric, JsonParser: input is not JSON text, a Map or an Array. |
| Gate is off | Information | Gate is off: the path stopped here — a connected Gate Input was false or Nil. |
| A rule is not satisfied | Information | Task {}: the path from {} stopped at {}: {} is not satisfied — a Per-Port Validation rule was not satisfied, so the Node waited. |
| Wrong value type | Error | Task {}: the path from {} failed at {}: wrong value type (expected {}, got {}) — the value on an Input is a type the Node cannot read. |
| On Error resolved a failure | Debug | On Error: continued with the last value / On Error: continued with the fallback (output {} = {}) — a continuing verb resolved the Operation’s failure. |
Both of those are recorded whatever the verb: a rule not satisfied at Information, a type mismatch at Error.
The Severity column is what separates a deliberate stop from a failure. The first two rows both carry the Operation’s own words — OnChange: input unchanged reads much like Math: input is not numeric, and nothing in the message says which kind of event it was. The severity does: Information means the Node stopped and the Task is working as built; Error means it could not finish. Read the severity first and the message second.
Where one of these messages names a place — a Trigger, a port — it carries an address rather than a number, and whoever shows you the message resolves it. The Editor shows the Node itself and takes you there; anywhere plainer, you read node index 2 or node index 2 input 1.
An index is the Node’s place in the Task, counted from 0, and it names the same Node whichever Execution Path it is running in. Two other numbers belong to a Node and neither of them is this one. Its id is the identity it keeps as you edit the Task, which is what the Editor labels it with — inserting or deleting a Node ahead of it moves its index and leaves its id alone. Its step number is what the Editor prints on a Node once you pick a Trigger, counting steps along the chosen Path, so it differs from the index as soon as a Path forks.
These aren’t a separate telemetry channel — every entry above shows up the same way any other server message does: the status bar and Last Statuses window described in Monitoring.
Every Task has a status verbosity setting; new Tasks default to None — a task says nothing unless asked. Raise it to Warning to record Operation failed and Wrong value type above. Stopped on purpose, a rule not satisfied, Gate is off, and a verb that resolved a failure are lower severity still (Information / Debug) and need the verbosity raised further to show. There’s no per-Operation override — verbosity is a single Task-level knob.
Every Port in Nonbl declares which MessagePack types it accepts (Inputs) or produces (Outputs). The Editor shows the resolved types as a small chip below each Port tag, and the compiler uses them to reject incompatible Connections as you wire them.
Each Port falls into one of four categories. The per-Operation pages tell you which flavour each Port uses; this section explains what that means.
Fixed
The type is decided by the Operation and cannot be changed. Examples: Counter.Count is always an Integer, Hysteresis.Out is always a Boolean, MQTT Trigger.Payload is always Binary.
Determined by a Property
A picker on the Node decides the type. Examples: the Output Format picker on Math and Compare, the Const Type picker on Constant, the OnOff Type picker on OnOff, the Mode picker on Random. You pick the semantic option in the Node’s Properties; the Port’s type follows.
User-configurable
You pick the accepted types yourself, in the Node tab’s Inputs and Outputs sections. Used for Operations whose types genuinely can’t be known ahead of time — JavaScript, SQLite Storage, JSON Parser, Report.Data, Condition.True/False/Out, AI.Structured, and AI.Context inputs. See Setting Port Types below.
Any
No narrowing; the Port accepts any MessagePack type. Used for Gate inputs (>), Logger.Data, and Compare.Data1 / Data2.
When you connect two Ports, the compiler intersects the producer’s Output types with the consumer’s Input types. If the two Ports have no type in common, compiling the Task produces a Connection Type Incompatible error — the offending Connection turns red in the Editor and the Task won’t deploy. You also get a red reject indicator when dragging a new Connection over an incompatible target.
Examples:
Math.Data1) fails at compile time.Constant whose Const Type picker is set to String into Hysteresis.In (numeric-only) fails.Compare Output into Gate.In succeeds — Gate.In is in the Any flavour and accepts everything.Flavours 2 (Property-driven) and 3 (User-configurable) resolve to concrete types as soon as you change the relevant picker or checkbox grid, and the compiler re-checks Connections immediately.
At runtime, when an Operation would receive an Input, the engine re-checks that the value’s actual type matches the declared mask. A rule that is not satisfied stops the Operation; a type it cannot read fails it, and the verb resolves the failure. Under Stop this branch the branch stops at the consuming Node; under either continue its Outputs resolve per the verb and the Path carries on. The “Wrong value type” Error above is recorded either way.
For User-configurable Ports (flavour 3), you pick the accepted MessagePack types yourself:
Str / Int).Nil, Boolean, Integer, Float, String, Binary, Array, Map.The chip under the Port tag on the canvas updates as soon as you apply the change, and downstream Connections re-check immediately.