Routing and validation
Three fences decide what happens next: route jumps to another step, condition drops the
message, validate rejects the request. They look similar and behave very differently — picking
the wrong one is the most common mistake in a first flow.
route — branch to a step
Each arm is a when line followed by a call line naming a step in the same flow. An
otherwise arm catches the rest.
## Step: classify```routewhen $.total > 10000: call review-manuallywhen $.currency != "EUR": call convert-currencyotherwise: call forward```
## Step: review-manually...
## Step: convert-currency...
## Step: forwardeffects: [http_egress]endpoint: https://orders.example.com/ingestArms are tested top to bottom and the first match wins. Execution jumps to the named step and continues from there in file order — it does not come back. Order your steps so the branch targets sit where you want execution to carry on.
A when expression that evaluates to something other than a boolean is a step error, not a
falsy result.
If no arm matches, the step fails. There is no implicit fall-through. When every input should
be handled, end with otherwise; when an unmatched input is genuinely an error, leave
otherwise out and let the failure surface.
A call naming a step that does not exist is caught at compile time.
condition — drop the message
One expression. True continues, false stops the flow and discards the message.
## Step: only-final-invoices```condition$.status == "FINAL"```Nothing else happens. No error, no fault handler, no response body. On a synchronous request the caller gets a success with nothing useful in it — which is why this is the wrong tool for rejecting a request.
It is the right tool for a queue or a connector feeding a flow with more messages than you care about: the drop is recorded as an intentional filter rather than a failure, so it does not consume retries and does not reach the dead-letter queue.
A non-boolean result is a step error.
validate — reject with a reason
One rule per line:
<expression> | <message> [| syntactic|semantic]## Step: check-order```validate$.orderId != null | "orderId is required" | syntactic$.total > 0 | "total must be positive"length($.items) > 0 | "at least one item is required"$.currency == "EUR" | "unsupported currency: {{ $.currency }}"```The first rule that evaluates to false rejects the request. Remaining rules are not evaluated,
so order them from cheapest and most fundamental to most specific — a rule that reads
$.items[0] must come after the rule that proves items is not empty.
There is no schema validation: no XSD or JSON Schema is loaded, and an arriving message is not checked against one. The rules are the expressions you write in the block, over the message as it arrived.
kind decides the HTTP status:
| Kind | Status | Use for |
|---|---|---|
syntactic |
400 Bad Request | The request is malformed — a required field is missing, a type is wrong |
semantic (default) |
422 Unprocessable Content | The request is well-formed but the values are unacceptable |
Without a ## Fault: section the caller gets the message directly:
$ curl -i -X POST http://localhost:9090/flows/acme/orders/run \ -H "Authorization: Bearer $NEXUS_KEY" \ -H 'Content-Type: application/json' -d '{"total":0}'
HTTP/1.1 400 Bad Request{"ok":false,"error":"orderId is required","step":"check-order"}Messages support interpolation, so you can name the offending value:
$.currency == "EUR" | "unsupported currency: {{ $.currency }}"Be deliberate about what you interpolate — the message reaches the caller. Do not put a secret,
a token or an internal identifier in one. A message that reads a name a secret_read step writes,
ctx.oauth2_token or ctx.headers.authorization is refused when the flow is published, and the
server log and L1 carry each {{ }} part of a message as its type, not its value; see
Faults.
A validation failure triggers the ## Fault: section if the flow has one, which is
how you return a problem-detail document instead of the default shape.
Choosing between them
| Aspect | condition |
validate |
|---|---|---|
| On false | Message dropped, flow stops | Request rejected with an error |
| Caller sees | Success, empty result | 400 or 422 with your message |
| Fault handler runs | No | Yes |
| Counts as a failure | No — an intentional filter | Yes |
| Retries consumed | No | Depends on the flow’s retry configuration |
| Number of tests | One expression per fence | Many rules per fence |
| Right for | Queued and connector input you want to ignore | Anything a caller is waiting on |
The rule of thumb: if somebody is waiting for an answer, use validate. A silently dropped
synchronous request is the hardest kind of bug to diagnose, because both ends report success.