Assignments

Assignments

Assignments

Assignments are the primary statement form. They define or update addressable bindings; a cell is the coordinate-addressed kind of binding. See concepts.md for the vocabulary used here.

This doc covers all assignment shapes. For rule blocks, see rules-and-schedules.md. For the language as a whole, see reference.md.


1. Anatomy

<target> IS <type-tag> <op> <expression>
<type-tag> <target> <op> <expression>
Part Required Notes
<target> yes A cell reference, named value, or finite range
<type-tag> no Semantic or native-kind contract, e.g. currency, List of String, or Tensor[?, 4]
<op> yes At top level, one of =, ~=, ?=. Compound operators are contextual: arithmetic in rule actions, colon-predicate add/remove (optionally with declared fields), and typed mutations such as table updates.
<expression> yes Any expression — see reference.md

Examples:

A1 = 10
A2 is currency = A1 * 100
currency A3 = A1 * 100
A3 ~= EXPENSIVE_FETCH()
A4 ?= MAYBE_FAIL()
A5 += :reviewed
A6:A10 = B1

2. Assignment Operators

2.1 = (Eager)

The default. The cell recomputes every time any of its inputs change.

A1 = SUM(B1:B10)

When any of B1 through B10 change, A1 recomputes immediately.

2.2 ~= (Lazy)

The cell does not compute until something reads it. Useful for expensive or external work.

A2 ~= ML_SCORE([0.1, 0.2, 0.3, 0.4])

Reads happen when the cell is displayed, inspected, or referenced by another cell. A downstream eager cell that references A2 triggers A2 to compute.

Once computed, the value is cached. The cache invalidates when an input changes, returning the cell to the lazy state.

2.3 ?= (Conditional Eager)

Same as =, but if the right-hand side evaluates to an error, the assignment is skipped — the existing value (if any) is preserved.

A3 ?= MAYBE_FAIL()

If MAYBE_FAIL() returns #N/A, A3 keeps its previous value rather than becoming #N/A.

2.4 Compound Assignments

WHEN increment_requested THEN
  A4 += 5
  B4 -= 5
  C4 *= 2
  D4 /= 2
END

These work only on single-cell targets. The right-hand side is evaluated against the current value of the target.

Arithmetic compound assignment is valid only in rule actions, where the target has a committed value and the rule transaction supplies a real prior-value source. At top level, Grid rejects it with GRID_TOP_LEVEL_COMPOUND_ASSIGNMENT; use a reactive rule action for state changes. Typed mutation contexts such as table updates retain their documented operator meanings.

2.5 Graph handle declarations

graph is an exact named-handle declaration rather than a cell type. It uses plain = and a graph-producing expression:

graph roads = GRAPH(edges AS edges, "id" AS edge_id, "from" AS from, "to" AS to)
graph reversed = roads.REVERSE()

Graph handles cannot target cells or ranges and do not support decorators, mutation operators, lazy/conditional assignment, or schedules. See graphs.md for construction and query semantics.

2.6 Table mutations (type-dispatched)

When the assignment target is a table-typed named wire, compound operators mean record mutations against the model's embedded table store — not arithmetic. Grid applies them as record operations rather than binary expressions.

Operator Meaning on table symbol
+= Insert row(s)
^= Upsert (merge on row _id)
*= Update (patch by _id or filter)
-= Delete (by _id or filter)
table Orders += { name: "Acme", amount: 100 }
table Orders += COLLECT(inbound_frame)
table Orders ^= { id: "rec01HXYZ", status: "Shipped" }
table Orders *= { id: "rec01HXYZ", amount: 200 }
table Orders -= { id: "rec01HXYZ" }

Inside rule actions, cells and numeric named wires use += / -= / *= / /= for arithmetic read-modify-write. Ordinary top-level arithmetic compound assignment is rejected. ^= remains the table upsert operator in this typed mutation context; exponentiation remains an expression operator.

