Expression
Compute derived fields from named inputs with sandboxed Jinja2 expressions — typed results, pass-through, missing-input semantics, and the YAML form.
Expression computes new fields from its inputs. Each output is a Jinja2 expression — the body of a template variable without the braces — evaluated with the node's inputs as its names. The expression returns a value, not rendered text: price * quantity is a number, [a, b] is a list, 'A' if score < 0.5 else 'B' is a string. Use it where a Python node would be a one-liner: a ratio, a rounded total, a tier label, a flag.
A first expression
from dynamiq.nodes.operators import Expression
from dynamiq.nodes.types import ExpressionItem, NamedField
from dynamiq.runnables import RunnableConfig
pricing = Expression(
name="pricing",
input_fields=[NamedField(name="price"), NamedField(name="quantity")],
expressions=[
ExpressionItem(key="total", expression="(price * quantity) | round(2)"),
ExpressionItem(key="tier", expression="'bulk' if quantity >= 10 else 'single'"),
ExpressionItem(key="items", expression="[price, quantity] | map('float') | list"),
],
)
result = pricing.run(input_data={"price": 2.5, "quantity": 12}, config=RunnableConfig(callbacks=[]))
print(result.output)
# {'total': 30.0, 'tier': 'bulk', 'items': [2.5, 12.0]}Configuration
namestrinput_fieldslist[NamedField]expressionslist[ExpressionItem]pass_throughboolExpressionItem(id, key, expression) and NamedField(id, name, type) come from dynamiq.nodes.types; id is generated when omitted.
What an expression can do
Anything a Jinja2 expression can: arithmetic and comparisons, string and list literals, if / else, and / or / not, in, attribute and item access on the inputs (applicant.fico, scores[0]; a record's key is read before a method of the same name, so invoice.items is the record's list), and the built-in filters — round, abs, length, lower, upper, trim, default, map, select, sum, min, max, join, int, float, string, list, and the rest of the Jinja2 filter set. The helpers a rule can call are available too: has(value) (present and not null), days_between(start, end), date(value) (an ISO or MM/DD/YYYY string, a date or a datetime), today(), len, abs, min, max, sum and round, so a due date or an age is one expression. Every expression runs in one module-level ImmutableSandboxedEnvironment, so it cannot mutate its inputs, reach Python internals through attributes such as __class__, or run an unbounded range.
| Expression | Result for {"amount": 250, "region": "EU", "scores": [3, 5, 4]} |
|---|---|
amount * 1.2 | 300.0 |
(amount * 0.2) | round(2) | 50.0 |
'high' if amount > 200 else 'low' | 'high' |
region in ['EU', 'UK'] | True |
scores | max | 5 |
(scores | sum) / (scores | length) | 4.0 |
discount | default(0) | 0 (default replaces a name that is missing; a present None stays None) |
(discount or 0) | 0, whether discount is missing or None |
Missing inputs and errors
- An input referred to on its own that is missing evaluates to
None(discount→None), so a plain reference never fails. An input missing inside a list or a dict the expression builds isNonethere too ([price, discount]→[2.5, None]), so the output stays serializable. An expression left lazy,items | map(attribute='price')without a closing| list, returns a list rather than a generator, for the same reason. - A missing input used inside an operation fails the run:
price * quantitywithoutquantityraises'quantity' is undefined. An input that is present butNonefails the same way:base_rate + llparaises aTypeErrorwhenllpaisNone. - Guard such inputs with
(x or 0), which covers both cases.| default(0)replaces only a name that is missing — Jinja2'sdefaultacts on an undefined name, and a presentNonepasses through it — so anullfrom upstream still reaches the arithmetic;| default(0, true)also replacesNoneand every other falsy value. - An expression that reaches for internals is refused by the sandbox at run time.
- An input named like a helper (
date) is the input where an expression reads it as a value and the helper where an expression calls it; an expression that does both is refused at construction. - An input named
selfpasses through but cannot be read: Jinja reserves the name inside an expression, so an expression reading it is refused at construction; aselfkey inside a record (payload.self.href) reads like any other. - A syntax error, a key that is not an identifier, or a key used twice fails at construction — or at YAML load — with a message naming the node and the key.
from dynamiq.nodes.operators import Expression
from dynamiq.nodes.types import ExpressionItem
try:
Expression(name="ratio", expressions=[ExpressionItem(key="ltv", expression="loan / /")])
except ValueError as error:
print(error)
# Expression 'ratio': 'ltv' is not a valid expression: unexpected '/'Run-time failures go through the node's error handling like any other node's.
Pass-through
With pass_through=True the output holds every input next to the computed fields, so a later node can read both from one place. An expression with the same key as an input replaces it:
from dynamiq.nodes.operators import Expression
from dynamiq.nodes.types import ExpressionItem
from dynamiq.runnables import RunnableConfig
bump = Expression(
name="bump",
pass_through=True,
expressions=[ExpressionItem(key="quantity", expression="quantity + 1")],
)
print(bump.run(input_data={"price": 2.5, "quantity": 1}, config=RunnableConfig(callbacks=[])).output)
# {'price': 2.5, 'quantity': 2}Inside a flow
An expression reads its inputs from upstream nodes through the usual mapping. Here it computes a rate from the workflow input and from a decision table's collect output:
from dynamiq import Workflow
from dynamiq.flows import Flow
from dynamiq.nodes import InputTransformer
from dynamiq.nodes.node import NodeDependency
from dynamiq.nodes.operators import DecisionTable, Expression
from dynamiq.nodes.types import DecisionRule, ExpressionItem, NamedField
from dynamiq.nodes.utils import Input, Output
from dynamiq.runnables import RunnableConfig
start = Input(id="start", name="start")
adjustments = DecisionTable(
id="adjustments",
name="adjustments",
hit_policy="collect",
aggregation="sum",
input_columns=[NamedField(name="fico", type="int"), NamedField(name="ltv", type="int")],
output_columns=[NamedField(name="llpa", type="float")],
rules=[
DecisionRule(id="a1", name="mid fico", when=["[680..739]", ""], then=["0.75"]),
DecisionRule(id="a2", name="high ltv", when=["", "> 80"], then=["0.25"]),
],
depends=[NodeDependency(node=start)],
input_transformer=InputTransformer(selector={"fico": "$.start.output.fico", "ltv": "$.start.output.ltv"}),
)
rate = Expression(
id="rate",
name="rate",
input_fields=[NamedField(name="base_rate"), NamedField(name="llpa")],
expressions=[
ExpressionItem(key="rate", expression="(base_rate + (llpa or 0)) | round(3)"),
ExpressionItem(key="tier", expression="'A' if (llpa or 0) < 0.5 else 'B'"),
],
depends=[NodeDependency(node=start), NodeDependency(node=adjustments)],
input_transformer=InputTransformer(
selector={"base_rate": "$.start.output.base_rate", "llpa": "$.adjustments.output.llpa"}
),
)
end = Output(
id="end",
name="end",
depends=[NodeDependency(node=rate)],
input_transformer=InputTransformer(selector={"rate": "$.rate.output.rate", "tier": "$.rate.output.tier"}),
)
workflow = Workflow(flow=Flow(nodes=[start, adjustments, rate, end]))
result = workflow.run(
input_data={"fico": 700, "ltv": 85, "base_rate": 6.5},
config=RunnableConfig(callbacks=[]),
)
print(result.output["end"]["output"])
# {'rate': 7.5, 'tier': 'B'}llpa or 0 guards the case where no adjustment matched: a collect table with sum returns None when no rule fired, and llpa | default(0) would let that None through into the addition.
YAML
nodes:
rate:
type: dynamiq.nodes.operators.Expression
name: rate
input_fields:
- { id: base_rate, name: base_rate }
- { id: llpa, name: llpa }
expressions:
- { id: x1, key: rate, expression: "(base_rate + (llpa or 0)) | round(3)" }
- { id: x2, key: tier, expression: "'A' if (llpa or 0) < 0.5 else 'B'" }
pass_through: false
depends:
- node: start
- node: adjustments
input_transformer:
selector:
base_rate: $.start.output.base_rate
llpa: $.adjustments.output.llpaQuote every expression: |, ', {, and [ all mean something to YAML.
Judgement
Ask typed questions about a record and get calibrated answers — three judges, the confidence measure, thresholds, and routing into Choice and Decision Table.
Sub-workflow
Run another Flow as one step — inputs and outputs, isolation per run, tracing, error propagation, recursion guards, Map fan-out, and nested flows in YAML.