WorkflowsOrchestration

Rules Node

Check a record against a list of rules — inputs read by path, derived values, severities, conditions and effective windows, messages that insert the record's values, CSV import and export, and findings in test cases.

The Rules node checks a record against a list of rules and reports one finding per rule. The record is whatever the upstream nodes produce — a claim with its policy, an invoice with its purchase order and receipt, a prescription with the formulary, a loan file with its documents — and each rule is a check written against it: the loss date falls inside the policy period, the invoice total is within 2% of the order, an adult member has an ID on file. Where a Decision Table picks the matching rows and returns their outputs, a Rules node evaluates every rule and says which held, which did not, and which could not be decided — the shape of a review, an audit, a pre-submission check or a compliance screen.

At run time the node returns status, the record's overall result; summary, a count per status; findings, one entry per enabled rule in list order with the values each check read (a rule switched off contributes none, so the list contracts rather than holding a placeholder); and derived, the values it computed.

The Rules configuration panel: three named inputs mapped from the Input node, a derived value and the missing-value policy
Claim adjudication: the claim, the policy and the claimant's history are the inputs; the payable amount is derived; each rule reads them by path.

Configure a Rules node on the canvas

Add the node and name it

Drag Rules from the Logic group of the node palette onto the canvas and connect an edge from the node that produces the record it checks — the Input node, an agent with a response format, a document extraction. Give it a short Name: later nodes read its outputs as $.<name>.output.status and $.<name>.output.findings, and the By rule view of the test cases lists its rules under it.

Name the inputs

Under Inputs, click Add input for each record the rules check and give it a Name and a Source — press / to pick an upstream output. Inputs are not typed: a rule reads a value by path, so policy.limits.theft reads the theft limit of the policy and items[0].amount the first item's amount, and a record of any shape needs no mapping beyond naming it.

An input named as_of, an ISO date, fixes the date the effective windows are compared with; without it the run date is used.

Inside a Map, an input needs no source: each item of the list is the input, and an input reads the item's field of the same name, so a batch of claims or invoices is reviewed one record at a time with the same rules.

Add derived values

Under Derived values, add the values several rules share — a ratio, a variance, an amount — with a Name and an Expression. They are computed once per record, in order, before the rules run, and a rule reads them by name like an input; a later derived value can read an earlier one. A value that cannot be computed, such as a division by a missing number, is null, and a rule that reads it is reported as not evaluated rather than failed. A member the expression could not find inside a list or an object it builds is null there too, so a list of discounts over items that lack one keeps a null in its place.

Write the rules

Click Add rule for each check. A rule has a Name, a Severity, a Check — the expression that must hold for the record to pass, such as claim.estimate <= policy.limits[claim.loss_type] — an optional Applies when condition, and a Message, a template rendered with the whole record when the check does not hold, every input and derived value and not only what the check read, so it can name an id or a limit the check never touched: Estimate {{ claim.estimate }} is above the {{ claim.loss_type }} limit. Details holds what a finding carries for the system that receives it: Category, Reason code, Effective from and Effective until, Tags and References.

SeverityWhen the check does not hold
FailThe finding is fail and the record's status is fail. The default.
WarnThe finding is warn; the record's status is warn unless another rule failed.
InfoThe finding is info and the status is unchanged — a note, not a defect.
The Rules section of the panel: each rule with its name, its check, a severity badge and an on/off switch, the last rule switched off

The switch on each rule takes it out without deleting it; an off rule reports nothing. The side panel lists the first 25 rules, and adding a rule past them opens the editor on it; Open editor opens the whole list in a dialog with a search box and severity and category filters, which is where a longer rule set is easier to work with.

The expanded rules editor: one rule opened to its name, severity, check, applies-when condition and message, the other rules collapsed under it

Decide what a missing value means

When a check reads a missing value sets what a rule reports when a value it reads is absent or null — a document that was never uploaded, a field the extraction did not find. By default the rule is reported as not evaluated, for someone to review, and the record's status is not_evaluated rather than pass. Report the rule's severity is the strict policy: a missing document counts as a failed check. Either way a missing value never passes a rule silently, and the record's status is never pass while a value was missing: a rule at severity info reports info under the strict policy, and the record still reads not_evaluated.

Connect the findings

Draw an edge from the node to the nodes that act on its result. The variable picker lists status, summary, findings, derived and each derived value as derived.<name>. A Choice condition on $.<name>.output.status routes a failed record to review; an Expression collects the reason codes of the findings that fired — findings | selectattr('status', 'in', ['fail', 'warn']) | map(attribute='reason_code') | list; an agent turns the findings into a letter; the Output node returns them to the caller.

Expression syntax

A check, an applies-when condition and a derived value use the same expression engine as the Expression node — Jinja2 expressions, without the braces — plus a few functions for records. The Expression syntax button under the rules opens the same reference in the editor. An expression may run over several lines, since a line break reads as a space, so a long condition can put one clause on each.

ExpressionMeaning
invoice.total <= po.total * 1.02, min(fee, 250)arithmetic and comparisons on values read by name or by path; min, max and abs
620 <= fico <= 850, currency in ['USD', 'EUR']within a range, or one of several values
ltv <= 0.8 and (dti <= 0.43 or has(reserves))and, or and not combine conditions; brackets group them
abs(invoice.total - po.total) <= 0.01, round(amount * rate, 2)money: compare within a tolerance, since 0.1 + 0.2 is not exactly 0.3, and round to cents
has(guarantor)the value is present: defined and not null
days_between(invoice.due_date, today()) > 30, date(maturity) < date('2030-01-01')the days from one date to the next, and a date from text as YYYY-MM-DD or MM/DD/YYYY
len(invoice.lines) > 0, invoice.lines | sum(attribute='amount')count a list, and total a field across it
'prime' if fico >= 720 else 'subprime'a value that depends on a condition

