Step Types
Every step type a blueprint can use, with an example of each and what it puts in the trace.
After reading this guide, you will know:
- The eight step types and when to use each.
- How
rulespicks the first, the highest or every matching rule. - How
run,sumandgroupwork over the rows of a list. - How
comparepicks between earlier steps. - What each step type records in the trace.
The Step Types
| Step | Gives | Use it for |
|---|---|---|
formula |
the formula’s value | arithmetic and small decisions |
rules |
the first, highest or every matching rule’s value | categories by precedence, every rule that applies |
lookup |
a column of a matching row, or the whole row | tables of rates, bands, line-ups |
run |
another calculation’s outputs, once or for each row of a list | reusing a calculation, per-item work |
repeat |
another calculation run again and again, one row each time | rounds of a fight, months of a loan |
sum |
one output added up across an earlier run | totals |
group |
an earlier run’s rows grouped, with some outputs added up | subtotals by category |
compare |
the earlier step with the highest value | the better of two amounts |
rmt describe lists them with an example of each. Every step has a snake_case name and exactly
one step type key.
An Order
The examples below all come from one blueprint, an order at a cafe:
tables:
products:
- {sku: tea, category: drinks, price: 4.50}
- {sku: coffee, category: drinks, price: 5.00}
- {sku: scone, category: food, price: 6.25}
calculations:
order:
inputs:
lines: {type: list}
member: {type: boolean, default: false}
coupon: {type: string, one_of: [none, save5], default: none}
steps:
- name: priced
run: {calculation: line, over: lines}
- name: subtotal
sum: {output: line_total, of: priced}
- name: by_category
group: {of: priced, by: [category], sum: [line_total, qty]}
- name: member_saving
formula: 'IF(member, ROUND(subtotal * 0.1, 2), 0)'
- name: coupon_saving
formula: 'IF(coupon = "save5", MIN(5, subtotal), 0)'
- name: saving
compare: {steps: [member_saving, coupon_saving], pick: highest}
- name: badges
rules:
pick: all
rules:
- {name: big_order, when: subtotal >= 30}
- {name: hungry, when: 'ANY(priced, p, p.category = "food")'}
- {name: regular, when: member}
- name: delivery
rules:
pick: first
rules:
- {name: free_for_members, when: member, value: 0}
- {name: free_over_40, when: subtotal - saving >= 40, value: 0}
- {name: standard, when: true, value: 7.50}
- name: total
formula: subtotal - saving + delivery
outputs: [subtotal, by_category, saving, badges, delivery, total]
line:
inputs:
sku: {type: string, one_of: {table: products, column: sku}}
qty: {type: number}
steps:
- name: product
lookup: {table: products, where: {sku: sku}}
- {name: category, formula: product.category}
- {name: line_total, formula: product.price * qty}
outputs: [category, qty, line_total]
$ cat order_facts.json
{"lines": [{"sku": "tea", "qty": 2}, {"sku": "scone", "qty": 3}, {"sku": "coffee", "qty": 1}], "member": true}
$ rmt run order.yml --facts order_facts.json
subtotal 32.75
by_category
category line_total qty
drinks 14 3
food 18.75 3
saving 3.28
badges
big_order big_order
hungry hungry
regular regular
delivery 0
total 29.47
formula
Works out a value with a formula over inputs, tables and earlier steps:
- name: member_saving
formula: 'IF(member, ROUND(subtotal * 0.1, 2), 0)'
The trace records the formula. See Formulas and Functions for everything a formula can say.
rules
Chooses between rules by their when conditions. Every rule’s condition is evaluated, then pick
decides:
pick |
Gives |
|---|---|
first |
the first matching rule’s value, in order |
highest |
the largest value among the matching rules |
all |
every matching rule, as {name: value} |
A rule’s value is a formula. A rule without one yields its name. when: true matches everything, which
suits a catch-all last rule.
Categories by precedence use pick: first, most specific rule first:
- name: delivery
rules:
pick: first
rules:
- {name: free_for_members, when: member, value: 0}
- {name: free_over_40, when: subtotal - saving >= 40, value: 0}
- {name: standard, when: true, value: 7.50}
Every rule that applies uses pick: all. Above, badges came back as
{big_order: big_order, hungry: hungry, regular: regular}; a small order gets {}.
The best of several uses pick: highest:
best_reward:
inputs:
points: {type: number}
spend: {type: number}
steps:
- name: reward
rules:
pick: highest
rules:
- {name: points_back, when: points >= 100, value: ROUNDDOWN(points / 100) * 5}
- {name: spend_back, when: spend >= 50, value: 'ROUND(spend * 0.05, 2)'}
- {name: nothing, when: true, value: 0}
outputs: [reward]
The trace lists every rule, whether it matched, the values its condition read and whether it was chosen:
$ echo '{"points": 250, "spend": 120}' | rmt run order.yml --calculation best_reward --trace --json
{
"outputs": {
"reward": "10"
},
"steps": [
{
"step": "reward",
"value": "10",
"rules": [
{
"rule": "points_back",
"condition": "points >= 100",
"matched": true,
"facts": {
"points": "250"
},
"chosen": true
},
{
"rule": "spend_back",
"condition": "spend >= 50",
"matched": true,
"facts": {
"spend": "120"
},
"chosen": false
},
{
"rule": "nothing",
"condition": "true",
"matched": true,
"facts": {},
"chosen": false
}
]
}
]
}
That last rule is quoted in the YAML because its formula has a comma inside {...}. Leave the quotes off
and check says so:
best_reward: step reward rule spend_back has a formula that cannot be read: too many opening parentheses. If it sits inside {...} in the YAML, quote it, because YAML splits {...} on commas
lookup
Takes a value from the row of a table whose columns match earlier values:
- name: product
lookup: {table: products, where: {sku: sku}}
where maps a column to the name of a value to match: here the sku column must equal the sku input.
With value: price the step is that column; without value it is the whole row, which formulas read as
product.category and product.price.
table names a blueprint table, a list input or an earlier step that is a list of rows. on picks rows
in effect on a date, and band picks the band a number falls in. See
Tables and Lookups.
The trace records the matching row:
product sku tea, category drinks, price 4.5 row sku tea, category drinks, price 4.5
run
Runs another calculation. Without over it runs once, with the values so far. With over it runs once
for each row of a list input or table:
- name: priced
run: {calculation: line, over: lines}
Each row sees the parent’s values plus its own fields (sku, qty), but never the list it belongs to.
It hands back only the calculation’s outputs. The trace has each row’s own steps:
priced 3 rows over lines
row 1
product sku tea, category drinks, price 4.5 row sku tea, category drinks, price 4.5
category drinks product.category
line_total 9 product.price * qty
...
A run once is read in later steps as name.output. A run over a list is a list of rows, read with
sum, group, or FILTER, PLUCK and ANY in a formula, as badges does with
ANY(priced, p, p.category = "food").
run also takes with, position, carry, start, earlier and update. See
Chaining with Imports and Runs and
Running over Lists.
repeat
Runs another calculation again and again, each time as one more row: times: n, or until: output with
a required limit:
- name: rounds
repeat: {calculation: combat_round, position: round, carry: {standing: standing_after}, until: fight_over, limit: 50}
sum
Adds up one output across the rows of an earlier run:
- name: subtotal
sum: {output: line_total, of: priced}
subtotal is 9 + 18.75 + 5 = 32.75.
group
Groups the rows of an earlier run by some outputs and adds up others, one row per group, in the order each group first appears:
- name: by_category
group: {of: priced, by: [category], sum: [line_total, qty]}
[
{"category": "drinks", "line_total": "14", "qty": "3"},
{"category": "food", "line_total": "18.75", "qty": "3"}
]
Only outputs can be grouped and summed, which is why line outputs qty as well as line_total.
compare
Picks one of several earlier steps by value. pick: highest is the one there is:
- name: saving
compare: {steps: [member_saving, coupon_saving], pick: highest}
The trace records which step it chose:
{"step": "saving", "value": "3.28", "chose": "member_saving"}
Your Own Step Types
When no step type and no formula can express something (splitting time at clock hours, walking a tree), the licensed engine can add a step type written in Ruby. See Custom Steps.
Next Steps
- Formulas and Functions - Operators, functions and list helpers.
- Tables and Lookups - Whole rows, effective dates and bands.
- Running over Lists - Position, carry, earlier and update.