A standalone top-level table-mutation statement is an explicit host command: it commits once when submitted, rather than becoming a recalculating formula. The same four forms are valid inside WHEN/EVERY/AT bodies. There the RHS uses the rule's pre-commit snapshot and the durable table write runs after the rule's atomic cell-state commit.


3. Targets

3.1 Single Cell

A1 = 10
$A$1 = 10
Sheet1!A1 = 10
'Q4 Revenue'!B2 = 10
Revenue = 10                 # named reference

Workbook surfaces

Surfaces are namespaces that store component values with the same namespace specifier syntax as sheets (Namespace!Component).

Dataset_1!type = "dataset"
Dataset_1!config = """
version = 1
kind = "dataset"
title = "Dataset_1"
...
"""
Dataset_1!layout = ""
Dataset_1!data = ""

The tab Dataset_1 appears when Dataset_1!type names a known surface kind. The frontend reads component values (especially !config) and renders the matching surface UI.

See ../product/surfaces.md for the surface families.

3.2 Named Address Aliases

A plain named assignment whose entire right-hand side is one absolute reference adds another address for the existing binding:

A1 = 125000
Revenue = A1
ReportedRevenue = Revenue

A1, Revenue, and ReportedRevenue address the same reactive binding. An external write or rule override through any of those names changes the value seen through all of them. They also share history and explanation identity; the aliases do not add calculation steps.

This rule applies only when the target is a name. Coordinate-to-coordinate assignment retains spreadsheet behavior:

B1 = A1                    # a second cell whose formula reads A1
Tax = Revenue * tax_rate   # a separate named calculation

The direct-reference form is deliberately recognizable from source. It is an alias when all of the following are true:

  • the target is a named address;
  • the right-hand side is exactly one absolute cell or named reference;
  • the assignment is ordinary eager =;
  • it has no type contract, default, STATE, schedule, lazy mode, or conditional mode.

input and output only declare the external surface, so they may label an alias without creating another calculation:

output Revenue = A1
input Assumption = B2

When a name needs its own type, state, schedule, or error-preservation policy, Grid creates an independent named binding even if the expression is a single reference. Unary predicates remain properties of the authored name rather than flowing through the aliased value.

3.3 Finite Ranges

A range target broadcasts the right-hand side to each cell in the range. The range must be a finite rectangle.

A1:A10 = B1                  # writes B1 to each of A1..A10
A1:C3 = 0                    # writes 0 to all 9 cells
A1:A5 = [10, 20, 30, 40, 50] # writes the array elementwise

Full-column (A:A) and full-row (1:1) targets are not allowed.

3.4 Destructuring

Bind multiple symbols from a single array-returning expression:

[total, avg, min, max] = SUMMARY(B1:B10)

Rest capture binds the tail:

[head, ...tail] = ARRAY_OF_VALUES

The number of named bindings before ... must not exceed the array length.


4. Type Tags

A type tag attaches semantic meaning to the value. The grammar is:

<target> IS [A | AN] <type-tag> = <expression>

Equivalently, prefix notation is accepted:

<type-tag> <target> = <expression>
A1 IS currency = 100000
A1 IS A currency = 100000
A1 IS AN fx_rate = 1.0825
currency A1 = 100000
currency x = 10
A2 is percentage = 21pct
A3 is date = d"2026-04-10"
A4 is bps = 25bps
A5 is score = ML_SCORE(features)
A6 is unit:m/s = distance / duration

Under strict, a value that cannot inhabit the tag's representation root is a #TYPE! — tagging is currency with a word-string errors, while a numeric string still coerces. loose (the default) applies the tag as an overlay without checking. This rides the coercion axis, so strict except coercion turns it off.

Tags form a hierarchy (USD is a currency is a number), and a leaf implies its ancestors — is USD needs no separate currency. See reference.md for the full tree.

Unit and currency tags are load-bearing in dimensional analysis. Physical units are built in (unit:m, unit:C, unit:delta_C) and currencies take the currency: qualifier (currency:USD, or bare USD); models may also declare scale-only custom units near the top of the source:

unit smoot = 1.7018 m
distance is unit:smoot = 2

