---
title: "Quick reference"
description: "The Indicator kit on one screen, then the parts of it that are easiest to forget: the four exports, the generated accessors (the p_ / in_ / out_ families plus…"
order: 92
section: "reference"
---

<!-- source: docs/indicators/reference/quick-reference.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Quick reference

The Indicator kit on one screen, then the parts of it that are easiest to
forget: the four exports, the generated accessors (the `p_` / `in_` /
`out_` families plus the cell and string families), every declaration
signature, the sources and their fields, the TA classes, and the
`om wrun` commands. The reference page of kScript (legacy) lists builtins
you call; this page lists what an Indicator declares, because an Indicator
says what it reads and writes and the host does the calling.

## One screen

### The four exports

| Export | Runs | Do this here | Never here |
| --- | --- | --- | --- |
| `init(): void` | once per evaluation, before any bar | read params through `p_<param>()`, size buffers, construct TA objects | read an input (the `in_` accessors trap outside `state()`) |
| `state(): i32` | once per bar, oldest first | read inputs through `in_<input>()` and the cell accessors, update module state, return `1` (row ready) or `0` (abstain) | write an output or a string slot |
| `finalize(): void` | after each bar whose `state()` returned `1` | write every output through `out_<output>(value)`, write string slots, then `emitRow()` LAST | read an input |
| `reset(): void` | when the host replays the forming bar | reassign every module-level variable, call `.reset()` on every TA object | allocate |

### Accessors (`src/gen/`, regenerated before every build)

