Lesson 4. Logic without a model

The three previous lessons revolved around a model. This one does not: there will be no LLM node in the graph at all. A message arrives at the entry, a node checks a condition, two branches build different answers, and both converge into a single exit.

The main argument of this lesson is cost. Starting a workflow costs one credit, and everything else on the bill is model calls and paid external operations. There are none here, so every run costs exactly one credit out of the hundred monthly ones, no matter how many times you repeat it while debugging the condition. For comparison: one bot answer from the first lesson costs about two credits. The formula is broken down in “Credits”.

Along the way these are two skills people stumble over most often: branching and templates.

Entry
If / Else
Urgent
Regular
Exit
  • Execute + Data
The condition picks one leg. Both branches lead into a single exit — that is allowed because they exclude each other.

What the graph will do

The scenario is mundane: a visitor writes into the chat, and we want to answer urgent requests differently from ordinary questions — without a model, by a word in the text. The logic is deliberately simple: the point of the lesson is the mechanics, not the bot’s cleverness.

Building it

  1. In the Workflows section press New Workflow and give it a name. On the empty canvas place Entry and Exit — they are in the Input / Output palette section, Boundaries group.

  2. Drag in the If / Else node — the Logic & Flow section, Branching group. Connect the output of Entry to its input.

  3. Open the node’s settings (select it and press the pencil) and put a single expression into the Condition field:

    {{ 'urgent' in inputs.input | lower }}

    inputs.input is what arrived at its input, i.e. the message text. The | lower filter lowercases it so that “Urgent” and “URGENT” count too.

  4. Place two Template nodes — the Data section, Values & State group. Connect one to the True output of If / Else, the other to the False output.

  5. In the first one write the answer for an urgent request, in the second one for a regular one. You can insert the message itself:

    Logged as urgent: {{ inputs.input }}. We will reply within an hour.
  6. Connect the outputs of both Template nodes to the input of Exit.

  7. Press Validate, then Save.

The condition: write one expression

The Condition field is templated, and the result depends on how you write it.

app.iterna.ai

Node configuration

If / Else

Parameters

Condition*
{{ 'urgent' in inputs.input | lower }}

The whole string is one expression: the result arrives as a boolean value.

The whole condition fits into a single expression — that is how it returns a real yes/no.

There is one rule and it is the same for all templated fields: if the entire string consists of a single {{ ... }}, a real value comes out — a number, a list, a yes/no. If the expression is mixed with text, the result is always a string. Details are in “Templates”.

For a condition this matters for the following reason. The platform also understands a mixed form such as {{ inputs.input }} > 100: it substitutes the value and then tries to read the result as a comparison of two numbers. As long as both sides turn out to be numbers, everything is honest. But if the left side turns out to be text — say, “Hello” arrived — there is nothing to compare, and the whole string counts as non-empty, that is, true. The True branch will fire every time, and it will look as though “the condition is broken”.

Useful forms for this node: {{ 'word' in inputs.input | lower }} — substring containment; {{ inputs.input | length > 200 }} — length; {{ variables.plan == 'paid' }} — a comparison against a variable.

Two ways to reference a value

There are two different references in templates, and confusing them is the second most common reason for empty fields.

Notation What it reads
{{ inputs.input }} The value delivered to this port by the immediate predecessor. The name is relative to each node: for the second Template node it is what came from If / Else, not from Entry.
{{ nodes.if_1.output }} The value of any node in the graph by its identifier. if_1 is the very id shown in its settings next to the name.

Next to output sit the other fields of the result. For If / Else that is {{ nodes.if_1.branch }} — the string true or false: handy when you want the answer to state which branch built it.

Speaking of values: If / Else changes nothing and passes its input through as is. So {{ inputs.input }} in both Template nodes is the user’s original message.

A reference in a template does not run a node

This is the key idea, and it explains half of all empty results. {{ nodes.X.output }} is a read from the run’s memory: it holds the outputs of nodes that have already finished. A reference wakes nobody up and does not change the execution order — a node is started only by an incoming edge (see “How a graph executes”).

So if you reference a node from a branch that was not taken, there will be nothing to read. There will be no error either: an unknown name renders as an empty string, and the node calmly finishes with a hole in the text. The hole can be found: the platform records every path that failed to resolve into the node’s debug data under the key __undefined_refs__ — that is the only trace of a typo in an id. Where to look at it is described in “Templates”.

The branches exclude each other: if picks exactly one leg, and the second edge becomes dead. The scheduler understands this and starts Exit as soon as at least one input has fired and all the remaining ones are dead. Exit runs once and receives the value of the branch that fired.

The wire.andjoin_partial_payload warning you saw during validation says exactly that: the value of the second branch will not reach the exit. That is usually what you want, hence a hint rather than an error.

The difference is exactly the question “can these two branches fire in the same run”. Mutually exclusive — a warning; simultaneous — an error.

Checking it

Open the Test tab and send two messages: one with the word “urgent”, one without. The answers should differ.

On the tab’s canvas you can see which node ran and which stayed grey. Click the skipped Template node — its card will show the reason for the skip: the condition above chose a different branch. This is the graph working normally, not a failure: the run counts as completed, not partial. The full list of reasons is in “Statuses”.

Each such run costs one credit and zero tokens — you can calmly try out different wordings of the condition.

If it did not work

What you seeWhyWhat to do
The True branch always firesThe condition is written in the mixed form and after substitution became non-empty text rather than a comparisonPut the whole comparison inside one pair of braces: {{ inputs.input | length > 100 }}
Both branches firedThe branches do not come from one conditional node but run in parallel — then they do not exclude each otherCheck that both edges leave the same If / Else node, from the True and False ports
The validator demands a barrier (wire.fanin_needs_barrier)The branches can fire in one run — a race for the input portJoin them with a wait_all node before the shared input
A node is grey, marked as skippedThe condition chose another branch, or the path was cut off higher up the graphOpen the node's card in the run and read the skip reason
There is a hole in the answer: part of the text is emptyA name in the template did not resolve — a typo in an id or a reference to a node from a branch that was not takenLook at __undefined_refs__ in the node's debug data; compare the id with the one in the settings
Curly braces show up in the answer verbatimThe field is not templated — substitution happens only in fields marked as templatedMove the expression into the Template field of a Template node
The andjoin_partial_payload warningExpected for this graph: mutually exclusive branches converge into one inputDo nothing — it blocks neither saving nor publishing

What next