Native value kinds and optional contracts

Native structures use the same IS relationship. The shortest useful type is the kind itself:

items IS List = VECTOR(1, "two", {name: "three"})
pending IS Deque = DEQUE()
weights IS Tensor = A1:D1000

Those declarations do not demand homogeneous elements or a predeclared shape. The constructor, inferred expression, and resident runtime value establish the kind; an annotation merely adds a contract when the author wants one. Omitting the annotation entirely remains equally valid.

Parameterized contracts participate in Grid's coercion axis. strict checks their contents recursively, warn keeps the value and emits a coercion diagnostic, and the default loose mode does not scan collection contents. Bare kinds always remain open. Type nesting is bounded at 64 levels so hostile source cannot exhaust the compiler stack.

Use of to add an element contract and to for a keyed value contract. Types nest without switching to a punctuation-heavy mini-language:

names IS List of String = VECTOR("Ada", "Edsger")
jobs IS Deque of Job = DEQUE()
orders IS Map of Customer Id to List of Order = MAP_OF()
routes IS Trie of Route = TRIE()
latency IS Quantile Sketch = KLL()

List is the language name for Grid's persistent indexed sequence. The legacy spellings Vector<String> and Map<String, Vector<Order>> remain accepted, but grid fmt emits List of String and Map of String to List of Order.

Tensor follows the same progression. Shape, semantic element type, and precision are independent, optional contracts:

samples IS Tensor = A1:D1000
points IS Tensor[?, 3] = A1:C1000
distances IS Tensor[?] of unit:m = A1:A1000
weights IS Tensor[2, 2] with precision 32 = PARAMETER([[0.1, 0], [0, 0.1]])
score IS Tensor[] = AVERAGE(weights)

Omitted precision uses ordinary Grid Number semantics. Precision 32 is an explicit storage/computation policy; precision 64 is available when authors want to pin the default. Low-bit 2/4/8 storage is quantization, not floating precision, and is deliberately not disguised as a numeric type.

Tensor contracts are persisted independently from backend promotion. Shape, precision, and semantic element metadata therefore survive compilation even when a formula takes a general evaluation path; runtime publication restores the semantic element envelope without copying resident Tensor storage.

4.3 Dynamic cast inside expressions

Declaration-level IS attaches a tag to the assignment target. To tag a value inside an expression — for intermediate results or nested assignments — use infix INTO:

B1 = A1 * fx_rate INTO currency
C1 = ROUND(amount, 2) INTO currency

This lowers to TYPE_TAG(value, "<tag>") and applies the same semantic overlay as target IS <tag> = …. Use IS on the target when the whole cell carries the tag; use INTO when only part of the expression should be tagged.


4.1 Tags On Range Targets

The tag applies to every cell in the range:

A1:A10 is currency = 0

4.2 Tags Are Sticky

Once a cell carries a type tag, the tag persists across recomputations until a different assignment overwrites it (or removes it by omitting the tag).

A1 is currency = 100              # A1 is currency
A1 = A1 + 1                     # A1 is still currency
A1 is percentage = 0.5            # A1 is now percentage
A1 = "hello"                    # A1 is now plain string (tag removed)

4.3 Unary predicates

Where a type tag says what a value is, a predicate says what is true about the symbol. Colon predicates form an open, multi-valued set orthogonal to the type. Fieldless predicates are ad hoc and need no declaration. Use predicate to declare a reusable typed field schema shared by unary attachments and explicit model-graph relationship payloads:

predicate reviewed(by: string, score: number)
 
x is :happy USD = 1.00            # type USD, plus predicate :happy
x is a :draft :urgent = 5         # several predicates, no type
x is a :reviewed(reviewer, 0.95) USD = 1.00
reviewer1 = PREDICATES(x).reviewed.by
reviewer2 = x.predicates.reviewed.by  # equivalent

The a or an after declaration-side is is optional surface sugar; the leading colon identifies a predicate. A declaration is single-valued by default, with an optional explicit one designation. A multi-valued predicate declares the field tuple that distinguishes its instances:

