Assignments
Assignments
Assignments are the primary statement form. Every cell value comes from an assignment.
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>[::<type-tag>] <op> <expression>| Part | Required | Notes |
|---|---|---|
<target> |
yes | A cell reference or a finite range |
<type-tag> |
no | Semantic type label, e.g. currency |
<op> |
yes | One of =, ~=, ?=, +=, -=, *=, /= |
<expression> |
yes | Any expression — see reference.md |
Examples:
A1 = 10
A2 as currency = A1 * 100
A3 ~= EXPENSIVE_FETCH()
A4 ?= MAYBE_FAIL()
A5 += 5
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 via:
- Direct reads from the API (
GET /api/models/:id/resolve/:symbol). - Reads by other cells (a downstream eager cell that references
A2triggersA2to 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
A4 += 5 # A4 = A4 + 5
A4 -= 5
A4 *= 2
A4 /= 2These work only on single-cell targets. The right-hand side is evaluated against the current value of the target.
3. Targets
3.1 Single Cell
A1 = 10
$A$1 = 10
Sheet1!A1 = 10
'Q4 Revenue'!B2 = 10
Revenue = 10 # named reference3.2 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.3 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>::<type-tag> = <expression>A1 as currency = 100000
A2 as percentage = 21pct
A3 as date = d"2026-04-10"
A4 as bps = 25bps
A5 as score = ML_SCORE(features)The runtime validates that the produced value is kind-compatible
with the tag. If you tag as currency but the expression returns a
string, you'll get #TYPE!.
See reference.md for the full list of
tags and which kinds they map to.
4.1 Tags On Range Targets
The tag applies to every cell in the range:
A1:A10 as 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 as currency = 100 # A1 is currency
A1 = A1 + 1 # A1 is still currency
A1 as percentage = 0.5 # A1 is now percentage
A1 = "hello" # A1 is now plain string (tag removed)5. Input / Output Decorators
Two optional decorator keywords may appear at the start of an assignment:
input <target>[as <tag>] = <expression>
output <target>[as <tag>] <op> <expression>They label cells as part of the model's external write surface
(input) and read surface (output). They are advisory in
ordinary deployments, but in protected mode the runtime
strictly enforces the surface (see § 5.4).
5.1 input Decorator
input marks a cell as an externally-writable input. The runtime
API may set its value via setInput / setBatchInput; in protected
mode, no other cell may be written.
input price = 0
input qty as 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 cells have no initial value to act as a default |
input A1 ?= … |
Conditional cells skip on error — undefined initial state |
input A1 += 1 (and -=, *=, /=) |
Compound assigns require an existing value |
input A1 = … ONCE (and EVERY, AT) |
Scheduled cells are runtime-driven, not externally writable |
Inside WHEN … THEN … END rule actions |
Rule actions are internal model machinery; declare input on the top-level cell instead |
The default value (the right-hand side of =) is the value the cell
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 cell as an externally-readable output. The runtime
API may resolve it via resolve / getRange / getModelSnapshot;
in protected mode, no other cell may be read.
output total = price * qty
output history ~= EXPENSIVE_AGGREGATE(transactions)
output last_seen = NOW() ONCE
output running_log += eventoutput is permitted on every assignment shape — eager, lazy,
conditional, compound, scheduled, ranges, destructuring, and
rule-action targets. Use it liberally to declare the API you are
exposing.
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 Protected Mode
A deployment opts in to protected mode by passing
protectedMode: true to deployModel (the in-memory TS runtime),
or by setting protectedMode: true on the deploy request that the
backend GridService forwards to the Lua runtimes.
When enabled:
- Writes —
setInput/setBatchInputreject any symbol that is not inmodel.declaredInputs. - Reads —
resolve/getRange/getModelSnapshotreject any symbol that is not inmodel.declaredOutputs.getRangerejects the entire range if any one cell is undeclared. - Deploy-time validation —
deployModelrunsvalidateProtectedModeland refuses the deployment when:- Any implicit input (a cell referenced but never assigned in
the source) is missing a matching
inputdeclaration. There must be no hidden write surface. - The model declares no
inputand nooutput. A protected deployment with no surface is almost certainly a mistake.
- Any implicit input (a cell referenced but never assigned in
the source) is missing a matching
Outside protected mode, the decorators are still parsed and
preserved — they appear on model.declaredInputs,
model.declaredOutputs, and on every snapshot — but the runtime
does not enforce access control.
5.5 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` decoratorTo attach input to a counter you intend to increment from rules,
declare the input separately:
input counter = 0 # external surface
WHEN trigger > 0 THEN
output counter += 1 # rule mutation, exposed as read
END6. Schedule Modifiers (TS Runtime)
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 by the parser:
| Form | v1 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 |
Compute exactly once at deploy/build time and never recompute. Useful for capturing a snapshot value (e.g. B1 = NOW() ONCE). The value persists across redeploys when the expression is unchanged; if a deploy changes the expression, the cell recomputes. |
6.1 Restrictions
Schedule modifiers attach only to eager = assignments. The code
path that detects modifiers explicitly rejects:
~=lazy assignments?=conditional eager assignments+=/-=/*=//=compound assignments
A model with A1 ~= expr EVERY ... builds with the error
Scheduled assignment modifiers support eager '=' assignments only.
A model with A1 ~= expr ONCE builds with the error
The ONCE modifier supports eager \=` assignments only.`
6.2 Runtime parity
Schedule modifiers — including ONCE, EVERY, and AT — run on
every runtime. Both Lua-backed runtimes (lua_generated,
lua_interpreter) compile the modifier expressions and rely on the
TS GridService to drive the schedule wakeup. Behavior matches the
in-process ts runtime exactly. The canonical style prefers explicit
rule blocks for shared models; modifiers are a convenience for short
single-action cases.
6.3 ONCE semantics
ONCE cells follow these rules:
- The expression evaluates at deploy time (during
deployModel), alongside other eager calculations. - After the first successful evaluation, invalidations from upstream inputs or calculations do not trigger recomputation. The cell stays at its captured value.
- On
deployModelof an updated source, the runtime preserves the priorONCEvalue when 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. Owner Symbols (Compiler-Synthesized)
When the compiler lowers nested external calls, it generates
synthetic targets like __ext__:A1:9f23c8. These are internal-only
and not user-addressable.
You will see them in:
- API responses for status / job inspection.
- Generated Lua source.
- Audit / observability streams.
You should never write a synthetic symbol in your own assignments.
8. Restrictions Summary
| Constraint | Where it applies |
|---|---|
| Range targets must be finite rectangles | Top-level and rule-action assignments |
Compound += / -= / *= / /= requires single-cell target |
All contexts |
Named-argument binding := is for function calls, not assignments |
All contexts |
Inside rule actions, =, ?=, and compound (+=/-=/*=//=) are allowed; ~= and nested rules are rejected |
Rule action body |
Compound RHS may not contain external_async calls (would re-trigger every wave) |
Rule action body |
| Sheet/workbook qualifiers 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 decorator is permitted on any assignment shape |
All contexts |
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 sheet qualifier
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 a single-cell 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 cell depends on its row/column index, prefer an array
comprehension — it lowers to a single MAKEARRAY cell 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 as currency = "hello" # #TYPE! — string can't be currency
A1 as currency = "100" # OK — string coerces to number
A1 as currency = 100 # canonical9.5 Cycle
A1 = A2 + 1
A2 = A1 + 1 # both cells become #CIRC!Cycles are detected at model build time and reported as #CIRC!.
Lazy cells (~=) participate in cycle detection.
10. Examples
A complete, well-typed model:
MODEL "Pricing"
RUNTIME "lua_generated"
VERSION "1.0.0"
# Declared inputs (writable surface in protected mode)
input A1 as currency = 100000 # base price
input A2 as percentage = 7pct # discount
input A3 as percentage = 21pct # tax rate
# Declared outputs (readable surface in protected mode)
output B1 as currency = A1 * (1 - A2) # discounted price
output B2 as 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 as currency ?= B2 * C1
# Internal counter (not part of the external surface)
E1 = 0
E1 += 1 # E1 → 1
# 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. See Also
reference.mdfor the type system.rules-and-schedules.mdfor rule blocks.external-functions.mdfor~=deep dive.errors.mdfor#TYPE!,#CIRC!, etc.