Operators

Operators

The deterministic control-flow nodes — Choice, Map, Decision Table, Rules, Expression, and Sub-workflow — what each one does and when to reach for it.

Operators are the nodes in dynamiq.nodes.operators that shape a flow without calling a model: they branch, iterate, decide, compute, and delegate. They cost nothing per run, behave the same way on every input, and combine freely with LLM, agent, and tool nodes in the same Flow.

The operators at a glance

NodeWhat it doesGuide
ChoiceEvaluates JSONPath conditions over its input and gates the nodes that depend on each option.Choice node
MapRuns one inner node once per item of a list, optionally in parallel.Map node
DecisionTableMatches typed inputs against rows of condition cells and returns the outputs of the matching rows.Decision Table
RulesChecks a record against a list of rules and returns one finding per rule, with the values each check read.Rules
ExpressionComputes new fields from its inputs with sandboxed Jinja2 expressions.Expression
SubWorkflowRuns another Flow as one step and returns its Output node's result.Sub-workflow

All six import from one place:

from dynamiq.nodes.operators import Choice, DecisionTable, Expression, Map, Rules, SubWorkflow

Every operator is an ordinary Node. It takes depends (or .depends_on()), input_transformer (or .inputs()), error_handling, caching, and callbacks like any other node — see Workflows, Flows & Nodes — and it serializes to YAML and appears in traces with its input and output.

Choosing between them

  • Route with Choice when the next step depends on a condition and the branches are different nodes.
  • Decide with DecisionTable when the logic is a matrix of rules — eligibility checks, pricing grids, scorecards, lookups — that a domain expert should be able to read and edit. One table replaces a chain of Choice nodes or a Python function full of if statements, and the rules that fired come back with the result.
  • Review with Rules when every check must be reported, not only the first match — a claim adjudication, an invoice match, a compliance screen, a pre-funding review. Each rule reads the record by path and has a severity, and its finding says whether it passed, failed, warned, did not apply or could not be evaluated; a missing value never passes silently.
  • Compute with Expression when you need a derived value — a ratio, a rounded total, a label — and a Python node would be overkill.
  • Reuse with SubWorkflow when a decision or a pipeline is shared by several workflows, or when a batch should run it once per item inside a Map.
  • Iterate with Map when the same node must run for every element of a list.

The decision operators are built for each other: a table's outputs feed an expression, a rule set's status drives a Choice and its findings feed an expression that collects reason codes, and the whole chain packages into a SubWorkflow that a Map runs over a batch. The loan pricing example in the repository's examples/components/core/dag directory, decision_workflow.yaml with run_decision_workflow.py, wires all of them together in YAML and runs offline.

Choice: first or all matching branches

By default a Choice follows the first option whose condition holds: every option after it is skipped, and an option without a condition is the else branch. Set hit_policy="all" to run every option whose condition holds. An option without a condition stays the fallback — under all it runs only when no conditioned option matched, wherever it sits in the list.

from dynamiq import Workflow
from dynamiq.flows import Flow
from dynamiq.nodes.node import NodeDependency
from dynamiq.nodes.operators import Choice, ChoiceOption, Expression
from dynamiq.nodes.types import ChoiceCondition, ConditionOperator, ExpressionItem
from dynamiq.runnables import RunnableConfig

route = Choice(
    id="route",
    name="route",
    hit_policy="all",
    options=[
        ChoiceOption(
            id="large",
            name="large",
            condition=ChoiceCondition(
                operator=ConditionOperator.NUMERIC_GREATER_THAN, variable="$.amount", value=1000
            ),
        ),
        ChoiceOption(
            id="international",
            name="international",
            condition=ChoiceCondition(
                operator=ConditionOperator.STRING_EQUALS, variable="$.region", value="EU"
            ),
        ),
        ChoiceOption(id="default", name="default"),  # no condition: the fallback
    ],
)

large_review = Expression(
    id="large_review",
    name="large_review",
    expressions=[ExpressionItem(key="check", expression="'manual review above 1000'")],
    depends=[NodeDependency(route, option="large")],
)
vat = Expression(
    id="vat",
    name="vat",
    expressions=[ExpressionItem(key="vat", expression="(amount * 0.2) | round(2)")],
    depends=[NodeDependency(route, option="international")],
)
plain = Expression(
    id="plain",
    name="plain",
    expressions=[ExpressionItem(key="check", expression="'no checks'")],
    depends=[NodeDependency(route, option="default")],
)

workflow = Workflow(flow=Flow(nodes=[route, large_review, vat, plain]))
result = workflow.run(
    input_data={"amount": 2500, "region": "EU"},
    config=RunnableConfig(callbacks=[]),
)
for node_id in ("large_review", "vat", "plain"):
    print(node_id, result.output[node_id]["status"], result.output[node_id]["output"])
# large_review success {'check': 'manual review above 1000'}
# vat success {'vat': 500.0}
# plain skip None

With the default hit_policy="first" the same input runs large_review only: vat is skipped because an earlier option already matched. In YAML the field is hit_policy: all on the node. The condition operators, AND/OR groups, and the _PATH variants that compare two inputs are covered in the Choice node guide.

A node behind an option that did not match is recorded with status skip, not failure, and nodes downstream of it are skipped too. Read the statuses in result.output to tell the branches apart — see Running workflows & results.

On this page