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.
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{"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 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
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.