predicate reviewed(id: symbol, by: symbol, at: datetime) many by (id)
 
x is :reviewed(:r1, :ada, NOW()) :reviewed(:r2, :lin, NOW()) = report
review_ids = PREDICATES(x).reviewed.id  # [:r1, :r2]
reviewers = x.predicates.reviewed.by    # [:ada, :lin]

The declared field count and types apply wherever :reviewed(...) is attached. An undeclared predicate may not carry fields. Field access is schema-checked; PREDICATES(x).reviewed and x.predicates.reviewed query Boolean presence, while a field of an absent predicate is BLANK. A many-valued field returns an array in stable first-insertion order. Declared field types fail closed with #TYPE! even in loose mode. For a single predicate, += :reviewed(...) replaces its atomic state and bare -= :reviewed removes it. For many, += supplies the full payload and upserts by the declared identity; -= :reviewed(key, ...) supplies keys in many by order and removes that instance; bare -= :reviewed removes all instances. These mutations are reactive inside WHEN, EVERY, and AT rules; a predicate introduced only by a rule starts absent until an add action fires. Unary predicates describe the symbol, not the value, so they don't flow through arithmetic. See the predicate guide and reference.md.

Reverse lookup is available through the unified reactive Graph receiver MODEL.PREDICATES:

all_reviewed = MODEL.PREDICATES.NEIGHBORS(:reviewed)
predicates_on_x = MODEL.PREDICATES.IN_NEIGHBORS("x")

Within this view, colon symbols are predicate vertices and quoted canonical names are model-symbol vertices. The colon therefore prevents :reviewed from colliding with a model symbol named reviewed. Each directed edge has kind :applies; structured fields live on that edge. Authored binary Predicate facts occupy the same view. A many unary predicate has one parallel edge per identity, though NEIGHBORS still returns each target symbol once. Rule mutations revise the view automatically. It is deliberately separate from MODEL.GRAPH, so unary-predicate membership does not change model dependency or dependency algorithms. The carrier-owned membership ID is an opaque versioned string, currently tag-membership:v2:<sha256>, and stays stable when an upsert changes payload fields without changing its typed identity. TAGS(x), x.tags, and the unary-only MODEL.TAGS view remain accepted for source compatibility; new source should use the Predicate spellings above.


5. Ownership, State, And Model Boundaries

Every writable binding has two conceptual value layers:

  • The base is the value produced by its top-level declaration.
  • A sticky override is a committed write from an admitted external writer or a rule action. While present, it wins over the base.

The value authors read is the effective value: the override when one is present, otherwise the base. RESET removes an override and reveals the base; CLEAR installs a blank override. Aliases share these layers because they are additional addresses for the same binding.

Declarations state who is supposed to write:

Form Intended owner Base value Other writers
A1 = expr formula expr none; a rule set earns GRID_MIXED_OWNERSHIP
input A1 = expr external caller expr admitted external writes
default A1 = expr formula plus override layer expr rules; external writes when also declared input
STATE A1 = expr model rules initial/reset value expr rule actions
rule-only target model rules BLANK rule actions

output does not grant write authority. It publishes a binding on the model's read surface.

Two optional decorator keywords may appear at the start of an assignment:

input  <target>[IS <tag>] = <expression>
output <target>[IS <tag>] <op> <expression>

They label bindings as part of the model's external write surface (input) and read surface (output).

5.1 input Decorator

input marks a binding as externally writable.

input price = 0
input qty   IS integer = 0
input items = []

Restrictionsinput may only appear on a plain eager = assignment with a default value. The parser rejects:

Form Why
input A1 ~= … Lazy bindings have no initial value to act as a default
input A1 ?= … Conditional bindings skip on error — undefined initial state
input A1 += 1 (and -=, *=, /=) Arithmetic compound assignment belongs in a rule action, not a declaration
input A1 = … ONCE (and EVERY, AT) Scheduled bindings are driven by time, not external writes
Inside WHEN … THEN … END rule actions Rule actions are model-owned; declare input on the top-level binding instead

