Tables and Lookups
Keep rates, bands and lists in tables, and read them with lookups by column, by effective date and by band.
After reading this guide, you will know:
- How to write a table and read one of its rows.
- How to take a whole row and read its columns.
- How to price on a date with rows that take effect
froma date. - How to find the band a number falls in.
- How to run a calculation over every row of a table.
Tables
A table is a named list of rows under tables:. Rows are mappings with the same columns, written one per
line:
tables:
prices:
- {plan: basic, from: 2025-07-01, price: 11.00}
- {plan: basic, from: 2026-07-01, price: 12.50}
- {plan: premium, from: 2025-07-01, price: 20.00}
Tables are values. A step reads a row with lookup, a formula reads the whole table by name
(FILTER(prices, p, p.plan = plan)), a run can run over it, and an input’s one_of can take its words
from a column. Imported blueprints bring their tables with them.
The blueprint used on this page prices a membership renewal:
tables:
prices:
- {plan: basic, from: 2025-07-01, price: 11.00}
- {plan: basic, from: 2026-07-01, price: 12.50}
- {plan: premium, from: 2025-07-01, price: 20.00}
# The first row whose below is above the visits; a row without below covers everything above.
loyalty:
- {below: 10, tier: bronze, discount: 0}
- {below: 50, tier: silver, discount: 0.05}
- {tier: gold, discount: 0.10}
plans:
- {plan: basic}
- {plan: premium}
calculations:
renewal:
inputs:
plan: {type: string, one_of: {table: plans, column: plan}}
renews_on: {type: date}
visits: {type: number}
steps:
- name: price
lookup: {table: prices, where: {plan: plan}, value: price, on: renews_on}
- name: band
lookup: {table: loyalty, where: {}, band: {by: visits, below: below}}
- name: tier
formula: band.tier
- name: charge
formula: 'ROUND(price * (1 - band.discount), 2)'
outputs: [price, tier, charge]
every_plan:
inputs:
renews_on: {type: date}
visits: {type: number}
steps:
- name: quotes
run: {calculation: renewal, over: plans, update: true}
- name: cheapest
formula: 'MIN(PLUCK(quotes, charge))'
outputs: [quotes, cheapest]
$ echo '{"plan": "basic", "renews_on": "2026-07-01", "visits": 10}' | rmt run membership.yml --trace
price 12.5
tier silver
charge 11.88
How it was worked out
price 12.5 row plan basic, from 2026-07-01, price 12.5
band below 50, tier silver, discount 0.05 row below 50, tier silver, discount 0.05
tier silver band.tier
charge 11.88 ROUND(price * (1 - band.discount), 2)
A Lookup
- name: price
lookup: {table: prices, where: {plan: plan}, value: price, on: renews_on}
| Key | Means |
|---|---|
table |
a blueprint table, a list input, or an earlier step that is a list of rows |
where |
column to the name of the value it must equal. {} matches every row |
value |
the column to take. Leave it out to take the whole row |
on |
the name of a date: of the matching rows, those with the latest from on or before it |
band |
{by: name, below: column}: the row whose below is the smallest above the value |
table and where are required. The trace records the row that matched.
Whole Rows
Without value, a lookup takes the whole row, and formulas read its columns:
- name: band
lookup: {table: loyalty, where: {}, band: {by: visits, below: below}}
- name: tier
formula: band.tier
That is the way to take several columns of one row, such as the attacker’s Fighting, weapon and armour
from a fight’s line-up: lookup: {table: line_up, where: {name: attacker}}, then striker.fighting.
Rates That Change on a Date
Give each row a from date and look up on a date. The rows with the latest from on or before it win:
- {plan: basic, from: 2025-07-01, price: 11.00}
- {plan: basic, from: 2026-07-01, price: 12.50}
- name: a renewal before the July price rise pays last year's price
facts: {plan: basic, renews_on: 2026-06-30, visits: 4}
expect: {price: 11.00, tier: bronze, charge: 11.00}
- name: from the first of July the new price applies
# 12.50 less silver's 5 percent = 11.875, rounded half up to 11.88
facts: {plan: basic, renews_on: 2026-07-01, visits: 10}
expect: {price: 12.50, tier: silver, charge: 11.88}
A price that does not change needs no new row: premium’s one row from 2025 still applies in November
- The shipped
electricity_billprices every smart meter read on its own date this way, so a price change part way through a bill applies from its own day.
Bands
A banded table has rows for “less than” each limit. band: {by: visits, below: below} takes the row
whose below is the smallest one above visits. A row with no below covers everything above the
others:
| visits | tier |
|---|---|
| 0 to 9 | bronze |
| 10 to 49 | silver |
| 50 and over | gold |
The limit is exclusive: 10 visits is silver. The shipped coc_7e_investigator finds a damage bonus from
STR plus SIZ this way, and electricity_bill finds a time-of-use window from the hour a read starts.
Running over a Table
A run can run over a table as it does over a list input. With update: true each row keeps its own
columns and the calculation’s outputs are added to them:
$ echo '{"renews_on": "2026-07-01", "visits": 75}' | rmt run membership.yml --calculation every_plan
quotes
plan price tier charge
basic 12.5 gold 11.25
premium 20 gold 18
cheapest 11.25
That is how the shipped electricity bill’s compare_plans bills the same reads on every plan.
When No Row Matches
A lookup that finds no row fails the run, naming the step:
$ echo '{"plan": "basic", "renews_on": "2024-01-01", "visits": 10}' | rmt run membership.yml
✗ renewal > price: No row where {plan: "basic"}
Here there are basic rows, but none in effect yet in 2024. Make sure every value an input allows has a row, and start effective dates early enough for the oldest facts you will run.
Next Steps
- Worked Examples - Pin a table’s figures with cases.
- Running over Lists - Look up rows of a list a calculation runs over.
- Blueprint Format - Every key a lookup takes.