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 = B12. 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
ENDThese 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 referenceWorkbook 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 = RevenueA1, 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 calculationThe 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 = B2When 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 elementwiseFull-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_VALUESThe 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 / durationUnder 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 = 2Native 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:D1000Those 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 currencyThis 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 = 04.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 # equivalentThe 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 = []Restrictions — input 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 # equivalentEach 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 againWithout 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_estimate5.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_countSTATE 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 inputTo 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 level6. 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() ONCESchedule modifiers are sugar for:
EVERY cron"0 * * * *" BACKFILL THEN
G1 = NOW()
END
AT dt"2026-12-31T23:59:00Z" SKIP MISSED THEN
G2 = TRUE
ENDThree 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:
- The expression evaluates on the first evaluation wave that includes the cell (normally model application or its first read).
- After the first successful evaluation, invalidations from upstream inputs or calculations do not trigger recomputation. The cell stays at its captured value.
- Same-id replacement or runtime-host restart preserves the prior
ONCEvalue if the cell's expression is unchanged. If the expression changes, the cell recomputes once with the new expression. - 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 # correct9.2 Single-Quoted String
A1 = 'hello' # 'hello' is parsed as a namespace specifier, not a string
A1 = "hello" # correct9.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 broadcastingUse 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 zerosWhen 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 # canonical9.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 MODELThis 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 A2Z9— result cell (receives the solved variable value).A2— variable cell the solver varies inside the bounds[lo, hi](fractional bounds are fine).GOAL A3 = 200000drives cellA3to the target by root finding;MINIMIZE/MAXIMIZEoptimize 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
reference.mdfor the type system.rules-and-schedules.mdfor rule blocks.external-functions.mdfor~=deep dive.errors.mdfor#TYPE!,#CIRCULAR_REF!, etc.