Expressions and policy rules
The run context, JSON Logic expressions, {{path}} text templates, the variables nodes set, the policy check's rules, ship-to addresses and the pricing chain.
Node configs hold data, not code. Conditions are JSON Logic expressions and
text fields interpolate {{path}} templates, so a flow is safe to store, to diff and to accept from
the copilot. Both are evaluated by packages/core/src/logic.ts.
The run context
Section titled “The run context”Every expression and template sees one object:
| Path | Contents |
|---|---|
order |
The order: id, type, title, customerId, currency, lines, meta, subtotal, tax, total, createdAt, and once priced fxRate and baseTotal (see Currency). Pricing and tax nodes update the totals as they run |
vars |
Run variables: seeded by the start request’s vars, then set by nodes and events (below). Carried across continue-as-new |
nodes |
Each node’s latest output, by node id, for example nodes.risk.score |
input |
The order’s meta |
now |
When this Workflow run started, in epoch milliseconds |
JSON Logic
Section titled “JSON Logic”Fields of kind logic take a JSON Logic expression: Condition’s expression, Loop’s while, Wait’s
until, Refund’s amount, and each value in Set variables’ assign. { "var": "order.total" }
reads a path from the context.
{ "and": [ { ">": [{ "var": "order.total" }, 1000] }, { "==": [{ "var": "vars.category" }, "it_hardware"] }] }OrderLoop adds three operations:
| Operation | Result |
|---|---|
{ "now": [] } |
The current time in epoch ms |
{ "days": [n] } |
n days in ms: { "+": [{ "var": "now" }, { "days": [14] }] } |
{ "len": [x] } |
Length of an array or string, else 0 |
A condition is true by JSON Logic’s truthiness: empty arrays, 0, "" and null are false. A plain
value (a number, true) is its own result, which is why Loop’s default while is true.
Expressions that read the clock run inside a step, so a replay sees the same value.
Text templates
Section titled “Text templates”Text fields replace {{path.to.value}} with the value at that path in the run context. A missing
value becomes an empty string and an object is written as JSON. Templates apply to Approval’s
title, Notify’s subject and body, End’s reason, Refund’s reason and Generate PO’s
supplier.
Order {{order.id}} is on its way ({{vars.tracking}})Variables nodes set
Section titled “Variables nodes set”| Variable | Set by |
|---|---|
policyFailures |
Policy check: the failure messages |
suggestedAddress |
Validate address, on corrected: the normalised ship-to |
refunded |
Refund, on ok: the amount refunded |
invoiceKey, dueDate |
Generate invoice |
poKey, supplier |
Generate PO (Demand forecast may also set supplier) |
tracking |
Create shipment |
returnTracking |
Return label |
erpRef |
ERP sync |
poNumber |
Extract order |
risk |
Risk score: the 0–100 score |
configurable (category) |
Classify: the label it chose, under its into field |
configurable (summary) |
Summarize: the summary, under its into field |
| any | Set variables: one per key in assign |
A Wait for event node stores the event’s payload under the event name, with each run of
characters other than letters and digits replaced by _: return.requested becomes
vars.return_requested, so a refund can use { "var": "vars.return_requested.amount" }.
Policy rules
Section titled “Policy rules”The Policy check node’s rules field is a list of rule
objects, checked by packages/core/src/policy.ts. Every rule runs, and the node takes fail with
every failure listed (in vars.policyFailures), so a customer hears about all of them at once. The
rule names are min_order_amount, max_order_amount, max_qty_per_sku, business_hours, no_po_box, restricted_skus.
| Rule | Fields | Fails when |
|---|---|---|
min_order_amount |
amount |
order.total is below amount |
max_order_amount |
amount |
order.total is above amount |
max_qty_per_sku |
qty (integer) |
Any SKU’s quantity, summed across lines, exceeds qty |
business_hours |
start (0–23), end (1–24), days (0 = Sunday … 6; default Monday–Friday), timeZone (default UTC) |
The order is checked outside [start, end) on the listed days, in timeZone |
no_po_box |
order.meta.shipTo (a string, or an object of address lines) contains a PO box |
|
restricted_skus |
skus (non-empty) |
Any line’s SKU is in skus |
[ { "rule": "min_order_amount", "amount": 25 }, { "rule": "max_qty_per_sku", "qty": 500 }, { "rule": "business_hours", "start": 8, "end": 18, "timeZone": "America/Chicago" }, { "rule": "no_po_box" }]Ship-to addresses
Section titled “Ship-to addresses”Validate address reads order.meta.shipTo as an
object with line1, city, postalCode and country, and optionally name, line2 and
region. checkAddress in packages/core/src/address.ts trims and collapses whitespace, maps
country names to two-letter codes ("usa" → US, "UK" → GB), and requires a state or province
for US, CA and AU, accepting the full name ("Illinois" → IL). It formats postcodes for US
(627011234 → 62701-1234), CA (M5V 3L9), GB (SW1A 2AA), AU and DE; other countries
only need one. The node takes valid when nothing changed, corrected when normalising changed a
field (the result is in vars.suggestedAddress, the order is untouched), and invalid with every
problem in its summary and output, including a missing or free-text ship-to.
{ "line1": "1 Main St", "city": "Springfield", "region": "IL", "postalCode": "62701", "country": "US" }Pricing chain
Section titled “Pricing chain”Price & quote prices each line through a chain of sources in
price_sources, lowest priority first. The seeded chain is price books (10, stops), contract
prices (20, stops) and volume tiers (30, keeps going), with list price (products.price) as the
fallback. resolvePrice in packages/core/src/pricing.ts:
- For each active source in order, finds its entries that match the line and takes the lowest price among them, so the deepest volume tier wins.
- Adds that price as a candidate. If the source has
stop_processing, the chain ends there. - Gives the line the lowest candidate, or the list price when no source matched.
A matching contract price therefore wins even above list price, and with the seeded priorities a
customer’s contract is reached before a cheaper volume tier. Entries in price_rules (price books
and volume tiers) match when every scope they set fits: sku, customer_id, customer_tier,
min_qty (the line’s quantity), and the starts_at/ends_at window. Each sets either a fixed
price or discount_pct off the list price. Contract prices are the rows in contract_prices for
the order’s customer.
The node’s useContracts and usePriceRules fields drop the contract source or the price-book and
volume sources for that node. A line with its own unitPrice keeps it, and purchase requests and
reorders price at products.cost without the chain. The node’s output lists each line’s
unitPrice, the source (by) and the entry (rule), and its summary counts lines per source. The
back office shows the chain in order.
Currency
Section titled “Currency”An order’s currency is any code with a row in fx_rates; the REST API answers 400 for one
without. Prices, price rules, credit limits, balances, budgets and reports are in the base currency,
USD (BASE_CURRENCY in core). Price & quote reads the rate (units of the order’s currency per one
USD), converts each base price at it, and stores it on the order as fxRate and on the orders row
as fx_rate, so later rate changes never reprice the order. A unitPrice given on the order is
already in the order’s currency. Totals carry baseTotal, the total in USD.
- Amounts on the order, its documents, payments and refunds are in the order’s currency.
- Credit check and budget check compare
baseTotal; the credit check reports in the order’s currency, and invoicing addsbaseTotalto the customer’s balance. - The dashboard converts at each order’s
fx_rate, so its figures are in USD. order.totalin an expression is in the order’s currency; useorder.baseTotalfor a threshold that should hold across currencies. Policy rule amounts compareorder.total.
The seeded rates (EUR 0.92, GBP 0.79, CAD 1.37) are demo values; a real deployment refreshes
fx_rates from a rates provider.
Durations
Section titled “Durations”Fields of kind duration are a whole number and a unit: second, minute, hour, day, week,
month (30 days) or year (365 days), singular or plural: "30 seconds", "2 days". In the
engine every wait and timeout is divided by TIME_SCALE and never drops below one second.