Jinja filters work over lists too: (household.members | selectattr('age', 'ge', 18) | rejectattr('id_on_file') | list | length) == 0 holds when every adult has an ID on file. A chain that ends in map, select or selectattr reads as a list whether or not | list closes it, in a check, a condition and a derived value alike, so every rule reads the same values.

A value the check needs that is missing makes the rule not evaluated rather than passing or failing it silently — a comparison against a missing lookup is never simply false. Ask about an optional value with has(...): in has(docs.FloodCert) and docs.FloodCert.zone in ['A', 'V'] the guarded read is optional, so a missing certificate lets the check decide instead of blocking it. A method call reads the value it is called on, so invoice.get('vat_rate', 0) needs invoice and reads 0 when the key is absent. A field named like a method of the record, items or update say, is the field: the method is reached only when the record has no such key. A field named like a helper, such as date, is the field where a check uses it as a value and the helper where a check calls it. A top-level input named self is the one name a check cannot read, since the expression engine reserves it: the runtime refuses such a check when the workflow is built, naming the rule, so read a self link inside a record (payload.self.href) or rename the input. A message inserts a value the same way a prompt does: {{ claim.estimate }}.

The editor blocks Save and Test on a rule without a check, a repeated id or a malformed effective window, naming the rule. The runtime compiles every expression when the workflow is built and refuses one it cannot parse before any record runs, naming the rule as well.

The Expression syntax reference open beside the rules, listing each form with an example

How a record is scored

Every rule that is on reports a status, and the finding carries the values the check read under evaluated, so a reviewer sees why without re-running anything.

StatusMeaning
passThe check held.
fail, warn, infoThe check did not hold; the status is the rule's severity, and the message is rendered with the whole record.
not_applicableApplies when did not hold for this record, or the record's date is outside the rule's effective window; the message says which.
not_evaluatedA value the check reads is missing, or the check could not be evaluated; the message names the value. Under the strict policy the rule reports its severity instead.

The record's status is fail if any rule failed, else warn if any warned, else not_evaluated if any check did not run, else pass. A check did not run when its finding is not_evaluated, and also when, under Report the rule's severity, the rule reported its severity for a missing value: an info finding never raises the status, so a record whose only gap sits under an info rule reads not_evaluated, not pass. A record passes only when every rule that applied was evaluated and held.

Effective windows. A rule with Effective from or Effective until applies only to records dated inside the window: a requirement that starts in January is not applicable to a file dated in December, and stays in the list with its history. The record's date is the as_of input when there is one, otherwise the day of the run.

Import and export rules as CSV

Export CSV downloads the rules with one header row — id, name, category, severity, applies_when, check, message, reason_code, references, tags, effective_from, effective_until, enabled — and one row per rule, the layout a rule owner can review in a spreadsheet and hand back. Import CSV reads it back, or any sheet whose header names the columns in any order, with spaces or underscores, in any case; only check is required. Lists are separated by semicolons, US dates are read as ISO, and a rule's own id is kept so findings and coverage keep naming the same rule after a round trip; a repeated id is refused rather than guessed. Importing into a node that already has rules asks before replacing them. Imports are capped at 10 MB and 5000 rules.

Findings in test cases

After the workflow's test cases run, a case whose output includes the node's findings shows them as a table when expanded — one row per rule, the status as a badge, the message beside it — above the raw output. The By rule view of the same tab lists every rule of every Rules node and Decision Table on the canvas with how many cases fired it — a check that did not hold, or a row that matched — and how many of those cases passed or failed. A rule that never fires across the cases is either dead or untested; a rule that fires in failing cases is the first to look at.

A test case expanded after a run, with its findings as a table: one rule warned, the others passed or did not apply
The By rule view of the Test cases tab: each rule with the cases that fired it and how many of them passed or failed

Worked example: claim adjudication

Inputs claim, policy and history from the Input node — the claim as filed, the policy record and the claimant's history — and a derived value payable = max(claim.estimate - policy.deductible, 0):

#RuleSeverityApplies whenCheckReason code
1Loss inside the policy periodFaildate(policy.effective_from) <= date(claim.loss_date) and date(claim.loss_date) <= date(policy.effective_until)POL-01
2Loss type is coveredFailclaim.loss_type in policy.covered_lossesCOV-01
3Estimate within the limitFailclaim.estimate <= policy.limits[claim.loss_type]COV-02
4Police report for a theftWarnclaim.loss_type == 'theft'has(claim.police_report_no)DOC-01
5Reported within 30 daysWarndays_between(claim.loss_date, claim.reported_date) <= 30FRD-01
6Frequent claimantInfohistory.claims_last_12_months >= 2falseFRD-02

A water-damage claim reported 12 days after the loss, inside the policy period, within the limit, from a claimant with one prior claim: rules 1, 2, 3 and 5 pass, rules 4 and 6 are not_applicable, the status is pass, and derived.payable is the estimate less the deductible. The same claim filed as a theft with no police report number: rule 4 reports warn, the status is warn, and a Choice after the node sends it to an adjuster with DOC-01 as the reason. Rule 6 is the info pattern: its check is false, so it fires whenever it applies and notes the history without touching the status.

Runtime and SDK

The node is dynamiq.nodes.operators.Rules. Expressions run in the same sandbox as the Expression node: they read the record and cannot reach code, and a run of hundreds of rules costs milliseconds. The rules compile when the workflow is built, so a malformed one fails before any record runs, naming the rule. The Error handling tab applies as on any node. The SDK class, the expression grammar and the YAML form are documented in Rules.

Pitfalls

On this page