Agent guide

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.

  1. Every line is exactly one statement. No bare expressions. No "implicit" assignments. Every assignment has a target and an operator.
  2. Strings use double quotes only. Single quotes are reserved for quoted namespace specifiers (sheet/workbook names with spaces).
  3. Range targets must be finite rectangles. A1:A10 is OK. A:A is not.
  4. Inside rule bodies (WHEN/EVERY/AT), use =, ?=, or the compound assignments (+=, -=, *=, /=). Lazy ~= and nested rule blocks are still rejected. No metadata.
  5. Do not choose an engine. Legacy RUNTIME directives remain parseable, but new generated models should omit them.
  6. Every EVERY and AT must have a missed-run policy (SKIP MISSED or BACKFILL). Pick one explicitly.
  7. Function names are case-insensitive at parse, but emit them uppercase. SUM, not sum.
  8. BLANK is not "". Use BLANK to mean absence; use "" to mean an empty string.
  9. Errors propagate, except through explicit inspectors. Any ordinary function receiving an error returns it unchanged. Error-handling functions (IFERROR, ISERROR, TRY, etc.), error-kind MATCH, and structural collection predicates are explicit exceptions. IN/HAS, SUBSET/SUPERSET, OVERLAPS, and ordered CONTAINS compare error members by code; diagnostic messages do not affect identity.
  10. External functions return asynchronously. Pair every FX_RATE/HTTP_JSON/ML_SCORE with a DEFAULT so downstream bindings stay computable.
  11. 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.
  12. 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 like revenue, total_cost, is_active.
  13. 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, WITH bindings) also span lines. The two things that still need a single line are chained THEN ... ELSE ladders and the THEN <expr> ELSE <fallback> tail of a WITH. If a chained THEN ... ELSE gets too long, switch to CASE WHEN or extract intermediates with DO. See reference.md §14.1.
  14. 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-aware DO block.
  15. Surfaces use namespace slots. Generate workbook surfaces with SurfaceName!type, SurfaceName!config, optional SurfaceName!data, and optional JSX SurfaceName!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 MODEL

Fill 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 bare

4. 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" or wild"???-*.csv" for wildcard patterns.
  • Use :identifier symbol literals for state labels.
  • Use html"...", xhtml"...", svg"""...""", or jsx"""...""" 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 and INTO <tag> for crossing-boundary values.
  • Use MATCH(subject, val -> result, _ -> default) for value-based matching.
  • Use error-literal arms in MATCH to branch on a specific error kind: MATCH(x, #DIV/0! -> 0, #N/A -> BLANK, _ -> x).
  • Use CASE WHEN for distinct-condition branching.
  • Use WITH ... THEN ... ELSE for error-tolerant external chains.
  • Use ASSERT value > 0 ELSE #N/A instead of nested IF(cond, val, #N/A).
  • Use DEFAULT rather than ?? (both work; DEFAULT is canonical).
  • Use DO ... END rather than top-level LET(...).
  • Use placeholder lambdas (_, _1, _2) only inside higher-order helpers and pipes.
  • Use +/, */, &/ reduction operators when they're clearer than SUM, PRODUCT, CONCAT.
  • Use data[^1] or data[-1] for last-element access instead of INDEX(data, ROWS(data)).
  • Use [2:5] slice notation instead of TAKE(DROP(data, 1), 4).
  • Use UNION/INTERSECT/EXCEPT for 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 SOLVE statement for goal-seek and bounded optimization over cells — SOLVE Result = Var IN [lo, hi] GOAL Cell = target, or MINIMIZE / MAXIMIZE an objective cell. It writes the answer to the result cell and never mutates the variable cell. (SOLVE is 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 or DO binding 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 default for an intentionally overridable calculation or STATE for model-owned mutable state.
  • Emit a RUNTIME directive in new models.
  • Omit SKIP MISSED / BACKFILL from EVERY/AT blocks.
  • Use placeholders standalone outside higher-order helpers and pipes (A1 = _ * 2 is 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 (see reference.md).
  • Treat BLANK and "" as equivalent.
  • Treat external function failures as binding errors without a fallback — always pair with DEFAULT.
  • Wrap a single function in LAMBDA when 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 MODEL

5.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 MODEL

5.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 MODEL

If 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 MODEL

5.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 MODEL

In 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 MODEL

5.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 MODEL

5.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 MODEL

6. 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.reads and bindings.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) END

Unary 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_input

Generate 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:

  1. Power (^) is right-associative. 2^3^2 = 2^(3^2) = 512.
  2. & binds tighter than comparison. "a" & "b" = "ab" parses as expected.
  3. Comparison is non-associative. 0 < x < 10 is not valid. Use x BETWEEN 0 AND 10 or 0 < x AND x < 10.
  4. THEN ... ELSE is lower than logical operators. A AND B THEN x ELSE y reads as (A AND B) THEN x ELSE y.
  5. Pipes are below all conditional and logical operators. cond THEN x ELSE y >> f reads as (cond THEN x ELSE y) >> f.
  6. Infix apply OF is function-first pipe. ABS() OF A1A1 >> ABS(); chains are right-associative (g() OF f() OF xx >> f() >> g()). Do not write A1 OF ABS(); do not chain callables with OF. %OF is percent-of, not apply.
  7. Functional FILTER binds elements explicitly. In values >> FILTER(value => value > 0), value is one element; _ remains the whole-value insertion marker for calls such as SORT(_, -1).
  8. Combinatoric infix binds tighter than *. 5 * 3 CHOOSE 2 is 5 * COMBIN(3, 2). Infix CHOOSE lowers to COMBIN, not the lookup function CHOOSE(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 one TAGS entry.
  • No generated model emits a RUNTIME directive.
  • 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 a DEFAULT downstream.
  • Every EVERY and AT block has SKIP MISSED or BACKFILL.
  • 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.md or external-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 ... ELSE stays on one line.

If any item fails, fix it before returning.


12. Programmatic Generation Loop

A typical agent loop:

  1. Read the user's intent and identify inputs, derivations, outputs, and rules.
  2. Use functions.md to confirm every function name and signature.
  3. Generate source using the standard template and style rules above.
  4. Run the self-validation checklist before returning the model.
  5. 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