The default value (the right-hand side of =) is the value the binding holds before any external write arrives. It can be any expression — including a function call — as long as it does not depend on other declared inputs that have no default of their own.

5.2 output Decorator

output marks a binding as externally readable.

output total       = price * qty
output history     ~= EXPENSIVE_AGGREGATE(transactions)
output last_seen   = NOW() ONCE
output default running_log = []

output may decorate top-level single-cell and named assignments. Rule actions do not declare model boundaries; declare the target as an output at the top level, or publish a semantic alias for it. Use outputs deliberately to define what the model promises to consumers.

5.3 Combining Decorators

input and output may appear together, in either order:

input output A1 = 0            # writable input that also surfaces as a read
output input A1 = 0            # equivalent

Each keyword may appear at most once per assignment. Decorators are case-insensitive (input, INPUT, Input all parse the same).

5.4 default Decorator

default marks a formula as an overridable base value. The formula computes normally, but a rule action (or an external input) may install an override on top; the override wins and is sticky (it persists until cleared), and RESET restores the base.

default price = model_estimate     # computed default, may be overridden
WHEN incident THEN price = manual_number END   # override wins, sticky
WHEN resolved THEN RESET price          END    # base governs again

Without default, a cell is owned by either its formula or its rule actions, never both: a rule that sets a plain formula cell shadows it silently and earns the GRID_MIXED_OWNERSHIP warning (see errors.md). default is the opt-in that makes the override intentional and inspectable — WHY reports the value as overridden rather than silently tracing the shadowed formula.

default applies to single-cell and named formula targets. It is not supported on range or table-mutation targets. It composes with type tags (default B1 IS currency = …). Like input/output, it is case-insensitive and appears at the start of the assignment.

It may compose with input when both external callers and model rules are intentional writers:

input default price = model_estimate

5.5 STATE Declaration

STATE declares model-owned mutable state. Its right-hand side is the initial value and the value restored by RESET; rule actions install sticky revisions on top of it.

STATE retry_count = 0
 
WHEN request_failed THEN retry_count += 1 END
WHEN request_succeeded THEN RESET retry_count END
 
output retries = retry_count

STATE is distinct from an ordinary formula. A formula describes what a value is now from its dependencies; state remembers what prior rule waves did. It is also distinct from input: rules own state, while callers own inputs.

A state declaration uses a single-cell or named target with eager =. It cannot itself be decorated input or output. Publish state through an output alias or a derived output, as above. STATE uses the same preserved base and override machinery as default, but keeps its authored state identity for formatting and tooling.

5.6 Declared Surface

Use decorators to make the model boundary explicit:

  • Inputs are the cells a caller or collaborator is expected to change.
  • Outputs are the cells a caller or collaborator is expected to read.
  • Any referenced but undeclared cell is just an implementation detail of the model.

Even when a workspace does not enforce the boundary, the declarations remain valuable documentation for people and tools.

5.7 Common Mistakes

input A1 += 1     # rejected: input requires plain `=` with a default
input A1 ~= 0     # rejected: input requires plain `=` with a default
A1 = input        # rejected: `input` is a reserved word
input input A1=0  # rejected: duplicate `input` decorator
STATE input A1=0  # rejected: state is model-owned, not an external input

To attach input to a counter you intend to increment from rules, declare the input separately:

input default counter = 0        # external surface plus intentional rule writes
WHEN trigger > 0 THEN
  counter += 1
END
output current_counter = counter  # publish the binding at top level

6. Schedule Modifiers

A single-action assignment can carry a schedule modifier. This is equivalent to wrapping the assignment in a single-action rule block.

G1 = NOW() EVERY cron"0 * * * *" BACKFILL
G2 = TRUE AT dt"2026-12-31T23:59:00Z"
G3 = NOW() ONCE

Schedule modifiers are sugar for:

EVERY cron"0 * * * *" BACKFILL THEN
  G1 = NOW()
