Grid Style Guide
Grid Style Guide
This is the canonical authoring style for Grid models. The grammar accepts more forms than this guide teaches; the canonical style picks one preferred way to spell each idea so models stay consistent across authors and tools.
Use it for shared models, documentation examples, and AI-generated source (see
ai-agent-guide.md). It is recommended for any model
that another person will review or maintain.
1. Why Have A Style Guide
Grid intentionally accepts a wide range of equivalent forms. That flexibility is useful for migration, exploration, and parser coverage, but it makes models feel inconsistent when reviewed across teams.
The canonical profile makes models:
- Readable — one preferred spelling per idea.
- Predictable — review and refactor without ambiguity.
- Portable — preferred forms avoid legacy or ambiguous spellings.
- Diff-friendly — small changes produce small diffs.
- Test-friendly — validators and parsers exercise the same surface.
If your team needs a different convention, fork the guide locally — just pick one rule per idea and stick to it.
2. The Canonical Profile (Cheat Sheet)
| Idea | Prefer | Avoid |
|---|---|---|
| Conditional | THEN ... ELSE |
? : |
| Null fallback | DEFAULT |
?? |
| Local bindings | DO ... END |
top-level LET(...) |
| Multi-branch (same subject) | MATCH(subject, ...) |
repeated IF(=, ...) |
| Multi-branch (different conditions) | THEN ... ELSE chained |
CASE WHEN (acceptable but not preferred) |
| Lambdas | (x) => ... named arrows |
LAMBDA(x, ...) (acceptable) |
| Higher-order helpers | MAP, REDUCE, SCAN, MAKEARRAY, BYROW, BYCOL |
hand-rolled loops |
| Async value | ~= for lazy / one-shot, = for eager |
mixing both for the same value |
| Error guard | DEFAULT then IFERROR, TRY |
nested IF(ISERROR(...)) |
| Type tag | Use on outputs and crossing-boundary values | Tag every intermediate |
| Strings | "double quotes" |
'single quotes' |
| Section comments | # Heading once per section |
One comment per line |
| Rules | WHEN ... THEN ... END blocks |
assignment-level schedule modifiers |
3. Model Shape
Every shared model follows this shape:
MODEL "Name"
DESCRIPTION "What this model does."
VERSION "x.y.z"
AUTHOR "owner"
TAGS "tag1", "tag2"
# Inputs
A1 IS type = ...
# Derived calculations
B1 IS type = ...
# Final outputs
C1 IS type = ...
# Rules
WHEN ... THEN ... END
END MODELThe pattern is: header → inputs → derivations → outputs → rules → END.
3.1 Engine Directives
Omit RUNTIME in new models. Grid runs .grid source the same way regardless
of the header.
Older models may carry a RUNTIME directive — Grid still parses it for
backwards compatibility, but new headers should not include one.
3.2 Sectioning
Group statements into clear sections with a single # heading:
# Inputs
A1 IS currency = 100000
A2 IS percentage = 7pct
# Derivations
B1 IS currency = A1 * (1 - A2)
# Outputs
C1 = B1 > 50000 THEN "ok" ELSE "small"Don't comment every line. Section headings carry the structural narrative.
4. Naming
4.1 Spatial And Named Bindings
A1 notation is the default. Add semantic aliases when the domain name becomes useful, and use named calculations when the name denotes a new derivation:
A1 = SUM(B1:B12)
revenue = A1 # alias: same binding, another address
tax = revenue * 0.21 # named calculation: a new bindingDo not add aliases merely to rename single-use intermediates. A3 is fine.
4.2 Lambda Parameters
Use parameter names that reveal meaning:
MAP(units, prices, (units, price) => ROUND(units * price, 2)) # good
MAP(units, prices, (a, b) => ROUND(a * b, 2)) # weak
MAP(units, prices, _1 * _2) # placeholder OK for trivial casesPlaceholder lambdas (_, _1, _2) are reserved for short
higher-order operations and pipe steps where the operation is
self-evident.
Use a named trailing block when the callback needs local bindings or more than one readable line:
MAP(rows) WITH row DO
amount = row.amount DEFAULT 0
ROUND(amount * exchange_rate, 2)
ENDKeep arrow lambdas for a single clear expression. A trailing block's WITH
names callback arguments; a leading WITH value DO ... remains whole-value
pipeline syntax.
4.3 Symbol Literals
Use :identifier for enum-like state labels. Derive them with CASE WHEN when each branch tests a condition:
status = CASE
WHEN score > 0.9 THEN :excellent
WHEN score > 0.7 THEN :good
ELSE :poor
ENDand with MATCH when every arm compares the same subject against a
value (MATCH arms are patterns, not conditions — see §8.3 of the
reference). MATCH is a function call, so its arms may wrap across
lines if the single-line form gets too long:
color = MATCH(status,
:excellent -> :green,
:good -> :amber,
_ -> :red
)Symbol values display nicely, sort consistently, and signal "this is a state, not free text".
5. Assignments
5.1 Use = By Default
A1 = 10 # eager
A2 IS currency = A1 * 100 # eager + type tagWrite the type-tag keyword uppercase (IS). Lowercase is parses — all
keywords are case-insensitive — but IS is what the canonical formatter
emits.
5.2 Use ~= For Lazy / Expensive
A3 ~= ML_SCORE(features) # lazy external
A4 ~= LARGE_MATRIX_INVERT(M) # lazy expensive5.3 Use ?= Sparingly
?= preserves a previous value when the RHS errors. Use it when an
input might be temporarily unavailable but you want to keep the last
known good value:
A5 = 0
A5 ?= MAYBE_FAIL_FETCH()Don't use ?= as a substitute for proper error handling
(DEFAULT, IFERROR, WITH).
5.4 Use Compound Assignments For Counters
WHEN payment_received THEN
counter += 1
total += amount
ENDUse arithmetic compound assignments only inside rule actions, where Grid has a
committed prior value. Most bindings are declarative and should use =.
5.5 Tag Outputs, Not Every Intermediate
A1 IS currency = 100000 # input — tag
B1 IS currency = A1 * 1.05 # output — tag
B2 = B1 + 100 # intermediate — usually no tagTag inputs (so callers know the shape), tag outputs (so consumers format correctly), and skip tags on internal intermediates unless they carry meaning.
6. Branching
6.1 Single Condition: THEN ... ELSE
status = score > 0.7 THEN "high" ELSE "low"6.2 Multiple Tiers: Chain THEN ... ELSE
Most-specific to least-specific:
grade = score >= 90 THEN "A" ELSE score >= 80 THEN "B" ELSE score >= 70 THEN "C" ELSE "F"A chained THEN ... ELSE may wrap at each THEN/ELSE — put each arm
on its own line, ELSE-first, when it gets long:
grade = score >= 90 THEN "A"
ELSE score >= 80 THEN "B"
ELSE score >= 70 THEN "C"
ELSE "F"CASE WHEN is still fine for long condition ladders; prefer whichever
reads more clearly.
6.3 Same Subject, Many Values: MATCH
color = MATCH(status, "draft" -> :gray, "pending" -> :amber, "approved" -> :green, _ -> :red)Use MATCH whenever the branches all compare against the same
subject. The _ arm is required if you don't enumerate every value
and want a default. MATCH arms may also wrap across lines as a
table — see §4.3 above.
6.4 Multi-Condition Tabular Form: CASE WHEN (Acceptable)
When several distinct conditions need separate clauses, CASE WHEN
reads well as a table:
grade = CASE
WHEN score >= 90 THEN "A"
WHEN score >= 80 THEN "B"
WHEN score >= 70 THEN "C"
ELSE "F"
ENDBut chained THEN ... ELSE is also fine, and is the canonical pick
for short chains.
7. Local Bindings
Use DO ... END to name intermediate values:
C1 IS currency = DO
revenue = SUM(A1:A12)
cost = SUM(B1:B12)
revenue - cost
ENDPrefer DO over top-level LET(...):
# Avoid
C1 = LET(revenue, SUM(A1:A12), cost, SUM(B1:B12), revenue - cost)DO is what LET desugars to. Authoring with DO keeps the visual
structure of "name some things, then return one thing".
8. Pipes
Use pipes when a value moves through a short, obvious left-to-right chain:
result = raw_value >> ABS() >> ROUND(2)
filtered = data >> FILTER(value => value > 0) >> SORT(_) >> TAKE(10)For an elementwise FILTER step, always bind the element with a named lambda.
Reserve _ in a pipe call for the complete piped value, as in SORT(_, -1).
Do not write the legacy FILTER(_, _ > 0) form in new models; it is accepted
only for source compatibility.
Avoid pipes when:
- The chain is one step (just call the function directly).
- The chain has nested branching (use
DO). - The placeholder slot needs to appear in unusual positions (use a named lambda).
8.1 Choosing A Relational Form
Choose syntax by the operation, not the data carrier:
- use
PICK/OMIT/RENAMEfor simple key shaping; - use spreadsheet array helpers for already-array-shaped masks and keys;
- use a short pipe for obvious left-to-right transformations;
- use a table method chain for one concise scalar aggregate;
- use
SELECTfor row predicates combined with computed projection, grouping, joins, sets, windows, or subqueries.
Equivalent forms share one logical meaning and may use the same engine. The
full decision guide and important non-equivalences are in
relational-authoring.md.
9. Higher-Order Helpers
A3 = MAP(A1#, A2#, (units, price) => ROUND(units * price, 2))
A4 = SCAN(0, A3#, (acc, value) => ROUND(acc + value, 2))
A5 = REDUCE(0, A3#, (acc, value) => ROUND(acc + value, 2))
B1 = BYROW(matrix, row => SUM(row))
B2 = BYCOL(matrix, col => AVERAGE(col))
B3 = MAKEARRAY(rows, cols, LAMBDA(r, c, INDEX(matrix, r, c) * scale))Style points:
- Name lambda parameters meaningfully.
- One arrow lambda per helper — don't nest deep lambdas inside lambdas.
- For simple elementwise operations, prefer
SIN.(arr)broadcast overMAP(arr, SIN(_)).
10. Error Handling
10.1 Default Order Of Preference
DEFAULTfor blank/error fallback to a value.IFERROR/IFNAfor explicit error handling.TRY ... ELSEinline form for one-off guards.WITH ... THEN ... ELSEfor multi-step external chains.ASSERTfor invariant checking on inputs.
10.2 Examples
# Cheap fallback
B1 IS currency = ROUND(amount * (rate DEFAULT 1.08), 2)
# Explicit error handling
B2 = IFERROR(VLOOKUP(...), "n/a")
# Inline guard
B3 = TRY 1 / divisor ELSE 0
# Multi-step chain
B4 = WITH data = HTTP_JSON(url), first = data.results[1]
THEN first.name ELSE "unknown"
# Input invariant
B5 IS percentage = input ASSERT input BETWEEN 0 AND 1Avoid nesting IF(ISERROR(...)) chains — they're harder to read than
the canonical alternatives.
11. External Functions
Pair every external call with a DEFAULT:
fx_rate = FX_RATE("EUR", "USD")
amount_usd IS currency = ROUND(amount_eur * (fx_rate DEFAULT 1.08), 2)Default to eager = when the model's outputs need the value as soon as the
model runs (as above). Use lazy ~= only when the call is expensive or
rarely read — the work then starts on first read, not at model load (see
external-functions.md §4):
nightly_score ~= ML_SCORE(features)Don't put external calls inside frequently-recomputed expressions unless you want every recompute to enqueue a job. Cache them at a named binding.
12. Rules
When a model needs reactive behavior, use full rule blocks rather than schedule modifiers:
# Good
WHEN A1 > 100 THEN
C1 = "alert"
C2 = NOW()
END
EVERY duration"PT15M" SKIP MISSED THEN
D1 = D1 + 1
END
AT dt"2026-12-31T23:59:00Z" BACKFILL THEN
E1 = TRUE
END# Avoid in shared models
G1 = NOW() EVERY cron"0 * * * *" BACKFILLSchedule modifiers are convenience sugar. They're fine for one-off scripts but blur the line between formula and rule in larger models.
Always include a missed-run policy (SKIP MISSED or BACKFILL).
13. Type Tags
13.1 Use Tags For
- Inputs that need validation.
- Outputs that need formatting (currency, percentage, date).
- Values crossing model boundaries.
- Anything where semantic meaning matters more than brevity.
13.2 Don't Use Tags For
- Trivial scratch intermediates.
- Boolean flags.
- Counters.
13.3 Common Tags
| Tag | When to use |
|---|---|
currency |
Money amounts |
percentage |
Rates expressed 0..1 or 0..100 |
bps |
Basis points |
score |
ML or rating scores |
date / datetime |
Calendar values |
duration |
Time spans |
currency:USD etc. |
Specific currency codes (bare USD works too) |
14. Comments
# Section heading
A1 = 100 # Inline comment ONLY when the line is non-obvious
/* Block comment for a multi-paragraph
explanation that wouldn't fit on one
line. */Avoid:
- One comment per line restating the formula (
# multiply by tax rate). - Block comments wider than 80 columns.
- Decorative comments (banners, ASCII art, separators).
15. Spacing And Layout
- One blank line between sections.
- Keep tightly related lines together. In longer
DOblocks and rule bodies, use blank lines to separate conceptual steps. - Use spaces around operators:
A1 + A2, notA1+A2.
15.1 When To Wrap Across Lines
Grid lets any expression inside (...), [...], or {...} span
multiple lines (see reference.md §14.1).
Use that freely when it makes the formula easier to scan — not for
its own sake.
Wrap when:
- A function call has more than ~3 meaningful arguments, or any argument is itself a non-trivial expression.
- A
MATCHhas more than ~3 arms, or the patterns/results are wide enough that side-by-side reading suffers. - An object literal has more than ~3 fields.
- A pipe (
>>) chain has more than ~2 stages. - A boolean predicate or arithmetic expression mixes operators of different precedence and benefits from one term per line.
Keep it on one line when it already fits and reads cleanly — most two- and three-argument calls don't need wrapping.
15.2 Wrapping Conventions
When you do wrap, follow these conventions so models stay legible across a team:
# Function calls: one argument per line, two-space indent, closing
# paren on its own line.
A1 = MATCH(status,
"draft" -> :gray,
"pending" -> :amber,
"approved" -> :green,
_ -> :red
)
# Object literals: one field per line, trailing comma optional,
# closing brace on its own line at the parent indent.
A2 = {
name: "Acme",
founded: 1999,
active: TRUE
}
# Comprehensions: keep the head expression on the first line, then
# put each FOR / IF clause on its own line.
A3 = [
row.revenue
FOR row IN sales
IF row.region = "NA"
]
# Pipes: wrap the whole chain in parens, with the leading >> at the
# start of each continuation line.
A4 = (data
>> FILTER(value => value > 0)
>> SORT(_)
>> TAKE(10))
# Long predicates: parens + leading operator per line.
A5 = (
is_active
AND tier = :gold
AND balance > 0
)Align -> arrows in MATCH arms when the patterns are short and
similar-shaped (it makes the table effect work). Don't force
alignment when patterns vary widely — uneven padding is worse than
no padding.
16. What's Out Of Style
These forms work but are not canonical for shared models:
| Form | Why avoid in shared models |
|---|---|
LET(...) at top level |
Use DO ... END |
? : ternary |
Use THEN ... ELSE |
?? |
Use DEFAULT |
'single quotes' for strings |
Use "double"; single is for quoted namespace specifiers |
| Schedule modifier on a single binding | Use a full rule block |
Nested IF(ISERROR(...)) chains |
Use IFERROR, WITH, or DEFAULT |
| Untyped published outputs | Add an IS <tag> annotation |
| Heavy placeholder use in long expressions | Use named arrow lambdas |
The parser still accepts all of these. They're just not preferred for shared, reviewed, or AI-generated code.
17. Worked Comparison
A model written in two styles:
17.1 Loose Style (Acceptable, Not Canonical)
A1 = 100
B1 = A1 ?? 0
C1 = LET(t, A1 * 1.21, t)
D1 = A1 > 50 ? "big" : "small"
E1 = IF(ISERROR(FX_RATE("EUR","USD")), 1.08, FX_RATE("EUR","USD"))
F1 = NOW() EVERY cron"0 * * * *" SKIP MISSED17.2 Canonical Style
MODEL "Example"
DESCRIPTION "Show the canonical style."
VERSION "1.0.0"
AUTHOR "owner"
TAGS "example", "style"
# Inputs
A1 IS currency = 100
# Derivations
B1 IS currency = A1 DEFAULT 0
C1 IS currency = DO
total = A1 * 1.21
total
END
D1 = A1 > 50 THEN "big" ELSE "small"
E1 IS fx_rate = FX_RATE("EUR", "USD") DEFAULT 1.08
# Periodic refresh
EVERY cron"0 * * * *" SKIP MISSED THEN
F1 = NOW()
END
END MODELThe canonical version is longer but it's structurally clear and tooling-friendly.
18. Linting And Validation
Grid diagnostics catch syntax and type issues. This guide covers style: names, layout, fallbacks, sectioning, and idioms that make a valid model easier to maintain.
19. See Also
reference.md— full language reference.relational-authoring.md— canonical choices among SQL, projection sugar, methods, array helpers, and pipes.assignments.md— assignment shapes in detail.rules-and-schedules.md— rule blocks.ai-agent-guide.md— strict AI generation rules.cookbook.md— worked recipes.
Embedded surface content
Use explicit language blocks for multiline surface content. Indent the payload by two spaces from the containing statement; align the closing delimiter with that statement. Preserve the embedded language’s relative indentation.
# Pricing component
Panel_1!type = "component"
Panel_1!config = <toml>
version = 1
kind = "component"
# Display
title = "Pricing"
</toml>
# Read the live model value when rendering the panel.
Panel_1!source = <jsx>
function Panel() {
const price = useCell("Price");
return <div>{price}</div>;
}
render(<Panel />);
</jsx>Use <yaml> or <json> when those encodings suit the configuration. Use
<html> for verbatim HTML and <jsx> for component source. Short typed quotes
and older quoted configurations remain supported. When teaching a multiline
quoted form, apply the same layout principles wherever whitespace is cosmetic.
Store structured document and notebook data in embedded JSON. The block evaluates to a structured value; a quoted JSON string evaluates to text. Native surface readers accept both forms for compatibility, while new authored examples use the structured form.
Review!data = <json>
{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Review the forecast assumptions." }
]
}
]
}
</json>Separate identity, bindings, layout, and behavior into readable sections when present. Add short comments that explain purpose or constraints; do not narrate every assignment. Use each embedded language’s comment syntax; JSON has no comments, so explain it immediately outside the block. Tiny examples do not need artificial sections.
Whitespace can be data. Do not mechanically indent TOML multiline strings, YAML scalar content, HTML whitespace, JSX text, template literals, CSV, or TSV. For existing payloads, preserve their bytes unless an encoding-aware operation establishes equivalent values. Surface data generation adds a two-space margin only when parsing before and after produces the same value. Markup and source editing preserve authored whitespace. The formatter preserves authored blocks; converting safe multiline data uses the preferred layout where supported. Quoted markup with Grid interpolation keeps its quoted form because tag blocks are verbatim.
These standards apply equally to public docs, canonical examples, templates, surface generators, and AI-generated models. Formatter checks handle mechanical layout; authors still review names, section boundaries, useful comments, and whether a reader can understand the example without reconstructing its intent.