Blocks

If

The two-way branch: an expression over the upstream payload routes down the true or false port, payload unchanged. Expression syntax and wiring both ports.

The two-way branch: a condition over the upstream payload routes execution down the true or false port. The other branch's steps are skipped.

What it does

If evaluates one expression against the immediate upstream step's output and picks a port. It is a transparent router: it never changes the payload, it only decides where the payload goes. Truthiness follows the usual rule that anything other than nil and false counts as true, so payload["tags"] on its own fires the true port whenever the list exists, and you don't have to coerce the result to a literal boolean.

The steps on the branch that didn't fire are skipped, and the skip propagates: a merge further down never waits on a branch that didn't run.

The expression is checked when the step is saved, not only when a run reaches it. A parse error or a call outside the sandbox shows up on the step in the Inspector and in the activation guard, so a typo can't ship.

Config

The if step in the Inspector One expression; truthy fires true, falsy fires false.

  • Condition: the expression, with payload bound to the immediate upstream step's output. Required. Default payload["amount"] > 1000.
payload["amount"] > 1000
payload["status"] == "active" and payload["score"] >= 50
String.contains?(payload["email"] || "", "@example.com")

Bracket access (payload["field"]), comparison, boolean and arithmetic operators, in, string concatenation with <>, the pipe, and a small allowlist of helpers (String.*, Map.*, Enum.* without function arguments, guards like is_nil) are available. It's a small expression sandbox, not a general programming language: no anonymous functions, no assignment, no case, and no module outside the allowlist.

Two equality operators: == is loose across the JSON boundary, so a real boolean true from an AI step matches "true" and the number 5 matches "5". === is strict.

Output

The upstream payload passes through unchanged on whichever port fired. If doesn't transform data, it only routes it. Given

{ "amount": 2500, "customer": { "email": "a@example.com" } }

and the condition payload["amount"] > 1000, the step on the true port receives exactly that map and can keep reading {{ payload.customer.email }}; whatever is wired to the false port sees nothing at all.

Patterns

  • Threshold gate: big orders to an approval, small ones straight through.
  • Found check: after a get from store, branch on payload["found"].
  • Error lane: a step with on-failure continue emits { "ok": false, "error": … } out its error port; an if on that lane can separate retryable failures from the rest before you notify anyone. See error handling.
  • Skip quiet periods: after a schedule, branch on a count from the previous step so the rest of the run only happens when there's something to do.

Gotchas

  • Wire both ports. An unwired false branch silently drops every non-matching payload. The run just ends there.
  • The condition reads the immediate upstream output only. Pull earlier values forward with set fields first if the decision needs them.
  • A field that's missing reads as nil, and nil > 1000 raises, which fails the step. Guard with (payload["amount"] || 0) when the key is optional.
  • Per-item filtering of a list is reshape's filter task. If is a single yes/no on the whole payload.

Related