Branching — if, else, and type tests

Branching — if, else, and type tests #

Act only under a condition with a plain if over an ordinary boolean expression — a field value, x != null, a combination with &&/||.

conditions #

function `Inbound Intake`(msg: <inbox-[:Email]->>) {
  if (Text.includes(msg.Subject, "order")) {
    write crm-[:Companies]-> { … }
  }
}

No else means the other cases simply do nothing. A condition may read a value that might not be there — if (o.viable) { … } on an extracted <boolean>, or o.stage == "Seed" — and a missing one counts as false, in &&, ||, ! and ? : alike. To choose between two values inside an expression, write cond ? a : b.

narrowing #

Narrow a source that may be several kinds with is, before you traverse it:

function `Route Change`(ev: <crm-[:`Webhook Event`]->>) {
  if (ev is <crm-[e:`Webhook Event` WHERE e.action == "record.created"]->>) {
    ev-[rec:Companies]-> {
      write rec { Description: "logged on creation" }
    }
  }
}

A change event stands for creates, updates, and deletes, and what you can reach differs by case — a deleted record isn't there to traverse to. Inside the arm ev is a created event, so the edges only a live record has are reachable; outside it they are not, and traversing without narrowing is refused while you author, naming the cases the edge belongs to. is is valid anywhere a boolean is; inside a walk's WHERE it is IS. If a record arrives without a kind on it — an event the trigger never discriminated, or one delivered without the field a test names — the run stops with that reason rather than picking a branch, so no arm ever runs against a record it can't identify.

A hop narrows from inside its WHERE the same way: testing a member's label (x.listName == "Deal Pipeline") picks that member for the whole hop — see blocks in the traversal chapter.

An else narrows too: reaching it proves the record isn't what the arms above tested, so the else sees the kinds that are left, and an else if chain keeps eliminating down to one. Handle every kind and the final else has nothing left to reach — reading a field there is refused, which is how you find a branch that can't run.

A test can also name a declared structure instead of a kind: it holds when the record carries every field that structure declares, with a compatible type. A nested node never gates the test — a record with none of that relationship carries an empty set of them, and fits. Carrying more than the declaration is fine. It narrows exactly like the tests above: the arm keeps the kinds that fit, the else keeps the rest.

node Contact {
  Name:  <text>
  Email: <text>
}

  if (rec is <Contact>) {
    write chat-[:Messages]-> { Message: "${rec.Name} <${rec.Email}>" }
  }

Presence narrows the same way: == null / != null settles whether a value that might be missing is there — see values that may not be there in the expressions chapter, and the guard below.

guard-with-error #

Stop a run with a reason instead of writing bad data — and settle what follows in the same line:

function `Notify Company Change`(evt: <crm-[:`Webhook Event`]->>) {
  channel = only(chat-[ch:Channels WHERE ch.Name == "automation-alerts"]->)
  if (channel == null) { fail("channel not found") }

  # `channel` is a present record from here on — no further guard needed
  write channel-[:Messages]-> { Message: "…" }
}

fail inside a traversal block ends the whole run, not that iteration — and writes already made stay made, in this run and earlier iterations. Guard before you write, not after.

Pitfalls #