Expressions in Nodlin

What are expressions?
Expression nodes compute a single value from connected graph data โ similar to a formula cell in a spreadsheet.
They are not full Nodlin scripts. Scripts (see Python scripting) define custom types, forms, images, and multi-step logic. Expressions are a separate, lighter interpreter for one-line Starlark formulas.
| Expression node | Script node | |
|---|---|---|
| Purpose | Calculate a result | Custom behaviour, UI, side effects |
| Length | Single formula line | Full script |
| Access | Aliases, related(), col(), aggregates | node, context, factory, forms, links |
| Result | Number, condition, list, or map | Full node payload + form + image |
When to use expressions
Use expressions when you need:
- Totals, averages, maxima over linked values
- Simple conditions (
revenue > cost) - A column from a table on a spreadsheet or script payload
- A named intermediate value other nodes can reference
Use scripts when you need forms, custom visuals, creating nodes/links, or multi-step workflows.
Related docs
- Python scripting โ full Starlark scripts
- Forms โ including the expression editor control
- Starlark language โ language reference
Mental model (Excel โ Nodlin)
| Excel | Nodlin expression |
|---|---|
=A1+B1 | price + quantity (node aliases) |
=SUM(A1:A10) | sum(related()) or sum(col(table, 1)) |
=AVERAGE(...) | avg(...) |
=MAX(...) / =MIN(...) | max(...) / min(...) |
=IF(A1>10,"high","low") | "high" if a > 10 else "low" |
Key difference: instead of cell addresses, you use aliases on connected nodes and/or the related() function over relationships.
Quick start
1. Alias connected inputs
Give related nodes short aliases (e.g. price, qty, trial). In expressions those names become variables.
(price + tax) * qty
2. Sum everything on a link
sum(related("value", "hasCost"))
3. Max age from a table on a spreadsheet
If a spreadsheet node is aliased trial and its value contains people as rows [["fred", 23], ...]:
max(col(trial["people"], 1))
4. Condition
revenue > cost
Result displays as TRUE / FALSE on the node.
Aliases
Each connected inbound node with an alias exposes its default binding as a variable:
- Prefer the
valueproperty - Else
condition - Else the whole payload (dict)
# Scalar values
price * qty
# Dict binding (e.g. spreadsheet value map)
trial["cost"]
trial["people"]
Tips
- Aliases must be unique among inbound related nodes used by the expression.
- Prefer aliases over
related()[0]when there is a single clear source node. - Nested paths use Starlark subscripts:
trial["a"]["b"],rows[0].
The related() function
related() reads properties from inbound connected nodes and always returns a list.
Syntax
related([prop,] [relation,] [type])
| Argument | Meaning | Example |
|---|---|---|
prop | Property to extract | "value", "condition", "cost" |
relation | Relationship filter | "hasCost", "input" |
type | Node type filter (requires relation) | "Product", "spreadsheetFile" |
Names can be short FQN references in lowerCamel (e.g. hasCost, spreadsheetFile) or matching user labels.
Examples
related()
# โ default value/condition from all inbound related nodes
related("value", "hasCost")
# โ value props over hasCost
related("value", "owns", "Product")
# โ Product values over owns
sum(related("value", "hasCost"))
avg(related())
max(related("value", "hasPrice"))
Exactly one match: hasOne
When you need exactly one related value (not a list), wrap with hasOne:
hasOne(related("value", "input", "spreadsheetFile"))
# โ single value dict; errors if 0 or more than one match
This mirrors script-side node.R.<rel>.hasOne() cardinality checks. Prefer an alias when the source is named; use hasOne(related(...)) for the unaliased case instead of a bare [0] index.
# Preferred when aliased
max(col(trial["people"], 1))
# Unaliased edge case
max(col(hasOne(related("value", "input", "spreadsheetFile"))["people"], 1))
Tables: col() and aggregates
col(rows, index_or_name)
Extract one column from a list of rows:
| Row shape | Second argument | Example |
|---|---|---|
| List of lists / tuples | Integer index (0-based) | col(trial["people"], 1) |
| List of dicts | String key | col(trial["people"], "age") |
# people = [["fred", 23], ["john", 26], ["peter", 92]]
max(col(trial["people"], 1)) # โ 92
sum(col(trial["people"], 1))
avg(col(trial["people"], 1))
min(col(trial["people"], 1))
Missing keys or out-of-range indexes are errors (so mistakes are visible).
List comprehensions (escape hatch)
Still valid when you need custom logic:
max([age for name, age in trial["people"]])
max([row[1] for row in trial["people"]])
sum([x for x in related() if x > 100])
For plain column extract + aggregate, col is clearer.
Empty lists
max([]) and min([]) fail. Ensure the source list is non-empty, or handle emptiness upstream.
Operators and conditions
Arithmetic and comparison
+ - * / ^ ยท == != < > <= >= ยท and or not
(price + tax) * qty
revenue > cost and status == "approved"
Conditional expression (ternary)
Use the expression form, not an if-statement:
# Valid
"high" if a > 10 else "low"
A1 if B2 > 0 else B2 + A1
# Invalid (statement โ not allowed in a single expression)
# if B2 > 0: A1 else: B2 + A1
Parentheses and strings
(price + tax) * qty
"hello"
'world'
Built-in functions (summary)
Nodlin-specific
| Function | Returns | Role |
|---|---|---|
related([prop,] [relation,] [type]) | List | Values from inbound related nodes |
sum(list) | Number | Sum numerics (non-numerics ignored) |
avg(list) | Number | Average numerics |
col(rows, index|name) | List | Column from list-of-rows |
hasOne(list) | Any | Exactly one list element |
Starlark (also available)
max, min, len, abs, sorted, enumerate, zip, any, all, int, float, str, bool, list, dict, type, โฆ
Modules
math.sqrt(16)
math.pi
math.ceil(3.2)
time.now()
# (time helpers as provided by the expression environment)
Result types and display
An expression evaluates to one of:
| Type | Example | On the node |
|---|---|---|
| Number | price * qty | Formatted value (scale, currency, %) |
| Condition | revenue > cost | TRUE / FALSE (or icons) |
| List | related() / col(...) | Compact list summary |
| Map | dict result | Key summary |
Formatting (on the expression node, not in the formula)
| Setting | Purpose |
|---|---|
| Scale | Decimal places (0โ8) |
| Currency / PCT | e.g. USD, EUR, or percentage |
| Size / colours | Node appearance |
| Show label | Title vs comment |
| List icon options | First / last / both for list results |
Errors surface with a clear error state on the node (and in the form) so formula mistakes are visible.
Worked examples
Totals and tax
Aliases: product = 29.99, taxRate = 0.08, qty = 3
(product + product * taxRate) * qty
# โ 97.1694 (format as USD, scale 2 โ $97.17)
Average cost over a relationship
avg(related("value", "hasCost"))
# connected costs [10, 15, 25] โ 16.67
Profit check
(product * qty) > sum(related("value", "hasCost"))
# โ True
Spreadsheet table column
Spreadsheet alias trial, value.people = name/age rows:
max(col(trial["people"], 1))
# โ 92
Single related spreadsheet without alias
max(col(hasOne(related("value", "input", "spreadsheetFile"))["people"], 1))
Writing expressions in the UI
Expression fields use a CodeMirror editor (Python highlighting) with an expandable helper panel:
- Named inputs โ insert aliases; open Fieldsโฆ for dict paths
- Aggregate column โ for list-of-rows fields, pick column + max/min/sum/avg โ inserts
max(col(...)) - Related list โ build
related(...)/sum(related(...))/hasOne(related(...)) - Operators โ chips and templates (
col, comprehensions, conditionals)
Habit that pays off: alias the source node, use Fieldsโฆ + Aggregate column, avoid hand-written related()[0] when hasOne or an alias will do.
Forms can also embed the same control via webform.addExpressionEditor โ see Forms.
Expressions vs scripts (checklist)
| Need | Prefer |
|---|---|
| One calculated number / condition / list | Expression |
| Reuse that value by name on neighbours | Expression + alias |
| Custom form, SVG, create/link nodes | Script |
| Multi-step workflow / actions | Script |
| Domain-specific node type | Script package |
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Unknown name | Missing alias, or node not inbound-related |
hasOne(): no items / >1 items | Zero or multiple matches โ fix links or filter relation/type |
col(): โฆ out of range / key not found | Wrong column index/name or ragged rows |
max/min on empty list | No rows โ check source path |
| Invalid if/else | Use a if cond else b, not if cond: โฆ |
| Type without relation | related requires relation whenever type is set |
Further reading
- Implementation-oriented language notes live with the expression agent in the server repo (
NODLIN_EXPRESSION_LANGUAGE_SPEC.md). - Script API (including relationship
hasOneon nodes): Starlark extensions - Starlark spec