END
 
AT dt"2026-12-31T23:59:00Z" SKIP MISSED THEN
  G2 = TRUE
END

Three forms are accepted:

Form Behavior
<expr> EVERY <duration|cron> [SKIP MISSED|BACKFILL] Wraps as an EVERY rule block
<expr> AT <datetime> [SKIP MISSED|BACKFILL] Wraps as an AT rule block
<expr> ONCE Capture the first successful evaluation and do not recompute it. Useful for snapshot values such as B1 = NOW() ONCE. An unchanged assignment keeps its captured value when the same resident model is replaced; changing the expression starts a new capture.

6.1 Restrictions

Schedule modifiers attach only to eager = assignments. Grid rejects:

  • ~= lazy assignments
  • ?= conditional eager assignments
  • += / -= / *= / /= compound assignments

A model that combines any schedule modifier with ~=, ?=, or a compound operator builds with GRID_ASSIGNMENT_SCHEDULE_OPERATOR. The diagnostic names the attempted scheduled form, explains why schedules require eager writes, identifies an eager top-level assignment as the valid context, and shows an = rewrite. See contextual grammar diagnostics.

6.2 Semantics

The canonical style prefers explicit rule blocks for shared models; modifiers are a convenience for short single-action cases. An inline EVERY or AT target exists as BLANK before its first firing. Its RHS is evaluated only by the scheduler, not as an ordinary base formula.

6.3 ONCE semantics

