Skip to content

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
```route
when $.total > 10000:
call review-manually
when $.currency != "EUR":
call convert-currency
otherwise:
call forward
```
## Step: review-manually
...
## Step: convert-currency
...
## Step: forward
effects: [http_egress]
endpoint: https://orders.example.com/ingest

Arms 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:

Terminal window
$ 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.