Grid Cookbook
Grid Cookbook
Recipes for common modeling patterns, organized by task.
Each recipe is self-contained: copy, paste, adapt. They use the
canonical style — see style-guide.md for the
underlying conventions.
Index
- Aggregations
- Filtering And Sorting
- Lookups And Joins
- Conditional Logic
- Text And Strings
- Dates And Times
- Arrays And Spilling
- Higher-Order Helpers
- External Data
- Error Handling
- Rules And Schedules
- Type Tags
- Performance Patterns
Aggregations
Sum a range, ignoring errors
A1 = SUM(IFERROR(B1:B100, 0))
# or
A1 = AGGREGATE(9, 6, B1:B100) # 9=SUM, 6=ignore errorsConditional sum
A1 = SUMIF(category, "produce", amounts)
A2 = SUMIFS(amounts, category, "produce", region, "west")Weighted average
A1 = SUMPRODUCT(weights, values) / SUM(weights)Running total
A1 = SCAN(0, values, (acc, x) => acc + x)Top N
A1 = TAKE(SORT(values, -1), 5) # top 5 descendingDistinct count
A1 = COUNTUNIQUE(items)
A2 = ROWS(UNIQUE(items))Filtering And Sorting
Filter by predicate
A1 = FILTER(data, condition)
A2 = FILTER(B1:B100, B1:B100 > 0)Filter with empty fallback
A1 = FILTER(data, condition, "no matches")Sort ascending / descending
A1 = SORT(data) # ascending
A2 = SORT(data, -1) # descending
A3 = SORTBY(data, keys) # sort by another columnSQL-style query
A1 = QUERY(data, "SELECT * WHERE col1 > 5 ORDER BY col2 DESC LIMIT 10")Pipeline filter + sort + limit
A1 = data
>> FILTER(_, _ > 0)
>> SORT(_, -1)
>> TAKE(10)Lookups And Joins
VLOOKUP / HLOOKUP / XLOOKUP
A1 = VLOOKUP("Alice", users, 2, FALSE)
A2 = HLOOKUP("Q1", quarters, 2, FALSE)
A3 = XLOOKUP("Alice", users[Name], users[Email], "n/a")INDEX / MATCH
A1 = INDEX(prices, MATCH("EUR", currencies, 0))Two-key lookup
A1 = INDEX(values, MATCH(1, (rows = "Alice") * (cols = "Q1"), 0))Lookup with explicit fallback
A1 = IFNA(VLOOKUP(name, users, 2, FALSE), "n/a")
A2 = VLOOKUP(name, users, 2, FALSE) DEFAULT "n/a"Conditional Logic
Single branch
A1 = score > 0.5 THEN "pass" ELSE "fail"Multiple tiers (chained, single line)
A1 = score >= 0.9 THEN "A" ELSE score >= 0.7 THEN "B" ELSE score >= 0.5 THEN "C" ELSE "F"For long chains, use CASE WHEN (below) which spans lines.
Match a known set
A1 = MATCH(status, "draft" -> :gray, "pending" -> :amber, "approved" -> :green, _ -> :red)For longer matches, wrap the arms across lines:
A1 = MATCH(status,
"draft" -> :gray,
"pending" -> :amber,
"approved" -> :green,
_ -> :red
)CASE WHEN (different conditions)
A1 = CASE
WHEN score >= 90 THEN "A"
WHEN score >= 80 THEN "B"
WHEN score >= 70 THEN "C"
ELSE "F"
ENDGuard with IF
A1 = IF(divisor = 0, 0, numerator / divisor)
# or
A1 = TRY numerator / divisor ELSE 0Text And Strings
Concatenate
A1 = "Hello, " & name & "!"
A2 = CONCAT("a", "b", "c")
A3 = TEXTJOIN(", ", TRUE, items)Format with placeholders
A1 = `total={B1:$#,##0.00} pct={B2:0.0%}`
A2 = TEXT(amount, "$#,##0.00")Substring / split
A1 = LEFT(name, 3)
A2 = RIGHT(name, 3)
A3 = MID(name, 2, 4)
A4 = SPLIT(csv_row, ",")Regex
A1 = REGEXMATCH("INV-2026-001", raw"^INV-\d+-\d+$")
A2 = REGEXEXTRACT("INV-2026-001", /INV-(\d+)/)
A3 = REGEXREPLACE("owner:ops", /owner:\w+/, "owner:shared")Wildcard match
A1 = WILDCARDMATCH(filename, glob"INV-*.csv")Trim and clean
A1 = TRIM(name)
A2 = LOWER(email)
A3 = PROPER("hello world")Dates And Times
Today / now
A1 = TODAY()
A2 = NOW()Construct a date
A1 as date = DATE(2026, 4, 10)
A2 as datetime = DATETIME(2026, 4, 10, 15, 30, 0)
A3 as date = d"2026-04-10"
A4 as datetime = dt"2026-04-10T15:30:00Z"Date arithmetic
A1 = TODAY() + 7 # 7 days from now
A2 = TODAY() + 1mo # 1 month from now
A3 = EOMONTH(TODAY(), 0) # end of current month
A4 = WORKDAY(TODAY(), 5) # 5 business days from todayDate difference
A1 = DAYS(end, start)
A2 = NETWORKDAYS(start, end)
A3 = YEARFRAC(start, end)Format a date
A1 = TEXT(TODAY(), "yyyy-mm-dd")
A2 = `as of {TODAY():yyyy-mm-dd}`Arrays And Spilling
Array literal
A1 = [10, 20, 30] # row, spills A1:C1
A1 = [10; 20; 30] # column, spills A1:A3
A1 = [1, 2; 3, 4] # 2x2 matrixReference a spill
B1 = SUM(A1#) # sum the entire spilled rectangle
B2 = INDEX(A1#, 2, 3) # specific cell withinSequence
A1 = SEQUENCE(10) # 1..10
A2 = SEQUENCE(10, 1, 0, 0.1) # 0.0, 0.1, 0.2, ..., 0.9
A3 = 1..10 # range shorthand
A4 = 1..2..10 # 1, 3, 5, 7, 9Constant fills
A1 = ZEROS(1000, 1) # 1000-row column of 0
A2 = ONES(3, 4) # 3x4 array of 1
A3 = FILL("TBD", 100, 1) # 100-row column of "TBD"
A4 = REPEAT(0, 1000) # column shorthand: 1000x1 of 0
A5 = REPEAT([1, 2, 3], 4) # tile a row: 4x3 of [1,2,3]FILL, ZEROS, ONES, and REPEAT produce a single array-valued
cell — one dependency-graph node — so they're the right call when
every cell would hold the same value. Use MAKEARRAY only when each
cell genuinely depends on its row/column index.
Reshape
A1 = MAKEARRAY(3, 4, LAMBDA(r, c, r * c))
A2 = TOROW(A1) # flatten to row
A3 = TOCOL(A1) # flatten to column
A4 = WRAPROWS(A1, 2) # rewrap into chunks of 2Combine
A1 = HSTACK(B1#, C1#) # side by side
A2 = VSTACK(B1#, C1#) # one below the otherHigher-Order Helpers
Map
A1 = MAP([1, 2, 3], x => x * 2)
A2 = MAP(prices, qtys, (p, q) => p * q)
A3 = SIN.([0, 1.57, 3.14]) # broadcast shorthandReduce
A1 = REDUCE(0, values, (acc, x) => acc + x)
A2 = +/ values # reduction operator shorthandScan (cumulative)
A1 = SCAN(0, values, (acc, x) => acc + x)Make array
A1 = MAKEARRAY(3, 5, LAMBDA(r, c, r * c))For most index-dependent fills, an array comprehension is more readable
and lowers to the same single-cell MAKEARRAY under the hood:
A1 = [r * c FOR r IN 1..3, c IN 1..5] # equivalent to MAKEARRAY above
A2 = [i * i FOR i IN 1..10] # 1D: 1, 4, 9, ..., 100
A3 = [r * c FOR r IN 1..5, c IN 1..5 IF r <= c] # upper-triangular, with BLANK below the diagonalReach for MAKEARRAY directly when the body needs three or more index
variables, when you want to share a heavy intermediate via LET, or when
the dimensions are computed dynamically.
Per-row / per-column
A1 = BYROW(matrix, row => SUM(row))
A2 = BYCOL(matrix, col => AVERAGE(col))External Data
Single FX rate
A1 as fx_rate = FX_RATE("EUR", "USD")
A2 as currency = ROUND(amount * (A1 DEFAULT 1.08), 2)HTTP JSON with defensive chain
A1 ~= HTTP_JSON("https://api.example.com/users/" & TEXT(user_id, ""))
A2 = WITH user = A1, email = user.email
THEN email ELSE "unavailable"ML score
A1 ~= ML_SCORE([0.12, 0.18, 0.27, 0.43])
A2 = A1 DEFAULT 0
A3 = A2 > 0.7 THEN "manual-review" ELSE "auto-approve"Retry with last cached value
B1 = FX_RATE("EUR", "USD")
# Use ?= so a transient failure doesn't blank the previous good value
last_good_rate ?= B1
display_rate = last_good_rate DEFAULT 1.08Error Handling
Cheapest fallback
A1 = source DEFAULT 0Catch all errors
A1 = IFERROR(EXPR, fallback)Catch only #N/A
A1 = IFNA(VLOOKUP(...), "n/a")Inline guard
A1 = TRY 1 / divisor ELSE 0Multi-step chain with single fallback
A1 = WITH data = HTTP_JSON(url), first = data.results[1], name = first.name
THEN name ELSE "unknown"Assert input
A1 as percentage = input ASSERT input BETWEEN 0 AND 1
A2 as currency = price ASSERT price > 0 ELSE #VALUE!Detect specific error type
status = IF(ISNA(value), "missing",
IF(ISERR(value), "error",
IF(ISBLANK(value), "empty", "ok")))Rules And Schedules
Reactive alert
WHEN load > threshold THEN
status = "alert"
alerted_at = NOW()
ENDPeriodic counter
EVERY duration"PT15M" SKIP MISSED THEN
ticks = ticks + 1
last_tick = NOW()
ENDCron schedule
EVERY cron"0 9 * * 1-5" SKIP MISSED THEN
daily_open = NOW()
ENDOne-shot at deadline
AT dt"2026-12-31T23:59:00Z" BACKFILL THEN
year_end_close = TRUE
ENDCombine reactive + scheduled
WHEN cash_runway < threshold THEN
alert = "liquidity"
END
EVERY duration"PT1H" SKIP MISSED THEN
cash_runway_log = cash_runway_log + 1
ENDType Tags
Currency conversion with tags
A1 as currency = 100000 # USD by default
A2 as fx_rate = FX_RATE("USD", "EUR")
A3 as currency = ROUND(A1 * (A2 DEFAULT 0.93), 2)Percentage display
A1 as percentage = 0.21 # displays as 21%
A2 as percentage = 21pct # equivalentBasis points
A1 as bps = 25bps # displays as 25bp
A2 = principal * A1 / 10000 # convert to a fraction for mathDate pipeline
A1 as date = TODAY()
A2 as date = A1 + 7
A3 as duration = duration"P1D"Performance Patterns
Cache an expensive call at a named cell
# Compute once, reuse many times
fx_eur_usd = FX_RATE("EUR", "USD") DEFAULT 1.08
A1 as currency = amount_a * fx_eur_usd
A2 as currency = amount_b * fx_eur_usd
A3 as currency = amount_c * fx_eur_usdUse ~= for cells that are rarely read
A1 ~= LARGE_MATRIX_INVERT(huge_matrix)Avoid range broadcasts that explode dependencies
A1:A1000 = 0 parses, but the parser expands it into one thousand
individual A1 = 0, A2 = 0, ..., A1000 = 0 cells — each with its
own dependency-graph node. For constant fills, prefer a single
array-valued cell that spills:
# Bad: 1000 individual cells
A1:A1000 = 0
# Good: one array cell, one graph node
A1 = ZEROS(1000, 1) # column of zeros
A1 = ONES(1000, 1) # column of ones
A1 = FILL("TBD", 1000, 1) # column of any constant
A1 = REPEAT(0, 1000) # equivalent shorthand for column of zerosWhen each cell depends on its row/column index, prefer an array
comprehension — it desugars to a single MAKEARRAY cell and reads more
naturally:
A1 = [r * c FOR r IN 1..1000, c IN 1..10]Use MAKEARRAY directly when you need three or more index variables, or
when the dimensions are computed dynamically. For constant fills the
helpers above (ZEROS/ONES/FILL/REPEAT) are clearer and avoid the
lambda machinery.
Hoist common subexpressions with DO
A1 = DO
base = SUM(B1:B100)
base * 1.1 + base * 0.05
ENDUse spill references instead of repeating ranges
# Bad
A1 = SUM(B1:B100)
A2 = AVERAGE(B1:B100)
A3 = MAX(B1:B100)
# Better
data = B1:B100
A1 = SUM(data)
A2 = AVERAGE(data)
A3 = MAX(data)See Also
reference.md— full language reference.functions.md— every built-in function.style-guide.md— canonical authoring style.ai-agent-guide.md— strict AI rules.examples/canonical/— seven worked end-to-end models.