ONCE cells follow these rules:

  1. The expression evaluates on the first evaluation wave that includes the cell (normally model application or its first read).
  2. After the first successful evaluation, invalidations from upstream inputs or calculations do not trigger recomputation. The cell stays at its captured value.
  3. Same-id replacement or runtime-host restart preserves the prior ONCE value if the cell's expression is unchanged. If the expression changes, the cell recomputes once with the new expression.
  4. If the first attempt errors (e.g. #DIV/0!), the result is not sticky — the cell retries on the next invalidation wave.

7. Nested External Calls

When an external call is nested inside a larger formula, Grid tracks that call separately so the fetched value can be cached and retried independently:

A1 = ROUND(FX_RATE("EUR", "USD") + 0.01, 4)

For readability, prefer naming the external boundary when it is reused:

eur_usd = FX_RATE("EUR", "USD") DEFAULT 1.08
A1 = ROUND(eur_usd + 0.01, 4)

8. Restrictions Summary

Constraint Where it applies
Range targets must be finite rectangles Top-level and rule-action assignments
Arithmetic compound += / -= / *= / /= is valid only on a single-cell or named target inside a rule action Rule action body
Value-first <value> AS <parameter> binds call arguments by name; it is not assignment All contexts
Inside rule actions, =, ?=, and compound (+=/-=/*=//=) are allowed; ~= and nested rules are rejected Rule action body
Rule action RHS may not contain external async calls because rule actions must commit atomically with the rule wave Rule action body
Namespace specifiers use single quotes for spaces; identifiers don't need them All contexts
input decorator requires a plain = assignment with a default value; rejected on ~=, ?=, compound, scheduled, and rule-action targets All contexts
output decorates top-level single-cell or named assignments; rule actions do not declare the model boundary Top level
STATE uses an eager = single-cell or named declaration and cannot also be input or output Top level
Each decorator (input, output) may appear at most once per assignment All contexts

9. Common Mistakes

9.1 Missing Operator

A1 10            # parse error
A1 = 10          # correct

9.2 Single-Quoted String

A1 = 'hello'     # 'hello' is parsed as a namespace specifier, not a string
A1 = "hello"     # correct

9.3 Range Target Too Large

A:A = 0          # rejected — full-column ranges not allowed as targets
A1:A1000 = 0    # works, but explodes the dependency graph
A1:A1000 ~= 0   # ~= not allowed in range target broadcasting

Use an anchor assignment with an array-valued RHS instead. For constant fills the dedicated helpers are clearest:

A1 = ZEROS(1000, 1)         # column of zeros
A1 = ONES(1000, 1)          # column of ones
A1 = FILL("TBD", 1000, 1)   # column of any constant value
A1 = REPEAT(0, 1000)        # equivalent shorthand for column of zeros

When each output element depends on its row/column index, prefer an array comprehension — it lowers to a single MAKEARRAY binding but reads more naturally:

A1 = [r * 2 FOR r IN 1..1000]
A1 = [r * c FOR r IN 1..100, c IN 1..10]

Reach for MAKEARRAY directly when you need three or more index variables, want to share heavy intermediates via LET, or are computing the dimensions dynamically. For constant fills, prefer FILL / ZEROS / ONES / REPEAT.

9.4 Incompatible Type Tag

A1 is currency = "hello"      # #TYPE! — string can't be currency
A1 is currency = "100"        # OK — string coerces to number
A1 is currency = 100          # canonical

9.5 Cycle

A1 = A2 + 1
A2 = A1 + 1     # both cells become #CIRCULAR_REF!

Cycles are detected and reported as #CIRCULAR_REF! unless iterative calculation is enabled for a convergent cycle. Lazy bindings (~=) participate in cycle detection.


10. Examples

A complete, well-typed model:

MODEL "Pricing"
VERSION "1.0.0"
 
# Declared inputs (writable surface in protected mode)
input  A1 is currency   = 100000    # base price
input  A2 is percentage = 7pct      # discount
input  A3 is percentage = 21pct     # tax rate
 
# Declared outputs (readable surface in protected mode)
output B1 is currency = A1 * (1 - A2)      # discounted price
output B2 is currency = B1 * (1 + A3)      # gross price
 
# Lazy expensive lookup, exposed as output
output C1 ~= FX_RATE("USD", "EUR")
 
# Conditional output (only if FX succeeds)
output D1 is currency ?= B2 * C1
 
# Internal counter (not part of the external surface)
default E1 = 0
WHEN increment_requested THEN E1 += 1 END
 
# Range broadcast (every cell carries the `output` decorator)
output F1:F5 = 0
 
# Destructuring (decorator broadcasts to each binding)
output [min, max, avg] = SUMMARY([100, 200, 300, 400])
 
END MODEL

This model is safe to deploy with protectedMode: true: every implicit input is declared (the model has no implicit inputs), and the model declares both an input and an output.


11. SOLVE Statements

A SOLVE statement declares a goal-seek or bounded optimization over workbook cells. The result cell receives the value of the variable cell that satisfies the goal; the variable cell itself is never mutated, so the sheet stays consistent and auditable.

SOLVE Z9 = A2 IN [0, 1] GOAL A3 = 200000
SOLVE B9 = A1 IN [0, 5] MINIMIZE A2
SOLVE B9 = A1 IN [0, 5] MAXIMIZE A2
  • Z9 — result cell (receives the solved variable value).
  • A2 — variable cell the solver varies inside the bounds [lo, hi] (fractional bounds are fine).
  • GOAL A3 = 200000 drives cell A3 to the target by root finding; MINIMIZE / MAXIMIZE optimize the objective cell instead.
  • The goal may reach the variable through intermediate cells (chains and diamonds compose by the chain rule); other referenced cells participate as data parameters at their current values.
  • Solves are reactive: editing an upstream input re-solves on the next resolve.

Current limits: one variable per statement, default-sheet addresses, and expression-backed chains (a chain crossing a descriptor-compacted range refuses). A statement the compiler cannot lower reports #CALC! (SOLVE_NOT_COMPILED); a solve that fails at runtime (no root in the bounds, non-smooth objective) reports #CALC! (SOLVE_FAILED) on the result cell without failing the rest of the model.

Solves execute on the solver-region lane and write back unconditionally — the result cell has no other evaluator. The resolve response carries a structured solverWrites report (cell, values, boundDuals), where boundDuals is the goal's marginal at the solution (∂goal/∂variable) — the shadow-price view of the solve; the same values appear in the solve's diagnostics.


12. See Also