| Accessor | Phase | Meaning |
| --- | --- | --- |
| `p_<param>(): f64` | `init()` | the param's value: its default, or the setting the chart user chose |
| `in_<input>(): f64` | `state()` | this bar's scalar input; NaN on a celled input's slot |
| `out_<output>(value: f64): void` | `finalize()` | writes one output; `emitRow(): void` commits the row |
| `in_<input>_cells(): i32` | `state()` | f64 cells in this bar's block: `0` for a present empty block, `-1` when the bar carries no block |
| `in_<input>_read(ptr: i32): i32` | `state()` | copies the block into module memory at `ptr`; returns bytes written, `0`, or `-1` |
| `in_<input>_max_cells: i32` | constant | the declared `max_cells`, in source tuples |
| `in_<input>_capacity: i32` | constant | the f64 count to preallocate (`max_cells` x the class's tuple width) |
| `sb_clear()`, `sb_text(s)`, `sb_int(n)`, `sb_f64(x, decimals)` | `finalize()` | build one UTF-8 line into a shared buffer, allocation-free |
| `str_<slot>(s: string)`, `str_<slot>_sb()` | `finalize()` | send a whole string, or the built line, to one slot |

### Declarations (`./sdk/declare`, top-level statements of `src/indicator.ts`)

| Declaration | Signature |
| --- | --- |
| Param | `param(name, default, { required?, min?, max?, description? })` |
| Input | `input(name, <source>.<field>, { symbol?, exchange?, interval?, side?, tenor?, token?, outcome?, binding?, missing?, description? })` |
| Celled input | `input(name, <class>.cells, { max_cells, block_size?, max_depth?, symbol?, exchange?, description? })` |
| Output | `output(name, plot?, panel?, { description?, unit?, color?, colors?, width?, opacity?, line_style?, color_by?, shape_where?, displacement_bars?, width_by?, widths? })`, returns an `OutputHandle` |
| Range | `range(upper, lower, { color?, colors?, color_by?, edge_width?, edge_line_style?, smooth? })` |
| Box | `box(name, { top, bottom, from?, to?, when?, panel?, color?, borderColor?, opacity?, borderWidth? })` |
| Segment | `segment(name, { yFrom, yTo, from?, to?, when?, panel?, color?, width?, lineStyle? })` |
| String slot | `string(name, { max_bytes, description? })` |
| Renderers | `render.text(name, { y, text, color?, size? })`, `render.label(name, { x, y, text, color?, size? })`, `render.table(name, { rows, cols, cells, position? })`, `render.shape(name, { output, shape, where? })`, `render.stats_row(name, { output, title?, format?, polarity? })`, `render.bgcolor(name, { where, color?, color_by?, colors? })` |
| Drawings | `draw.line(name, { x1, y1, x2, y2, color?, width?, line_style? })`, `draw.box(name, { left, top, right, bottom, color? })`, `draw.polyline(name, { points, color?, width?, line_style? })`, `draw.label(name, { x, y, text, color? })` |

Vocabulary the declarations take:

| Slot | Values |
| --- | --- |
| `plot` | `line`, `bar`, `area`, `histogram`, `candle`, `shape`, `scatter`, `none` (data-only: computed, never drawn) |
| `panel` | `overlay` (the price pane), `lower` (its own pane) |
| `line_style` / `lineStyle` / `edge_line_style` | `solid`, `dashed`, `dotted` |
| `shape` (renderer) | `circle`, `cross`, `triangle_up`, `triangle_down`, `diamond`, `arrow_up`, `arrow_down`, `flag`, `square` |
| `missing` | `carry` (default on a secondary), `nan`, `zero`; on the primary input `nan` and `zero` densify the grid |
| `side` | `BUY`, `SELL` |
| `tenor` | `ONE_D`, `THREE_D`, `ONE_W`, `ONE_M`, `TWO_M`, `THREE_M`, `SIX_M`, `ONE_Y` |
| `outcome` | `YES`, `NO` (`NO` on the `close` field only) |
| `binding` | `required` (the market arrives per use, never together with `symbol`) |
| Box and segment offsets | integer literals in -500..500, or an output handle whose per-bar value truncates to the offset |
| Colors | any string on outputs and segments; a box fill needs hex, `rgb()`, or `hsl()` |

### Sources and fields

| Source | Fields | Knobs |
| --- | --- | --- |
| `ohlcv` | `open`, `high`, `low`, `close`, `volume` | pins only |
| `trades` | `open`, `high`, `low`, `close`, `volume` | `side` REQUIRED |
| `funding` | `rate_open`, `rate_high`, `rate_low`, `rate_close`, `predicted_open`, `predicted_high`, `predicted_low`, `predicted_close` | pins only |
| `oi` | `open`, `high`, `low`, `close` | pins only |
| `liquidations` | `liquidations` | `side` optional (absent = both sides summed); sparse, so a primary usually declares `missing: "zero"` |
| `implied_volatility` | `implied_volatility` | `tenor` REQUIRED |
| `skew` | `skew` | `tenor` REQUIRED |
| `token_supply` | `marketcap`, `first_marketcap`, `marketcap_dominance_percent`, `circulating_supply`, `total_supply`, `max_supply`, `total_value_locked`, `fully_diluted_valuation`, `cg_marketcap_rank`, `total_volume`, `usd_price` | `token` REQUIRED |
| `odds` | `open`, `high`, `low`, `close` (default), `volume` | conditionId as `symbol` plus `outcome`, or `binding: "required"`; `exchange` is implicit and refused |
| `time` | `bar_open_sec` | no knobs, never the primary input |
| `metric` (sheet only) | `metric: "wrun/@scope/name/output"`, `params` | no pins; refused when pinned coarser than the primary |

Pins: `symbol` and `exchange` go together (a lone half is refused);
`interval` pins alone and is read as of the coarser candle's close. The
chart reads its own market for every input; your machine honors pins.

### Celled classes (`abi_version: "wrun-2"`)

| Class | Tuple | Width | Knobs | Served by the daemon |
| --- | --- | --- | --- | --- |
| `volume_profile` | `[low, high, buy, sell]` per price bucket | 4 f64 | optional `symbol` + `exchange` | yes |
| `book` | `[price, size, side]` per level, side `+1` bid / `-1` ask, bids descending then asks ascending | 3 f64 | `block_size` REQUIRED (`om block-sizes`), optional `max_depth`, optional pin pair | yes |
| `tape` | `[offsetMs, price, size, side]` per live print, side `+1` BUY / `-1` SELL | 4 f64 | `min_size` REQUIRED, `history: "0"` default, optional pin pair | live buffer only |
| `trade_volume_by_size` | declared for cross-host parity | | none | no: refused as `wrun_cells_unavailable` |

### The TA classes (`./sdk/ta`)

One class per kScript (legacy) TA builtin, every one checked bar for bar
against the kScript engine. Construct in `init()`, `.update(...)` once per
bar in `state()` (it returns the primary value, `NaN` until warm),
`.reset()` from `reset()`. `update(x)` takes one value unless noted.

| Group | Classes | Per bar |
| --- | --- | --- |
| Averages | `Sma`, `Ema`, `Rma`, `Wma`, `Hma`, `Alma`, `Swma`, `Linreg` | `update(x)`; `Vwma` is `update(x, volume)` |
| Statistics and series | `Sum`, `Median`, `Percentile`, `Variance`, `Stdev`, `Zscore`, `Change`, `Mom`, `Roc`, `Cum`, `Fixnan` | `update(x)`; `Correlation` is `update(a, b)` |
| Oscillators | `Rsi`, `Cmo`, `Tsi`, `Macd` | `update(x)` |
| Oscillators over OHLC | `Cci`, `Wpr`, `Stoch`, `Stochastic` | `update(high, low, close)`; `Mfi` is `update(high, low, close, volume)`; `Obv` is `update(close, volume)` |
| Ranges and bands | `Tr`, `Atr` | `update(high, low, close)`; `Bb` is `update(x)`, `Keltner` is `update(x, high, low, close)`, `Donchian` is `update(high, low)` |
| Window extremes | `Highest`, `Lowest`, `HighestBars`, `LowestBars` | `update(x)` (pass the high or the low, the column kScript reads by default) |
| Trend systems | `Adx`, `Ichimoku`, `Psar`, `Supertrend` | `update(high, low, close)`; `Vwap` is `update(open, high, low, close, volume, tsMs)`, `tsMs` the bar's open time in milliseconds |
| Events | `Rising`, `Falling`, `PivotHigh`, `PivotLow` | `update(x)`; `ValueWhen` is `update(condition, x)`, `BarsSince` is `update(condition)`, `Cross` is `update(a, b): i32` (`+1` up, `-1` down, `0` otherwise) |

Multi-output classes return the primary line and carry the rest as
fields: `Bb`, `Keltner`, `Donchian` (`basis`, `upper`, `lower`); `Macd`
(`macd`, `signal`, `hist`); `Stoch`, `Stochastic` (`k`, `d`);
`Supertrend` (`line`, `direction`); `Adx` (`adx`, `plusDi`, `minusDi`);
`Ichimoku` (`tenkan`, `kijun`, `senkouA`, `senkouB`, `chikou`);
`Highest`, `Lowest` (`bars`, how many bars ago); `HighestBars`,
`LowestBars` (`value`, the matching extreme).

Every class allocates in its constructor and restores its just-constructed
state on `.reset()`. Two honest exceptions to bit-exactness: `Ichimoku`'s
`chikou` is the current close (the engine reads a future bar), and `Vwap`
anchors other than none, `"day"` and a millisecond bucket are unproven.
The composite and every convention are in
[TA library](../functions/ta-library.md).

### Commands

```bash
om wrun templates --format text
om wrun create @you/my-indicator ./my-indicator --template sma-codefirst
om wrun build ./my-indicator
om wrun validate ./my-indicator
om wrun install ./my-indicator --replace
om wrun list --format text
om wrun show @you/my-indicator
om metric get --metric wrun/@you/my-indicator/sma:period=20 --symbol BTCUSDT --exchange BINANCE_FUTURES
om metric series --metric wrun/@you/my-indicator/sma --params period=20 --symbol BTCUSDT --exchange BINANCE_FUTURES --bars 60
om chart indicator preview --type wrun/@you/my-indicator/sma --bars 300
om wrun source search polymarket "fed rate decision"
om watch publish ./my-indicator --dry-run
om install @you/my-indicator
om wrun upgrade @you/my-indicator --version 0.2.0
om wrun remove @you/my-indicator
```

| Command | What it does |
| --- | --- |
| `om wrun templates` | lists the nine starting points: `sma-codefirst`, `sma-box`, `sma`, `vp-buy-share-codefirst`, `polymarket-odds`, `conviction-score`, `event-asset-divergence`, `escalation-risk`, `hud-terminal` |
| `om wrun create <package> [dir]` | scaffolds a workspace (`--template`, `--force`); `om wrun scaffold <dir> --name` is the same verb with the name as a flag |
| `om wrun build <dir>` | derives the sheet from declarations, regenerates `src/gen`, runs `bun install` once and `bun run build`, checks the module against the contract, exports `dist/package` (`--out`, `--wasm`, `--compile-command`, `--install`, `--replace`, `--no-install-deps`) |
| `om wrun install <dir>` | the build plus the local install under `~/.openmarket/packages` |
| `om wrun export <dir>` | the export step alone (`--force`, `--install`) |
| `om wrun validate <dir>` | validates a workspace or an exported package without building |
| `om wrun list`, `om wrun show <id>` | installed packages and their outputs; one package or metric id |
| `om wrun source search <provider> <query>`, `om wrun source set <name>` | find a Polymarket conditionId; repoint a pinned odds input (refused on a derived sheet and on a bindable input) |
| `om wrun upgrade <package>` | moves saved alert locks onto an installed version (`--version`) |
| `om wrun remove <package>` | uninstalls (`--version` for one version); warns when alerts still reference the package |
| `om metric get`, `om metric series` | the newest value; one `[barOpenSec, value]` pair per bar (`--bars` 1..500, default 30) |
| `om chart indicator preview` | draws one locally installed output on your active chart, published or not |
| `om watch publish <dir>` | publishes a package directory to the registry (`--dry-run` prints what would ship) |
| `om install`, `om search --kind wrun-indicator`, `om packages delete`, `om access` | consumers, discovery, deleting a package you publish, private-package grants |

### Caps

16 boxes, 16 segments, 32 renderers, 64 drawings, 64 polyline points, 64
string slots of at most 4096 bytes, 64 KiB of strings per row, 8 MiB per
run, 2 MiB of expanded render result, offsets in -500..500, 4 MiB of
module memory. Every number, with the refusal it produces, is in
[Limits](limits.md).

## The four exports, exactly

The module contract is exact and the build refuses deviations statically,
naming the found and required signature:

- `init(): void` runs once per evaluation, before any bar. Read params
  here through `p_<param>()`.
- `state(): i32` runs once per bar. Read that bar's inputs through
  `in_<input>()`. Return `1` when a row is ready, `0` during warm-up.
  Modules on the second ABI read celled inputs here too: the cell
  accessors trap outside `state()`.
- `finalize(): void` runs after each ready bar: write every output through
  `out_<output>(value)`, write any string slots, then commit the row with
  `emitRow()` LAST.
- `reset(): void` clears ALL module state: reassign every module-level
  variable and call `.reset()` on every TA object. The runtime calls it
  for live same-bar replay, so a stale field here corrupts re-evaluated
  bars.

Redesigned signatures (`init(args: Array<f64>)`, a `finalize` that returns
the value) DO compile under AssemblyScript; the build's static ABI check
refuses the module afterwards, so keep the contract and let values flow
through the accessors. A row whose `state()` returned `0` has no
`finalize()` call, no drawn point, and no metric value
([Execution model](../core-concepts/execution-model.md)).

## Generated accessors

The runtime contract is positional; the accessors are how your source
stays name-attached. One function per declared name, regenerated from the
sheet before every build:

- `p_<param>(): f64` (params, read in `init`)
- `in_<input>(): f64` (inputs, read in `state`)
- `out_<output>(value: f64): void` plus `emitRow(): void` (outputs,
  written in `finalize`)

A declared name becomes its accessor by lowercasing it and collapsing
every run of characters outside `[a-z0-9_]` to one `_`: param `fast.len`
becomes `p_fast_len()`, input `btc-close` becomes `in_btc_close()`, output
`BTC-Ratio` becomes `out_btc_ratio()`. Param and input names are lowercase
in the sheet (`[a-z0-9][a-z0-9._-]*`); output names may carry capitals
(`[A-Za-z0-9][A-Za-z0-9._-]*`) and lose them in the accessor. Two names in
one family that escape identically are refused at generation, and
duplicate param names are refused at the schema. Every accessor family
gets the same treatment: a string slot `summary` sends through
`str_summary()` and `str_summary_sb()`, a celled input `profile` reads
through `in_profile_cells()` and `in_profile_read()`.

A sheet whose names need escaping, and the source that reads them:

```json
{
  "id": "ref-accessors",
  "abi_version": "wrun-1",
  "warmup_bars": 26,
  "params": [
    { "name": "fast.len", "default": 12, "min": 1, "max": 200 },
    { "name": "slow.len", "default": 26, "min": 2, "max": 400 }
  ],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "btc-close": { "source": "ohlcv", "field": "close", "symbol": "BTCUSDT", "exchange": "BINANCE_FUTURES" }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "btc-close", "description": "Fixed BTC reference market" }
  ],
  "outputs": [
    { "index": 0, "name": "spread", "plot": "line", "panel": "lower" },
    { "index": 1, "name": "BTC-Ratio", "plot": "line", "panel": "lower" }
  ]
}
```

```typescript
import { in_btc_close, in_close } from "./gen/inputs";
import { emitRow, out_btc_ratio, out_spread } from "./gen/outputs";
import { p_fast_len, p_slow_len } from "./gen/params";
import { Ema } from "./sdk/ta";

let fast = new Ema(12);
let slow = new Ema(26);
let spread: f64 = NaN;
let ratio: f64 = NaN;

export function init(): void {
  fast = new Ema(i32(p_fast_len()));
  slow = new Ema(i32(p_slow_len()));
}

export function state(): i32 {
  const close = in_close();
  const btc = in_btc_close();
  spread = fast.update(close) - slow.update(close);
  ratio = btc > 0.0 ? close / btc : NaN;
  return isNaN(spread) || isNaN(ratio) ? 0 : 1;
}

export function finalize(): void {
  out_spread(spread);
  out_btc_ratio(ratio);
  emitRow();
}

export function reset(): void {
  fast.reset();
  slow.reset();
  spread = NaN;
  ratio = NaN;
}
```

Editing the sheet moves the SLOT inside each accessor while your source
keeps the NAME, so reordering params or inputs never changes what the
module computes; a renamed declaration renames its accessor, and the
compiler then points at every stale import.

Raw positional slot literals (`getFloat(0)`, `getInt(0)`, `setOutput(0, ...)`, `wrun_arg_f64(0)`, `wrun_output_f64(0, ...)`) fail a scaffold build
BEFORE the compiler runs, in every file under `src/` except `src/sdk` and
`src/gen`, because slots silently rebind when the sheet changes. Variable
indexes stay legal: `src/sdk/sdk.ts` wraps the raw host imports for
genuinely dynamic access. Builds that bypass the scaffold (`--wasm`,
`--compile-command`) skip the lint and are exported with `positionalAbi: true`: their params and inputs bind by metadata POSITION, so never reorder
such a package's metadata entries.

## Declarations, in one place

The signatures in the table above are the whole code-first grammar; the
rules the extractor enforces, each as a named build error:

- Names are string literals; defaults and option values are literals; the
  code never runs at build time.
- Declarations are top-level statements of `src/indicator.ts` only; one
  anywhere else names the file and line.
- Indexes follow declaration order: the first `input(...)` is slot 0 (the
  primary input, which sets the request grid every other input aligns
  to), and reordering declarations reorders slots while the generated
  accessors keep your source name-attached.
- Box and segment coordinates are output handles bound by a top-level
  `const` (`let`, `var`, and `export const` bind too; a handle may be
  bound below the shape that uses it); the sheet records the output's
  name, never the handle.
- `color_by`, `width_by`, and `shape_where` name a DIFFERENT declared
  output; an output cannot color, widen, or gate itself. `colors` needs
  at least two entries beside `color_by`; `widths` and `width_by` go
  together.
- A celled input (`<class>.cells`) requires `max_cells`; scalar-feed knobs
  (`interval`, `side`, `tenor`, ...) are refused on it; `book` requires
  `block_size`.

Declaring a string slot, a renderer, a drawing, or a celled input stamps
the derived sheet `abi_version: "wrun-2"`; scalar-only declarations keep
deriving the first ABI. `range`, `box`, and `segment` are ABI-neutral:
they never flip the sheet. The derived sheet records `generated_from: "declarations"` plus a `source_digest`, and is DERIVED state from then on:
`om wrun source set` refuses it with `wrun_source_set_generated`, and the
agent's metadata argument refuses with `wrun_metadata_generated`. A
workspace with no declarations stays metadata-first, the only mode for
languages without an extractor. The field-by-field sheet dialect, pins,
odds modes, and metric composition are in
[Script definition](../functions/script-definition.md); the styling
ladders, `fills`, and style knobs (hand-written sheets only) are in
[Styling](../functions/styling.md).

## The cell channel (`wrun-2`)

Modules that declare `abi_version: "wrun-2"` may read celled inputs: a
variable-length block of f64 cells per bar (a volume profile's per-price
rows, a book snapshot's levels) beside the scalar argument block. A celled
input is declared in the sheet (`cellType: "array"` + `max_cells`) or in
the source (`input(name, <class>.cells, { max_cells })`), reads a celled
source class ([Data sources](../core-concepts/data-sources.md)), and
receives cells in fixed-width TUPLES per class: `volume_profile` =
`[low, high, buy, sell]` (4 f64s), `book` = `[price, size, side]` (3
f64s). `max_cells` counts tuples.

The scaffold generates one accessor family per celled input (no scalar
accessor: the input's slot in the scalar block holds NaN), callable during
`state()` only:

- `in_<input>_cells(): i32`: f64 cells in this bar's block (tuples x tuple
  width); `0` for a present, empty block; `-1` when the bar carries no
  block.
- `in_<input>_read(ptr: i32): i32`: copies the block into the module's
  exported memory at `ptr`; returns bytes written, `0` for a zero-cell
  block, `-1` for a missing block.
- `in_<input>_max_cells: i32`: the declared `max_cells` (source tuples).
- `in_<input>_capacity: i32`: the f64 count to preallocate (`max_cells` x
  the class's tuple width); generated when the class's width is known.

Cells are IEEE 754 f64, little-endian, 8 bytes each, contiguous in block
order. `max_cells` is a contract, not a hint: preallocate
`in_<input>_capacity` f64s (a `StaticArray<f64>` sized by it allocates
once at module start), and a bar whose block exceeds the cap refuses the
WHOLE evaluation by name (`wrun_cell_block_too_large`; a block is never
truncated). A call outside `state()` traps by name, an out-of-bounds `ptr`
is refused naming the sizes, and a read of an input not declared
`cellType: "array"` is refused naming the declared set. Underneath the
accessors sit two host imports in the `wrun` namespace, `wrun_arg_len(index)`
and `wrun_arg_bytes(index, ptr)`; positional literals on them are a
scaffold build error exactly like `getFloat(0)`, so scaffold source goes
through the accessors and only `--wasm` / `--compile-command` builds call
the imports directly. A second-ABI module that reads no celled input is
legal: such packages may be scalar-only, and scalar behavior is
bit-identical across every contract.

Alignment is an exact join, never a fill: each primary-grid row gets the
celled observation with the same bar open, a bar with no observation gets a
PRESENT EMPTY block (the module sees `0` cells, not `-1`), and celled
values are never carried forward (a replayed block would double-count
volume). Celled inputs never abstain a row: warm-up stays the module's
`state()` decision. A celled class can never be the primary input, may pin
`symbol` + `exchange` together, and never pins `interval`.

Summing a bar's volume profile (total volume = buy + sell in every
`[low, high, buy, sell]` tuple):

```json
{
  "id": "ref-profile-sum",
  "abi_version": "wrun-2",
  "warmup_bars": 1,
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "profile": { "source": "volume_profile" }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "profile", "cellType": "array", "max_cells": 512 }
  ],
  "outputs": [{ "index": 0, "name": "total", "plot": "line", "panel": "lower" }]
}
```

```typescript
import { in_close, in_profile_capacity, in_profile_cells, in_profile_read } from "./gen/inputs";
import { emitRow, out_total } from "./gen/outputs";

const cells = new StaticArray<f64>(in_profile_capacity);
let total: f64 = NaN;

export function init(): void {}

export function state(): i32 {
  in_close(); // the scalar block still carries every scalar input
  const n = in_profile_cells();
  if (n < 0) return 0; // this bar carries no block
  total = 0;
  if (n > 0) {
    in_profile_read(i32(changetype<usize>(cells)));
    for (let i = 0; i + 3 < n; i += 4) {
      total += cells[i + 2] + cells[i + 3]; // buy + sell per [low, high, buy, sell] tuple
    }
  }
  return 1;
}

export function finalize(): void {
  out_total(total);
  emitRow();
}

export function reset(): void {
  total = NaN;
}
```

The first ABI is frozen: its import allowlist never grows, and a `wrun-1`
package that imports the cell functions is refused naming the way in
(declare `abi_version: "wrun-2"`). Which celled classes this daemon's data
plane serves, and the named refusals for the rest, are in
[Data sources](../core-concepts/data-sources.md). Where celled packages
run: alerts, `om metric get` / `om metric series`, and chart previews
evaluate them; backtests and screens refuse them by name
(`wrun_celled_metric_unsupported`).

## The string channel (`wrun-2`)

Second-ABI sheets may declare `string_slots`: numbered, named, byte-capped
text channels written once per ready bar during `finalize()`. Slots are
NOT outputs: `outputs` stays numeric-only and every output is still a
metric; strings exist so renderers and drawing labels can carry text, and
they never become metrics.

When the sheet declares slots (or the source declares `string(...)`), the
scaffold generates `src/gen/strings.ts`:

- `sb_clear()`, `sb_text(s)`, `sb_int(value)`, `sb_f64(value, decimals)`
  build one UTF-8 line allocation-free into a shared buffer sized to the
  largest declared `max_bytes` (allocated once at module start, so per-bar
  string work allocates nothing).
- `str_<slot>(s: string)` encodes and sends `s` to that slot;
  `str_<slot>_sb()` sends the built line. Both pass the REQUIRED byte
  count, so a line over the slot's `max_bytes` refuses host-side by name;
  strings are never truncated.

A slot not written that bar is ABSENT, which is distinct from a written
empty string: `render.text` draws every present slot (empty included),
`render.label` keeps the last present NONEMPTY one, and `render.table`
waits for a row where every cell is present. Writing the same slot twice
in one `finalize()` replaces its bytes. The channel is `finalize()`-only:
the underlying import (`wrun_output_str(slot, ptr, len)`, `wrun`
namespace) traps by phase anywhere else, and positional literals on it are
the same scaffold build error as the cell imports (use the generated
senders). Host-enforced caps, each a named refusal: `max_bytes` per slot
(at most 4096), 64 slots per package, 64 KiB of string bytes per row,
8 MiB per run. Bytes must be valid UTF-8; a broken sequence refuses the
evaluation instead of landing as a replacement character.

An RSI with two slots: a readout the label renderer keeps on the newest
bar, and a zone word written only on bars that are in a zone, so the text
mark stays absent everywhere else:

```typescript
import { input, line, lower, none, ohlcv, output, param, render, string, time } from "./sdk/declare";
import { in_bar_t, in_close } from "./gen/inputs";
import { emitRow, out_rsi, out_tag_x } from "./gen/outputs";
import { p_period } from "./gen/params";
import { sb_clear, sb_f64, sb_int, sb_text, str_readout_sb, str_zone } from "./gen/strings";
import { Rsi } from "./sdk/ta";

param("period", 14, { min: 2, max: 200 });
input("close", ohlcv.close);
input("bar_t", time.bar_open_sec);
output("rsi", line, lower, { color: "#38bdf8" });
output("tag_x", none, lower, { description: "Bar time in epoch seconds, the label's x" });
string("readout", { max_bytes: 32 });
string("zone", { max_bytes: 16 });
// One label, riding the newest bar whose readout slot was written.
render.label("rsi_tag", { x: "tag_x", y: "rsi", text: "readout", color: "#38bdf8", size: 11 });
// One text mark per bar whose zone slot was written; quiet bars leave it absent.
render.text("rsi_zone", { y: "rsi", text: "zone", color: "#f59e0b", size: 10 });

let rsi = new Rsi(14);
let period: i32 = 14;
let value: f64 = NaN;
let barTime: f64 = NaN;

export function init(): void {
  period = i32(p_period());
  rsi = new Rsi(period);
}

export function state(): i32 {
  barTime = in_bar_t();
  value = rsi.update(in_close());
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  out_rsi(value);
  out_tag_x(barTime);
  sb_clear();
  sb_text("RSI ");
  sb_int(period);
  sb_text(": ");
  sb_f64(value, 1);
  str_readout_sb(); // "RSI 14: 63.2", at most 32 bytes
  if (value >= 70.0) str_zone("overbought");
  else if (value <= 30.0) str_zone("oversold");
  emitRow();
}

export function reset(): void {
  rsi.reset();
  value = NaN;
  barTime = NaN;
}
```

Renderers and drawings are selected after the run under one lifecycle both
hosts implement ([Plotting](../functions/plotting.md) and
[Drawing objects](../functions/drawing-objects.md)); the worked table
example in [Script definition](../functions/script-definition.md) writes
four slots per bar through this module surface.

## Language cheat

The file is AssemblyScript: TypeScript syntax over fixed-width numbers,
compiled to a module with no filesystem, no network, and no allocation
after `init()`. What the kScript (legacy) reference lists as data types,
math, strings, control flow, and user functions maps like this:

| Need | Form |
| --- | --- |
| Numbers | `f64` for every param, input, and output; `i32` for counts, periods, and loop bounds; `bool` for flags. Casts are explicit: `i32(p_period())`, `f64(count)`. |
| Missing value | `NaN`, tested with `isNaN(x)`; write NaN to an output for "nothing here", or `return 0` from `state()` to abstain the row. `Infinity` in an output is refused by name. |
| Windows and history | a `StaticArray<f64>` sized once (from a param's `max`) and written as a ring buffer; there is no `close[1]`, keep yesterday's value in a module-level variable. |
| Collections | `StaticArray<T>` and `Array<T>` allocated at module start or in `init()`, never in `state()`; `Map<K, V>` for keyed state, sized up front. |
| Math | `Math.abs`, `Math.max`, `Math.min`, `Math.sqrt`, `Math.pow`, `Math.exp`, `Math.log`, `Math.floor`, `Math.ceil`, `Math.round`, `Math.trunc`, `Math.sign`, the trigonometric set, `Math.PI`, `Math.E`, all over `f64`. |
| Strings | only in string slots; build with `sb_text` / `sb_int` / `sb_f64`, never `+` on a string per bar (that allocates). |
| Control flow | `if` / `else`, `for`, `while`, `break`, `continue`, `switch` on integers; a number is not a truth value (`if (value > 0.0)`, not `if (value)`). |
| Functions | `function name(x: f64, n: i32): f64 { ... }` at module scope, typed params and return; closures cannot capture locals, so a reducer is a named function over a buffer. |
| Types | `class Zone { top: f64 = NaN; bottom: f64 = NaN; alive: bool = false; }`, constructed in `init()`; a `type` alias for readability. |
| Time | `input("bar_t", time.bar_open_sec)` gives epoch seconds; sessions are integer math on it (UTC). |
| Colors | never computed: declared per output, box, segment, or renderer (hex, `rgb()`, `hsl()`, or a named color where a fill is not involved), or chosen per bar through a `color_by` ladder. |

A rolling highest-high that shows the shapes above (a buffer sized once, a
typed helper function, explicit casts):

```typescript
import { input, line, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_high } from "./gen/inputs";
import { emitRow, out_highest } from "./gen/outputs";
import { p_bars } from "./gen/params";

param("bars", 20, { min: 1, max: 500, description: "Lookback in bars" });
input("high", ohlcv.high);
output("highest", line, overlay, { color: "#38bdf8" });

const MAX_BARS: i32 = 500; // the param's max: the buffer is sized once, never per bar
const highs = new StaticArray<f64>(MAX_BARS);
let n: i32 = 20;
let cursor: i32 = 0;
let count: i32 = 0;
let highest: f64 = NaN;

// A plain function: typed params, a typed return, no closure over locals.
function maxOf(values: StaticArray<f64>, len: i32): f64 {
  let best = -Infinity;
  for (let i = 0; i < len; i++) {
    if (values[i] > best) best = values[i];
  }
  return best;
}

export function init(): void {
  n = i32(p_bars());
}

export function state(): i32 {
  highs[cursor] = in_high();
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  if (count < n) return 0;
  highest = maxOf(highs, n);
  return 1;
}

export function finalize(): void {
  out_highest(highest);
  emitRow();
}

export function reset(): void {
  cursor = 0;
  count = 0;
  highest = NaN;
}
```

## Sheet fields, by name

Where each top-level field of `wrun/metadata.json` is explained:

| Field | Meaning | Page |
| --- | --- | --- |
| `id`, `name`, `description`, `overlay` | the sheet's short name, display strings, the legacy placement hint | [Script definition](../functions/script-definition.md) |
| `abi_version` | `wrun-1` (absent means this) or `wrun-2`; the sole ABI authority | [Script definition](../functions/script-definition.md) |
| `wasm_sha256`, `warmup_bars` | the build stamp installs verify; the warm-up hint the fetch planner reads | [Script definition](../functions/script-definition.md) |
| `params[]` | `{name, default, required?, min?, max?, description?}`; a `style` binding makes a style-only knob | [Styling](../functions/styling.md) |
| `inputSources{}`, `inputs[]` | sources keyed by input name; `{index, name, description?, cellType?, max_cells?}` | [Data sources](../core-concepts/data-sources.md) |
| `outputs[]` | `{index, name, plot?, panel?, unit?, displacement_bars?, ...styling}` | [Plotting](../functions/plotting.md) |
| `fills[]`, `ranges[]` | shaded band between two rendered outputs; banded range with edges and a ladder | [Styling](../functions/styling.md) |
| `boxes[]`, `segments[]` | per-bar shapes over outputs (16 each) | [Drawing objects](../functions/drawing-objects.md) |
| `string_slots[]`, `renderers[]`, `drawings[]` | the second ABI's text and decoration vocabulary | [Script definition](../functions/script-definition.md) |
| `generated_from`, `source_digest` | provenance of a sheet derived from declarations | this page |

The full validation list, every refusal by path, is at the end of
[Script definition](../functions/script-definition.md); the messages you
will actually meet, with their fixes, are in
[Common errors](../faq/common-errors.md).
