Grid AI Agent Authoring Guide
Grid AI Agent Authoring Guide
This document is a strict contract for AI agents (LLMs, code assistants, codegen pipelines) generating Grid model source.
It is more directive than the human-facing
style-guide.md because LLMs benefit from
unambiguous rules and concrete examples.
Use this as system context. Feed this whole file (or
grammar.json) into your agent's system prompt when generating Grid code. The grammar profile is small enough to include in every request.
1. The 12 Hard Rules
These rules are non-negotiable. Violations produce models that don't parse, don't deploy, or behave incorrectly at runtime.
- Every line is exactly one statement. No bare expressions. No "implicit" assignments. Every assignment has a target and an operator.
- Strings use double quotes only. Single quotes are reserved for sheet/workbook qualifiers.
- Range targets must be finite rectangles.
A1:A10is OK.A:Ais not. - Inside rule bodies (
WHEN/EVERY/AT), use=,?=, or the compound assignments (+=,-=,*=,/=). Lazy~=and nested rule blocks are still rejected. No metadata. - Rule blocks run on every runtime.
lua_generated,lua_interpreter, andtsall supportWHEN/EVERY/ATblocks and<expr> EVERY/ATmodifiers. The TS GridService orchestrates rule waves and schedules wakeups for the Lua runtimes, so behavior matchestsexactly. If unspecified,RUNTIME "lua_generated"is the recommended default; switch totsonly when you need spatial references. - Every
EVERYandATmust have a missed-run policy (SKIP MISSEDorBACKFILL). Pick one explicitly. - Function names are case-insensitive at parse, but emit them
uppercase.
SUM, notsum. BLANKis not"". UseBLANKto mean absence; use""to mean an empty string.- Errors propagate. Any function receiving an error returns that
error unchanged unless it's an explicit error-handling function
(
IFERROR,ISERROR,TRY, etc.). - External functions return asynchronously. Pair every
FX_RATE/HTTP_JSON/ML_SCOREwith aDEFAULTso downstream cells stay computable. - Don't invent functions. Use only names in
functions.md. If you need behavior outside the catalog, compose it from existing functions or stop and ask. - Don't use keywords as identifiers. Grid's grammar is
contextual (no truly reserved words), but keyword identifiers
create ambiguous parses and break with grammar extensions. The
"effectively reserved" list is in
reference.md. Pick clearer names likerevenue,total_cost,is_active. - Newlines fold inside brackets, separate statements outside.
Any expression inside an unclosed
(...),[...], or{...}may span multiple lines — that includes function calls,MATCH(...), parenthesized expressions, array and object literals, and comprehensions. Block forms (DO,CASE WHEN,WHEN/EVERY/AT,WITHbindings) also span lines. The two things that still need a single line are chainedTHEN ... ELSEladders and theTHEN <expr> ELSE <fallback>tail of aWITH. If a chainedTHEN ... ELSEgets too long, switch toCASE WHENor extract intermediates withDO. Seereference.md §14.1. - External functions cannot appear in rule action bodies.
FX_RATE/HTTP_JSON/ML_SCOREmust be called at the top level, not insideWHEN/EVERY/ATaction bodies. Rely on the function's cache TTL for refresh.
2. Standard Output Template
When asked to produce a Grid model, emit this template:
MODEL "<short title>"
DESCRIPTION "<one sentence>"
RUNTIME "<lua_generated|ts>"
VERSION "1.0.0"
AUTHOR "<author or 'AI Agent'>"
TAGS "<comma-separated tags>"
# Inputs
<input cells>
# Derivations
<derived cells>
# Outputs
<output cells>
# Rules (any runtime)
<rule blocks>
END MODELFill in every header field. Order the cells from inputs → derivations
→ outputs. Use # Section comments to group.
3. The Decision Tree
When generating each cell, decide in this order:
Is this an input the user provides?
YES → A* assignment with type tag, no formula
NO → continue
Does it depend on an external API or expensive computation?
YES → use ~= and pair downstream cells with DEFAULT
NO → use =
Does it have a meaningful semantic type (currency, percentage, etc.)?
YES → add `as <tag>`
NO → no tag
Does it branch on conditions?
All branches compare the same subject? → MATCH
Different conditions per branch? → CASE WHEN or chained THEN ELSE
Single condition? → THEN ... ELSE
Does it have multiple intermediate values?
YES → DO ... END
NO → inline expression
Could it produce an error?
YES → DEFAULT, IFERROR, or WITH ... ELSE
NO → leave bare4. Always Do / Never Do
Do
- Emit exactly one assignment per statement.
- Use spaces around operators:
A1 = SUM(B1:B5). - Use
"double quotes"for strings. - Use
raw"..."when backslashes should not be processed. - Use
glob"*.csv"orwild"???-*.csv"for wildcard patterns. - Use
:identifiersymbol literals for state labels. - Use
json"""..."""/yaml"""..."""/csv"""..."""for embedded structured values. - Use
A1#to refer to a spilled array as a single value. - Use
as <tag>for outputs and crossing-boundary values. - Use
MATCH(subject, val -> result, _ -> default)for value-based matching. - Use
CASE WHENfor distinct-condition branching. - Use
WITH ... THEN ... ELSEfor error-tolerant external chains. - Use
ASSERT value > 0 ELSE #N/Ainstead of nestedIF(cond, val, #N/A). - Use
DEFAULTrather than??(both work;DEFAULTis canonical). - Use
DO ... ENDrather than top-levelLET(...). - Use placeholder lambdas (
_,_1,_2) only inside higher-order helpers and pipes. - Use
+/,*/,&/reduction operators when they're clearer thanSUM,PRODUCT,CONCAT. - Use
data[^1]ordata[-1]for last-element access instead ofINDEX(data, ROWS(data)). - Use
[2:5]slice notation instead ofTAKE(DROP(data, 1), 4). - Use
UNION/INTERSECT/EXCEPTfor set operations. - Use
QUERY(data, "SELECT ... WHERE ... ORDER BY ... LIMIT ...")for table queries.
Never
- Emit a bare expression without a target (
SUM(A1:B5)on its own). - Use
'single quotes'for strings (those are sheet qualifiers). - Use full-column / full-row range targets (
A:A = 0). - Use
~=,?=,+=inside a rule action body. - Nest rule blocks (
WHEN ... WHEN ... END END). - Define a target with both a top-level assignment and a rule action.
- Emit
WHEN/EVERY/ATblocks underRUNTIME "lua_generated". - Omit
SKIP MISSED/BACKFILLfromEVERY/ATblocks. - Use placeholders standalone outside higher-order helpers and pipes
(
A1 = _ * 2is invalid). - Invent function names. If you don't recognize a name, don't emit it.
- Use reserved words as identifiers (
MODEL,WHEN,THEN,END, etc. — seereference.md). - Treat
BLANKand""as equivalent. - Treat external function failures as cell errors without a fallback —
always pair with
DEFAULT. - Wrap a single function in
LAMBDAwhen an arrow lambda will do. - Generate models with no header.
5. Concrete Examples By Task
5.1 "Compute total revenue from monthly figures"
MODEL "Monthly Revenue"
DESCRIPTION "Sum monthly revenue and compute average."
RUNTIME "lua_generated"
VERSION "1.0.0"
AUTHOR "AI Agent"
TAGS "demo"
# Inputs
A1 as currency = 12000
A2 as currency = 13500
A3 as currency = 14200
A4 as currency = 15000
A5 as currency = 16800
# Derivations
B1 as currency = SUM(A1:A5)
B2 as currency = ROUND(B1 / 5, 2)
# Outputs
C1 = `total={B1} avg={B2}`
END MODEL5.2 "Classify customer scores into tiers"
MODEL "Score Tiers"
DESCRIPTION "Bucket scores into excellent/good/fair/poor."
RUNTIME "lua_generated"
VERSION "1.0.0"
AUTHOR "AI Agent"
# Inputs
A1 as score = 0.85
# Tier classification (CASE WHEN reads well as a table when each arm tests a different condition)
B1 = CASE
WHEN A1 >= 0.9 THEN :excellent
WHEN A1 >= 0.7 THEN :good
WHEN A1 >= 0.5 THEN :fair
ELSE :poor
END
# Output
C1 = `score={A1} tier={B1}`
END MODEL5.3 "Convert with an FX rate"
External functions cannot appear inside rule action bodies. The
runtime does not automatically refresh external values on a
schedule (cache policy is metadata-only in v1). An external is
recomputed when an upstream input changes or when an explicit
resolve arrives.
Just call the function at the top level and pair with a DEFAULT:
MODEL "FX Conversion"
DESCRIPTION "Convert a notional amount with a default fallback rate."
RUNTIME "lua_generated"
VERSION "1.0.0"
AUTHOR "AI Agent"
# Inputs
A1 as currency = 100000
# External signal (computed at deploy; recomputed when A1 changes)
B1 as fx_rate = FX_RATE("EUR", "USD") DEFAULT 1.08
# Derivation
C1 as currency = ROUND(A1 * B1, 2)
END MODELIf you need explicit periodic refresh today, drive it from outside
the engine (cron job, webhook) by writing to an input cell that
B1's formula depends on.
5.4 "Alert on a threshold and time-stamp it"
MODEL "Threshold Alert"
DESCRIPTION "Page ops when load exceeds a threshold."
RUNTIME "lua_generated"
VERSION "1.0.0"
AUTHOR "AI Agent"
# Inputs
A1 = 0 # load
A2 = 100 # threshold
# Reactive rule
WHEN A1 > A2 THEN
B1 = "ops-paged"
B2 = NOW()
END
END MODEL5.5 "Filter and aggregate a list"
MODEL "List Aggregation"
DESCRIPTION "Filter positive values, sort descending, take top 5."
RUNTIME "lua_generated"
VERSION "1.0.0"
AUTHOR "AI Agent"
# Input array
A1 = [-3, 7, 1, -2, 9, 4, 8, -1, 5, 6]
# Pipeline
B1 = A1
>> FILTER(_, _ > 0)
>> SORT(_, -1)
>> TAKE(5)
# Aggregations
C1 = SUM(B1#)
C2 = AVERAGE(B1#)
END MODEL5.6 "Defensive parse of an external JSON response"
MODEL "External Lookup"
DESCRIPTION "Fetch a user record by id; tolerate failure."
RUNTIME "lua_generated"
VERSION "1.0.0"
AUTHOR "AI Agent"
# Inputs
A1 = 42
# Lazy external call
B1 ~= HTTP_JSON("https://api.example.com/users/" & TEXT(A1, ""))
# Defensive chain
C1 = WITH user = B1, name = user.name, email = user.email
THEN `name={name} email={email}` ELSE "unavailable"
END MODEL5.7 "Matrix scenario with broadcasting"
MODEL "Scenario Matrix"
DESCRIPTION "Apply bear/base/bull multipliers to a monthly revenue series."
RUNTIME "lua_generated"
VERSION "1.0.0"
AUTHOR "AI Agent"
# Base series
A1 = [120, 135, 142, 150, 168]
# Scenario multipliers (column vector)
B1 = [0.92; 1.00; 1.08]
# 3x5 scenario matrix (an array comprehension; lowers to MAKEARRAY)
C1 = [INDEX(A1, m) * INDEX(B1, s) FOR s IN 1..3, m IN 1..5]
# Per-scenario totals and per-month averages
D1 = BYROW(C1, row => SUM(row))
D2 = BYCOL(C1, col => AVERAGE(col))
END MODEL6. Common Mistakes And Fixes
| Mistake | Fix |
|---|---|
A1 10 |
A1 = 10 |
A1 = 'hello' |
A1 = "hello" |
A1 = SUM(B1:B5 |
A1 = SUM(B1:B5) |
A1 = _ * 2 |
A1 = MAP(B1:B3, _ * 2) or A1 = LAMBDA(x, x * 2) |
EVERY duration"PT5M" THEN ... END |
EVERY duration"PT5M" SKIP MISSED THEN ... END |
WHEN ... THEN B1 ~= ... END |
WHEN ... THEN B1 = ... END (no ~= in rule body) |
RIGHT(B1) or LEFT(B1) parsed as a spatial reference |
The grammar reserves RIGHT(...) and LEFT(...) for the built-in text functions; bare LEFT / RIGHT keywords (no parens) read the adjacent cell. Use LEFT(B1, 3) for the text function or LEFT / RIGHT (no parens) for the spatial reference. |
A:A = 0 |
Use A1 = ZEROS(N, 1) (or FILL(value, rows, cols) / REPEAT(value, count)) — one array cell, not N |
A1:A1000 = 0 |
A1 = ZEROS(1000, 1) — one dependency-graph node, not 1000 |
MAKEARRAY(rows, cols, LAMBDA(r, c, expr)) for index-dependent fills |
[expr FOR r IN 1..rows, c IN 1..cols] (comprehension lowers to a single MAKEARRAY cell, with cleaner syntax) |
B1 = "default"; WHEN ... THEN B1 = "active" END |
Pick one ownership; use B1 = cond THEN "active" ELSE "default" |
A1 = ADD(5, _) standalone |
Use A1 = ADD(5, B1) or wrap as A1 = LAMBDA(x, ADD(5, x)) if you genuinely want a function value |
IF(ISERROR(EXPR), fallback, EXPR) |
IFERROR(EXPR, fallback) |
IF(ISBLANK(A1), 0, A1) |
A1 DEFAULT 0 |
A1 = WITH x = 5 x + 1 |
A1 = WITH x = 5 THEN x + 1 |
CASE WHEN x > 0 THEN 1 (missing END) |
CASE WHEN x > 0 THEN 1 END |
A1 as currency = "hello" |
A1 as currency = 100 (string can't be currency) |
Multi-line THEN A ELSE\n B THEN C ELSE D |
Single-line, or use CASE WHEN (chained THEN ... ELSE is not bracketed) |
WITH x = 1, y = 2 THEN x + y\nELSE 0 (ELSE on next line) |
All of THEN ... ELSE ... on one line |
7. The Type System In One Page
Every value has a kind and a type tag.
| Kind | Examples |
|---|---|
blank |
BLANK |
number |
42, 3.14, 0xFF, 21pct, 25bps |
string |
"hello", :label |
boolean |
TRUE, FALSE |
date |
d"2026-04-10", dt"2026-04-10T15:30:00Z" |
array |
[1, 2, 3], [1; 2; 3], [1, 2; 3, 4] |
object |
json"""{...}""", regex literals |
error |
#N/A, #DIV/0!, etc. |
Type tags add semantic meaning to a kind:
| Tag | Kind | Meaning |
|---|---|---|
currency |
number |
money |
percentage / pct |
number |
percent |
bps |
number |
basis points |
score |
number |
0..1 typical |
fx_rate |
number |
exchange rate |
unit:USD |
number |
typed currency code |
date / datetime |
date |
calendar |
duration |
string |
ISO duration |
cron |
string |
cron expression |
symbol |
string |
enum label |
Apply tags at assignment:
A1 as currency = 100000
B1 as percentage = 21pct
C1 as date = d"2026-04-10"The runtime validates compatibility. If you tag as currency = "hello",
the cell becomes #TYPE!.
8. The Error Codes In One Page
Source-writable literals (9):
| Code | Literal | Cause | Recover |
|---|---|---|---|
N/A |
#N/A |
Lookup miss; explicit "no value" | IFNA, DEFAULT |
DIV/0 |
#DIV/0! |
Divide by zero | IFERROR, guard |
VALUE |
#VALUE! |
Wrong arg shape/type | IFERROR, WITH |
REF |
#REF! |
Invalid reference | structural fix |
NAME |
#NAME? |
Unknown function/argument | check spelling |
NULL |
#NULL! |
Empty range intersection | range syntax |
NUM |
#NUM! |
Numeric domain error | input validation |
CALC |
#CALC! |
Iterative method failure | tune tolerances |
SPILL |
#SPILL! |
Spill blocked | move formula |
Runtime-only (do not write as literals — they're generated):
| Code | Cause |
|---|---|
TYPE |
Type-tag mismatch on assignment |
CIRC |
Circular reference detected at build |
Both can be detected with ISERROR(value) or value IS ERROR.
9. Operator Precedence Cheat Sheet
For the full table see reference.md.
The five most common decisions:
- Power (
^) is right-associative.2^3^2 = 2^(3^2) = 512. &binds tighter than comparison."a" & "b" = "ab"parses as expected.- Comparison is non-associative.
0 < x < 10is not valid. Usex BETWEEN 0 AND 10or0 < x AND x < 10. THEN ... ELSEis lower than logical operators.A AND B THEN x ELSE yreads as(A AND B) THEN x ELSE y.- Pipes are below all conditional and logical operators.
cond THEN x ELSE y >> freads as(cond THEN x ELSE y) >> f.
When in doubt, parenthesize.
10. Self-Validation Checklist
Before returning a generated model, walk this checklist:
- Header has
MODEL,DESCRIPTION,RUNTIME,VERSION,AUTHOR, and at least oneTAGSentry. -
RUNTIMEmatches the features used (tsif any rule blocks). - Every assignment has a target on the left, an operator, and a non-empty expression.
- All strings use double quotes.
- No range target larger than 100 cells (refactor with array formulas if larger).
- Every external call (
FX_RATE,HTTP_JSON,ML_SCORE) is paired with aDEFAULTdownstream. - Every
EVERYandATblock hasSKIP MISSEDorBACKFILL. - No rule body contains
~=,?=, or+=. - No top-level assignment shares a target with a rule action.
- No function name appears that isn't in
functions.mdor the external registry. - Identifiers don't collide with reserved words.
- All published outputs carry a type tag.
- No multi-line
MATCH(...), multi-line function call, or multi-line chainedTHEN ... ELSE.
If any item fails, fix it before returning.
11. Machine-Readable Schema
For programmatic use, grammar.json provides:
- All function signatures with parameter names, types, return types, variadic info, and external contracts.
- All operators with precedence and associativity.
- All literal forms.
- All directives, runtimes, evaluation modes, and execution classes.
- All error codes with literals and descriptions.
- All rule block templates.
- Type tag taxonomies.
A typical agent loop:
- Fetch
grammar.json. - Build a function-name lookup table from
functions[*].name. - When generating a function call, look up the signature to validate argument count and named-arg names.
- After generating each cell, run the self-validation checklist above.
- Optionally compile the generated model with
npm run compile:modeland surface any diagnostics back to the user.
12. When To Stop And Ask
The agent should stop and ask the user when:
- The user requests behavior that requires a function not in the registry (don't invent one).
- The user requests CRDT-style multi-writer collaboration (out of v1 scope).
- The user requests charts, pivots, or other visualization features (out of v1 scope).
- The user requests a custom external function (not user-definable in v1).
- The user's intent is ambiguous about runtime profile and the model uses borderline features.
Better to ask one question than to emit a model that won't deploy.
13. See Also
reference.md— full language reference.functions.md— every built-in function.errors.md— error codes and recovery.assignments.md— assignment forms.rules-and-schedules.md— rule blocks.external-functions.md— async functions.style-guide.md— human-facing style guide.cookbook.md— recipes by task.grammar.json— machine-readable grammar profile.