Conditions and branching

A condition is a fork on the canvas: the flow checks something and takes the “yes” path or the “no” path. It is what turns a set of steps into logic.

What it looks like

The “Condition” node is added via “Add node” → “Condition”. It has two outgoing points: the green “yes” and the red “no”. Each can get its own chain of steps.

A “Request” flow with a branch

What you can check

In the panel, a condition is one or more rows of the form “field — operator — value”. The rows are combined by a switch: “and” (all must match, the default) or “or” (at least one).

The operators, in plain language:

Operator The check
equals / not equals An exact match of text or a number
greater / greater or equal / less / less or equal Numbers and dates
contains A substring occurrence: form.email contains @
matches pattern A regular expression, for strict format checks
password matches Comparing the entered password with a hash from the CMS (for member sign-in)
token is valid Checking a one-time link from an email (password reset, confirmation)

Three subtleties:

  • In the values for “password matches” and “token is valid” you can substitute data ({{ results.… }}); the other operators compare the value literally.
  • Visitor cookies are available to checks too — cookies.name. The “Members only” mode is built on this check (it is enabled on the “Start” node and needs no manual conditions).
  • If a check breaks (a typo in a field name), the condition counts as not matched — the flow takes the “no” path. A safe fallback.

An example from the “Request” flow (see the shot above): one row, form.email — contains — @. The visitor entered an email — we go write the letter and show “Thank you”; they left junk — we politely return them to the form.

A few ready recipes. Each is one or two rows in the condition panel; the expression is written whole: field, operator, value.

The email looks like an email — the letter goes to you, otherwise back to the form:

form.email  contains  @

Consent to processing — accept the request, otherwise show a page asking to confirm consent:

form.agree  equals  yes

A promo code from a list — a discount in the letter, otherwise the regular price. Several allowed values — several rows combined with the “or” switch:

form.promo  equals  BLACKFRIDAY
form.promo  equals  SALE30

Adults only — the regular path, otherwise a refusal:

form.age  greater or equal  18

Corporate email — the request goes to sales, otherwise the general pool. A regular expression cuts off the popular free domains:

form.email  matches pattern  .*@(?!gmail|yandex|mail)\..+

Regular expressions are a precise but fragile tool: get one character wrong and the condition starts silently dropping valid requests. So always test a complex regex on the preview with several different values, and where a simple “equals” with a list of values joined by “or” will do — use that instead.

To try it live: primer.dynapi.ru/contacts — submit the form with an empty email field and the condition sends you back; with it filled in, you get a confirmation letter and the “Thank you” page.

An expression instead of “field — operator — value” rows

The condition has a second mode: the “Expression (expr-lang)” switch — and a single boolean expression chooses the branch (the node config field is predicate_expr). When both the rows and the expression are filled, the expression wins — the rows stay in the node as the editor’s fallback copy.

An example — “the member is signed in and found in the CMS as active”:

member.id != nil && member.id != ""
  && len(results?.login?.members ?? []) > 0
  && results.login.members[0].status == "active"

This shows the main list-reading rule: ?. protects key access but not the [0] index — reading the first element is covered by the length check, and the && short-circuit keeps the index from running on an empty list.

What matters:

  • The result must be true or false. A compile error or a read through a nil chain stops the flow with a visible error in the run journal — neither “yes” nor “no” is picked silently. A missing field is nil; compare against it explicitly — != "" alone is not enough (for a missing field nil != "" is true).

  • The expression carries function equivalents of the row operators: bcrypt_matches(candidate, hash) — comparing a password with a hash from the CMS (same as “password matches”), token_valid(token, purpose, hash) — checking a one-time email link (same as “token is valid”) — plus the digests for webhook signatures: md5_hex/sha1_hex/sha256_hex/ sha512_hex and the HMAC variants like hmac_sha256_hex(secret, input). A signature check as one expression:

    hmac_sha256_hex(secret("whsec"), raw_body) == headers["X-Signature"]
    
  • Project secrets are read via secret("name"); an unknown name denies the check (the safe fallback).

  • Saving checks compilation: a non-compiling expression is rejected.

The “field — operator — value” rows are the LEGACY mode: they work and stay supported, but new logic should prefer the expression. One exception — composite signatures with key rotation (Stripe t=…,v1=…): they are still verified only by the row mode — the “Webhook signature matches (digest/HMAC)” operator.

The “no” branch is optional

If the condition only has the “yes” path connected, a failed condition simply ends the flow (the visitor stays on the same page). Nothing needs to be drawn for symmetry’s sake.

A common question: what if a branch has several steps?

Chain the steps inside the branch — “yes” can lead to a letter, then to the “Thank you” page, while “no” goes straight to a redirect. Branch length is unlimited; inside a branch you can also put another condition and get a third level of forking.

Making a flow loop (a link back to an earlier node) is something the platform won’t allow — saving is rejected with an error.


Next article: Routes: how a visitor reaches a flow →