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 into your agent's system prompt when generating Grid code.
1. The 15 Hard Rules
These rules are non-negotiable. Violations produce models that don't parse or behave correctly when evaluated.
- 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 quoted namespace specifiers (sheet/workbook names with spaces).
- 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. - Do not choose an engine. Legacy
RUNTIMEdirectives remain parseable, but new generated models should omit them. - 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, except through explicit inspectors. Any ordinary
function receiving an error returns it unchanged. Error-handling functions
(
IFERROR,ISERROR,TRY, etc.), error-kindMATCH, and structural collection predicates are explicit exceptions.IN/HAS,SUBSET/SUPERSET,OVERLAPS, and orderedCONTAINScompare error members by code; diagnostic messages do not affect identity. - External functions return asynchronously. Pair every
FX_RATE/HTTP_JSON/ML_SCOREwith aDEFAULTso downstream bindings 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. - Use external functions deliberately in rule bodies.
A bare async call such as
HTTP_JSON(webhook_url)is a post-commit effect. It cannot be used in a rule assignment RHS because the atomic rule wave does not await it. Put value-producing async work at the top level or in a dependency-awareDOblock. - Surfaces use namespace slots. Generate workbook surfaces with
SurfaceName!type,SurfaceName!config, optionalSurfaceName!data, and optional JSXSurfaceName!source. Do not invent prefix forms.
2. Standard Output Template
When asked to produce a Grid model, emit this template:
MODEL "<short title>"
DESCRIPTION "<one sentence>"
VERSION "1.0.0"
AUTHOR "<author or 'AI Agent'>"
TAGS "<comma-separated tags>"
# Inputs
<input bindings>
# Derivations
<derived bindings>
# Outputs
<output bindings>
# Rules
<rule blocks>
END MODELFill in every header field. Order the bindings from inputs → derivations
→ outputs. Use # Section comments to group.
3. The Decision Tree
When generating each binding, decide in this order. A coordinate target is a
cell; a named target may be an independent calculation or a direct alias (see
concepts.md):
Is this an input the user provides?
YES → A* assignment with type tag, no formula
NO → continue
Does it depend on an external service or expensive computation?
YES → use ~= and pair downstream bindings with DEFAULT
NO → use =
Does it have a meaningful semantic type (currency, percentage, etc.)?
YES → add `IS <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
html"...",xhtml"...",svg"""...""", orjsx"""..."""when the value is markup/code text that should round-trip verbatim. - Use
json"""..."""/yaml"""..."""/csv"""..."""for embedded structured values. - Use
<tag>...</tag>typed literals when an embedded payload is clearer with an explicit close marker, for example<dna>ATCG</dna>or<json>{"a":1}</json>. - Use
A1#to refer to a spilled array as a single value. - Use
IS <tag>for typed outputs andINTO <tag>for crossing-boundary values. - Use
MATCH(subject, val -> result, _ -> default)for value-based matching. - Use error-literal arms in
MATCHto branch on a specific error kind:MATCH(x, #DIV/0! -> 0, #N/A -> BLANK, _ -> x). - 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. - Use workbook surfaces when the user asks for an app-like UI, dashboard, custom interface, map, board, chart, table, document, or workflow screen.
- Use a
SOLVEstatement for goal-seek and bounded optimization over cells —SOLVE Result = Var IN [lo, hi] GOAL Cell = target, orMINIMIZE/MAXIMIZEan objective cell. It writes the answer to the result cell and never mutates the variable cell. (SOLVEis a statement, not an assignment.) - Use the analytical catalog instead of hand-rolling numerics: optimization
(
LINEAR_PROGRAM,MIXED_INTEGER_PROGRAM,MINIMIZE.GLOBAL), differential equations (ODE_SOLVE,PDE_SOLVE_1D), linear algebra (EIGEN.*,SVD.*), and fitting (NSOLVE,CURVE_FIT).
Never
- Emit a bare expression without a target (
SUM(A1:B5)on its own). - Use
'single quotes'for strings (those are namespace specifiers). - Emit monolithic prefix-typed surface roots (
dataset Dataset_1 = …) in new models. Use namespace slots instead (Dataset_1!type,Dataset_1!config, …). - Insert spaces inside HTML-style typed literal tags (
<dna>, not< dna >) or close aliases with a different spelling (<mol>must close with</mol>, not</molecule>). - Use full-column / full-row range targets (
A:A = 0). - Use lazy
~=or put an external call in a rule assignment RHS. Use a bare external call for a post-commit effect; use a top-level orDObinding when the rule needs the call's value. - Nest rule blocks (
WHEN ... WHEN ... END END). - Define a target with both a plain top-level formula and a rule action. Use
defaultfor an intentionally overridable calculation orSTATEfor model-owned mutable state. - Emit a
RUNTIMEdirective in new models. - 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 structural or operator keywords as identifiers (
MODEL,WHEN,THEN,END,NOT, etc.). Grid's grammar is contextual, so some keyword-shaped names parse in narrow positions, but they are not stable authoring names (seereference.md). - Treat
BLANKand""as equivalent. - Treat external function failures as binding 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."
VERSION "1.0.0"
AUTHOR "AI Agent"
TAGS "demo"
# Inputs
A1 is currency = 12000
A2 is currency = 13500
A3 is currency = 14200
A4 is currency = 15000
A5 is currency = 16800
# Derivations
B1 is currency = SUM(A1:A5)
B2 is 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."
VERSION "1.0.0"
AUTHOR "AI Agent"
# Inputs
A1 is 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"
When a model needs an external function's value, bind it outside the rule (or
use DO for a dependency-aware workflow). A bare call in a rule body means
"run this after committing the rule," not "wait for this value."
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."
VERSION "1.0.0"
AUTHOR "AI Agent"
# Inputs
A1 is currency = 100000
# External signal with a fallback
B1 is fx_rate = FX_RATE("EUR", "USD") DEFAULT 1.08
# Derivation
C1 is 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 binding 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."
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."
VERSION "1.0.0"
AUTHOR "AI Agent"
# Input array
A1 = [-3, 7, 1, -2, 9, 4, 8, -1, 5, 6]
# Pipeline
B1 = A1
>> FILTER(value => value > 0)
>> SORT(_, -1)
>> TAKE(5)
# Aggregations
C1 = SUM(B1#)
C2 = AVERAGE(B1#)
END MODELIn a pipe, generate FILTER(value => predicate [, if_empty]). Never generate
the legacy FILTER(_, _ > 0) spelling: the named lambda binds an element, while
_ in calls such as SORT(_, -1) marks the whole piped value.
5.6 "Defensive parse of an external JSON response"
MODEL "External Lookup"
DESCRIPTION "Fetch a user record by id; tolerate failure."
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."
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 MODEL5.8 "Goal-seek the price that hits a target profit"
MODEL "Break-Even Price"
DESCRIPTION "Find the unit price that yields a target profit."
VERSION "1.0.0"
AUTHOR "AI Agent"
# Inputs
A1 is number = 1000 # units sold
A2 is currency = 12 # unit cost
A3 is currency = 40000 # fixed cost
A4 is currency = 20 # price the solver varies
# Derivation
A5 is currency = A1 * (A4 - A2) - A3 # profit at the current price
# Goal-seek: find A4 in [0, 100] that drives profit (A5) to 50,000.
# B1 receives the solved price; A4 is never mutated.
SOLVE B1 = A4 IN [0, 100] GOAL A5 = 50000
END MODEL6. Surface And Custom UI Generation
When the user asks for a UI, dashboard, app, form, board, map, chart, or custom front end, generate model source plus surface slots. Keep formulas and business logic in ordinary model assignments; use the surface for interaction and presentation.
6.1 Pick the surface
| User Intent | Preferred Surface |
|---|---|
| Records, CRM, inventory, orders, editable row data | Table |
| Data import, cleanup, shaping, preview pipeline | Dataset |
| Locations, territories, spatial inspection | Map |
| Events, deadlines, schedules | Calendar |
| Analytical charts | Charts |
| Presentation charts or dashboard visuals | Visuals |
| OHLC market data | Candlestick |
| Narrative report, memo, explanation | Document |
| Freeform planning canvas | Board |
| KPI dashboard or tiled control room | Layout |
| Workflow stages, tasks, pipeline | Kanban |
| Node-link relationships | Diagram |
| Comments, chat, forum, decision log | Discussion |
| Prediction workflow | Predict |
| Custom UI assembled from blocks | App |
| Custom React/JSX UI | Component |
Prefer a built-in surface when it fits. Use App when the user wants a custom
screen but did not explicitly ask for code. Use Component when the user asks
for custom React, bespoke layout, custom interaction, or a UI that should be
easy for a developer to extend.
6.2 Built-in surface slot template
Every built-in surface needs at least !type and !config. Add !data = ""
when the surface owns interactive state.
Table_1!type = "table"
Table_1!config = """
version = 1
kind = "table"
title = "Accounts"
[columns]
bind = "Accounts"
title = "Accounts"
[[columns.fields]]
key = "name"
label = "Name"
[[columns.fields]]
key = "owner"
label = "Owner"
"""
Table_1!data = ""The kind in TOML must match the !type family. Bind fields should name model
symbols created elsewhere in the source.
6.3 Component surface template
A Component surface needs a component type tag, a config with read/write
bindings, and JSX source. Declare writes narrowly; Grid rejects writes outside
the allowlist.
Component_1!type = "component"
Component_1!config = """
version = 1
kind = "component"
title = "Scenario Console"
[bindings]
reads = ["Revenue", "Orders"]
writes = ["Scenario"]
"""
jsx Component_1!source = <jsx>
function App() {
const revenue = model.useValue("Revenue");
const orders = model.useRows("Orders");
function setScenario(value) {
model.set("Scenario", value);
}
return (
<ui.Stack gap={12}>
<ui.Metric label="Revenue" value={revenue} format="currency" />
<ui.Row>
{["bear", "base", "bull"].map((scenario) => (
<ui.Button key={scenario} onClick={() => setScenario(scenario)}>
{scenario}
</ui.Button>
))}
</ui.Row>
<ui.Table columns={orders.columns} rows={orders.rows} empty="No orders yet." />
</ui.Stack>
);
}
render(<App />);
</jsx>Available Component scope:
| Name | Use |
|---|---|
React |
React namespace. |
useState, useEffect, useMemo, useRef, useCallback, useReducer |
React hooks. |
model.get(name) |
Read a scalar once. |
model.useValue(name) |
Reactively read a scalar. |
model.useValues(names) |
Reactively read several symbols. |
model.useRows(name) |
Read row data with status, columns, and rows. |
model.useRange(ref) |
Read a range-like binding as rows. |
model.set(name, value) |
Write an allowed symbol. |
model.setFormula(name, formula) |
Write an allowed formula target when supported. |
ui.Stack, ui.Row, ui.Card, ui.Text, ui.Badge, ui.Button |
Layout, display, and action primitives. |
ui.Metric, ui.KpiGrid, ui.Progress, ui.Sparkline |
Common dashboard, KPI, progress, and trend primitives. |
ui.Table, ui.EmptyState, ui.Tabs |
Row display, empty states, and view switching. |
ui.TextInput, ui.NumberInput, ui.Select, ui.Toggle, ui.Slider |
Controlled input primitives. |
Standard JSX elements such as div, section, input, select, table,
and svg are valid. Do not reference package components unless the host has
made them available in the surface scope.
When generating a custom UI, emit the Component surface as one package: the
!type slot, the !config slot, and the !source slot. Mirror the built-in
Component scaffolds available from the new-surface menu and the Component
editor:
- Dashboard: read scalar KPIs plus one row/range binding; no writes.
- Records: read one row/range binding and optional scalar search/default state; no writes unless the user asks for row actions.
- Controls: read and write only the scalar symbols the UI edits; declare those
symbols in both
bindings.readsandbindings.writes.
Do not generate JSX that writes to a symbol missing from bindings.writes;
Grid will reject the write at runtime.
6.4 Custom UI rules
- Keep formulas, table shaping, and calculations outside the JSX.
- Put only presentation state or UI-builder state in
!data. - Declare every symbol the UI reads under
bindings.reads. - Declare only intentional write targets under
bindings.writes. - Use
model.useRows("Name")for row-backed interfaces and render loading, empty, and error states when helpful. - Do not call browser globals or external network APIs from Component source.
- Do not invent
ui.*primitives. Use the listed primitives or standard JSX. - Include
render(<App />);exactly once at the bottom of Component source.
7. 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) |
data >> FILTER(_, _ > 0) |
data >> FILTER(value => value > 0); use a named lambda for pipe element binding |
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-valued binding, 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 binding, with cleaner syntax) |
B1 = "default"; WHEN ... THEN B1 = "active" END |
Use B1 = cond THEN "active" ELSE "default" for derivation, or default B1 = "default" for an intentional sticky override |
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 is currency = "hello" |
A1 is 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 |
component Component_1 = ... |
Use Component_1!type, Component_1!config, and jsx Component_1!source = <jsx>...</jsx> |
<ui.Chart data={rows} /> |
Use a documented ui.* primitive or standard JSX/SVG; do not invent primitives |
8. 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, html"<p>Hi</p>", svg"""<svg/>""", <jsx>render(<App />);</jsx> |
boolean |
TRUE, FALSE |
date |
d"2026-04-10", dt"2026-04-10T15:30:00Z", human-authored @today / @2026-04-10 |
array |
[1, 2, 3], [1; 2; 3], [1, 2; 3, 4] |
object |
json"""{...}""", <json>{...}</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 |
currency:USD |
number |
typed currency code (bare USD works too) |
date / datetime |
date |
calendar |
duration |
string |
ISO duration |
cron |
string |
cron expression |
symbol |
string |
enum label |
Apply tags at assignment:
A1 is currency = 100000
B1 is percentage = 21pct
C1 is date = d"2026-04-10"Tags form a hierarchy — USD is a currency is a number, json is an
object — and a leaf implies its ancestors (is USD needs no currency).
Under strict, tagging an incompatible value (IS currency = "hello") is a
#TYPE!; under loose (the default) the tag is an unchecked overlay.
Unary predicates are a separate, orthogonal axis on a symbol (type = what it is, predicate = what's true about it). Fieldless colon predicates are open and need no declaration. Structured predicates use a typed declaration:
predicate reviewed(by: string)
x is :happy USD = 1.00 # type USD + predicate :happy; `a` is optional
x is a :reviewed(reviewer) = 1 # typed structured predicate
ready = x is :urgent # boolean membership query
who = PREDICATES(x).reviewed.by # structured field access
same = x.predicates.reviewed.by # equivalent property form
WHEN stale THEN x += :urgent END # reactive add; undeclared starts FALSE
WHEN reviewed THEN x += :reviewed(reviewer) ENDUnary predicates attach to the symbol, not the value (they don't flow through
arithmetic); a symbol without that predicate answers FALSE. a and an
after declaration-side is are readability-only and may be omitted. A
declaration is single-valued by default (an explicit trailing one is also
accepted). To retain several instances, declare stable identity fields:
predicate reviewed(id: symbol, by: symbol, at: datetime) many by (id)
WHEN new_review THEN x += :reviewed(:r1, reviewer, NOW()) END # insert/upsert r1
WHEN retract THEN x -= :reviewed(:r1) END # remove only r1
WHEN reset THEN x -= :reviewed END # remove every review
reviewers = x.predicates.reviewed.by # array for `many`An add supplies the complete payload and replaces the payload already stored
under the same identity without moving it; a keyed remove supplies only the
identity values, in many by (...) order. A bare remove clears all instances.
Presence is true when at least one instance remains. A many-valued field access
returns an array in stable first-insertion order; it returns BLANK when none
remain. Numeric compound assignment keeps its arithmetic meaning inside rule
actions and is rejected at top level. Field names are checked against the
declaration, and field type contracts produce #TYPE! on mismatch even when
ordinary type-tag coercion is loose.
Use MODEL.PREDICATES for reverse lookup. Predicate names are colon-symbol
vertices and model symbols are quoted canonical-name vertices:
all_reviewed = MODEL.PREDICATES.NEIGHBORS(:reviewed)
predicates_on_x = MODEL.PREDICATES.IN_NEIGHBORS("x")
instances = MODEL.PREDICATES.OUT_DEGREE(:reviewed)Its unary portion is a reactive bipartite graph, separate from MODEL.GRAPH.
Each unary edge is :applies; authored binary Predicate facts occupy the same
view. A many predicate contributes parallel edges keyed by its declared
identity, while NEIGHBORS deduplicates the target symbol. The
colon is the predicate namespace: :reviewed cannot collide with the model
symbol "reviewed". Rule mutations automatically invalidate the view. If consuming
MODEL.PREDICATES.EDGES(), treat membership_id as one opaque versioned string
(currently tag-membership:v2:<sha256>); it is stable across payload-only
upserts with the same complete typed identity, but its digest is not syntax.
The older TAGS(x), x.tags, and unary-only MODEL.TAGS spellings remain
accepted compatibility forms; do not generate them in new source.
Open predicate rules and laws use the same symbol truth model:
predicate releasable means reviewed AND NOT blocked
AND EVERY dependency IS :releasable
predicate touches IS symmetric
predicate contains IS inverse OF inside
predicate before IS transitive AND asymmetric
predicate owner IS functional
has_issue = EXISTS dependency OF service WHERE dependency IS :vulnerable
safe = NO path VIA flow* FROM public_input TO secret_output AVOIDS :sanitized
path publication = ingress THEN (transform OR archive)* THEN store
published = MUST(public_input publication ledger)
sensitive = EXISTS publication OF public_input IS :sensitive
sealed = NO path VIA publication
FROM public_input TO secret_output AVOIDS :sanitized
exposed = REACHABLE VIA publication FROM public_inputGenerate only finite rules with stratified negation. Use therefore to check a
conclusion, and bind a modal query before asking WHY(query_cell) for its
proof. Top-level MUST/MAY/CANNOT are constraints; assigned forms are
queries. For open predicates, unknown means MUST = FALSE, MAY = TRUE, and
CANNOT = FALSE; only an explicit prohibition makes CANNOT true. A path cut
uses only relations named by its route (or dependency when VIA is omitted),
never every binary fact in the program. Compose a route with THEN, OR,
postfix *, REVERSE, and parentheses. Name reusable routes with
path name = ...; they may be imported from .gs. Declare a route before
using it. From that point forward its name shadows an ordinary relationship of
the same name; the declaration's right-hand side can still name that
relationship (path flow = flow*).
After declaration, use the name directly as a read-only binary predicate in
queries, rule bodies, and EXISTS/EVERY. A matching route supports it and an
absent route refutes it, so MUST, MAY, CANNOT, and EVIDENCE have closed
graph semantics. Never generate a fact, rule head, law, or top-level modal
assumption for a path name. Evaluation stays lazy; it does not create an
all-pairs edge relation.
REACHABLE VIA name FROM origin returns the sorted accepting symbols, with
optional AVOIDS :p. Its structured WHY evidence carries one witness route
per result when complete and explicitly marks budget-truncated evidence as
partial without truncating the result itself.
Generate one ground atom inside
top-level modalities and assigned MAY/CANNOT; only assigned MUST accepts
a compound proposition. Write repeated routes explicitly (VIA flow*);
bare VIA flow is accepted only as a Predicate v3 compatibility spelling and
formats to the explicit form. WHY on a successful path cut returns a checked
product-frontier certificate when the explanation budget can retain it, and
an ordered counterexample route when false. Always retain the
predicate introducer on laws; it is
what selects an open predicate when a name also belongs to a qualitative
family. Use the explicit p(x) := ... form only when a generated
or advanced rule needs visible variables.
Built-in qualitative predicates need no declaration block. Temporal and topological facts share the same query surface:
design before build
build before launch
answer = design before launch
possible = MAY(build meets launch)
parcel inside district
district separate from river
contained = MUST(parcel inside district)
connected = CANNOT(parcel separate from district)Plain queries mean MUST; MAY and CANNOT expose uncertainty.
RELATION(x,y) inspects the remaining relation set, RELATION_STATUS(x)
inspects consistency, and WHY(answer) carries the active facts and retained
composition proof. Reactive facts use ordinary IF and UNLESS conditions.
Topology uses separate from, touches, partially overlaps, coincides with, inside, encloses, and intersects; containment may be refined with
with boundary contact or without boundary contact. MODEL.PREDICATES is
the separate graph projection of authored binary facts. If one symbol pair
belongs to both families, inspect it with RELATION(x, y, temporal) or
RELATION(x, y, topology). See the predicate guide.
9. 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 |
Grid-generated only (do not write as literals):
| 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.
10. Operator Precedence Cheat Sheet
For the full table see reference.md.
The most common precedence 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. - Infix apply
OFis function-first pipe.ABS() OF A1≡A1 >> ABS(); chains are right-associative (g() OF f() OF x≡x >> f() >> g()). Do not writeA1 OF ABS(); do not chain callables withOF.%OFis percent-of, not apply. - Functional FILTER binds elements explicitly. In
values >> FILTER(value => value > 0),valueis one element;_remains the whole-value insertion marker for calls such asSORT(_, -1). - Combinatoric infix binds tighter than
*.5 * 3 CHOOSE 2is5 * COMBIN(3, 2). InfixCHOOSElowers toCOMBIN, not the lookup functionCHOOSE(index, ...).
When in doubt, parenthesize.
11. Self-Validation Checklist
Before returning a generated model, walk this checklist:
- Header has
MODEL,DESCRIPTION,VERSION,AUTHOR, and at least oneTAGSentry. - No generated model emits a
RUNTIMEdirective. - 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 external function calls. - No top-level assignment shares a target with a rule action.
- No function name appears that isn't in
functions.mdorexternal-functions.md. - Identifiers don't collide with reserved words.
- Pipeline FILTER steps use a named predicate lambda;
_is used only for whole-value insertion or an explicitly placeholder-based helper. - All published outputs carry a type tag.
- If the request includes a UI, the source includes the right surface slots.
- Custom UI surfaces declare reads and writes, use only available Component scope, and keep calculations in model assignments.
- Multi-line expressions only occur inside brackets/parentheses,
block forms, or other documented multi-line contexts; chained
THEN ... ELSEstays on one line.
If any item fails, fix it before returning.
12. Programmatic Generation Loop
A typical agent loop:
- Read the user's intent and identify inputs, derivations, outputs, and rules.
- Use
functions.mdto confirm every function name and signature. - Generate source using the standard template and style rules above.
- Run the self-validation checklist before returning the model.
- If a validation or evaluation tool is available in your product surface, run it and repair diagnostics before returning the final source.
13. 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 a custom external function (Grid's external functions are a fixed built-in catalog, not user-definable).
- The user requests a third-party React package that is not available in the Component surface scope.
- The user's intent is ambiguous enough that several model shapes would be reasonable.
Better to ask one question than to emit a model that won't deploy.
14. See Also
reference.md— full language reference.functions.md— every built-in function.errors.md— error codes and recovery.assignments.md— assignment forms.../product/surfaces.md— built-in and custom workbook surfaces.rules-and-schedules.md— rule blocks.external-functions.md— async functions.style-guide.md— human-facing style guide.cookbook.md— recipes by task.