---
title: "Script definition"
description: "Everything a kScript (legacy) define(), input(), source(), and print() call did, an Indicator declares: placement per output, settings as param(...), feeds as…"
order: 29
section: "functions"
---

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

# Script definition

Everything a kScript (legacy) `define()`, `input()`, `source()`, and
`print()` call did, an Indicator declares: placement per output, settings
as `param(...)`, feeds as `input(name, source.field)`, and a debug value
as a data-only output. The declarations at the top of `src/indicator.ts`
derive the sheet (`wrun/metadata.json`), and the sheet is what every
host, the registry, and the runtime read. This page is both halves: the
port of each kScript definition call, then the sheet dialect field by
field, because a hand-written sheet is still the wire format and the
only mode for languages without a declaration extractor.

## From define() to outputs

`define(title, position, showPriceAxis, customTitle, format, maxBarsBack)`
registered the script and placed it. Nothing in an Indicator registers a
script: the package manifest (`om-package.json`, `@scope/name`) names it,
and placement is per output.

| `define(...)` argument | Indicator |
| --- | --- |
| `title` | the package `name` in `om-package.json`; the sheet's `name` for listings and legends |
| `position="onchart"` / `"offchart"` | `output(name, plot, overlay)` or `output(name, plot, lower)`, per output; a package can draw in both panes |
| `showPriceAxis` | nothing to declare: a `lower` pane has its own axis |
| `customTitle=" ($period)"` | nothing yet; the legend shows the output `description` and the chart's settings show the params |
| `format="percentage"` | `unit: "%"` on the output (`price`, `%`, or a short label) |
| `maxBarsBack` | nothing: state lives in your variables; size buffers from a param's `max` |

`strategy(...)`: a package that declares it places its own orders through the engine's broker ([Strategies overview](../strategies/overview.md)), and one without it computes values only;
the nearest form is `om backtest run` on the published metric
([Strategy functions](strategy-functions.md)).

## From input() to param

`input(name, type, defaultValue, label, constraints, options, group)`
became `param(name, default, { required?, min?, max?, description? })`,
read once in `init()` through the generated `p_<name>()` accessor. Every
param is a number (`f64`); every param shows in the chart's settings for
the overlay. The types:

| kScript `type` | Indicator |
| --- | --- |
| `number`, `int`, `float`, `slider` | `param(name, default, { min, max, description })`; cast with `i32(...)` for a bar count |
| `boolean` | `param(name, 1, { min: 0, max: 1 })`, tested as `p_flag() > 0.5` |
| `select`, `multiSelect`, `string`, `text`, `color`, `color[]`, `source`, `timeframe`, `session`, `symbol` | **not in Indicators yet** as params; pins are declarations, colors are per-output options ([Typed inputs](typed-inputs.md)) |
| `constraints.step`, `group` | no counterpart; `label` is `description` |

## From source() to input

`source(src, symbol, exchange)` opened a feed and returned a series with
history. An Indicator names the FIELD it reads: `input(name,
source.field, options?)` declares one per-bar number, read in `state()`
through `in_<name>()`, and the host aligns every input to the primary
input's grid. There is no series object and no history array; keep the
values you need ([Execution model](../core-concepts/execution-model.md)).

| kScript | Indicator |
| --- | --- |
| `source("ohlcv", currentSymbol, currentExchange)` then `.close` | `input("close", ohlcv.close)`: an unpinned input follows the chart |
| `source("ohlcv", "BTCUSDT", "BINANCE")` | `input("btc_close", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES" })`, both pins together |
| `source("liquidations", ...)` | `input("liqs", liquidations.liquidations, { missing: "zero" })` |
| `source("funding_rate", ...)` | `input("funding", funding.rate_close)` |
| `source("buy_sell_volume", ...)`, `.buy` | `input("buy", trades.volume, { side: "BUY" })` |
| `orderbook(...)`, `volume_profile(...)` | `input("book", book.cells, { max_cells, block_size })`, `input("profile", volume_profile.cells, { max_cells })` |

The sources and every field are the [Data sources](../core-concepts/data-sources.md)
catalog; the options are `exchange`, `symbol`, `interval`, `outcome`,
`binding`, `tenor`, `side`, `token`, `missing`, `description`, and for
celled classes `max_cells`, `block_size`, `max_depth`.

## From print() to a debug output

`print()` and `printTimeSeries()` wrote to a script console. There is no
console: declare a data-only output (`none`) for the value you want to
see, write it in `finalize()`, and read it in the chart legend or with
`om metric series` on your machine. It costs nothing to draw and it is a
metric you can alert on, which is often what the print was for.

```typescript
import { funding, input, line, lower, none, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close, in_funding } from "./gen/inputs";
import { emitRow, out_debug_alpha, out_funding_pct, out_rsi, out_smoothed } from "./gen/outputs";
import { p_period, p_show_raw } from "./gen/params";
import { Ema, Rsi } from "./sdk/ta";

// define(title="RSI", position="offchart", axis=true, format="percentage") became one lower output with a unit.
param("period", 14, { min: 2, max: 200, description: "RSI Period" });
// input(type="boolean") became a 0/1 param.
param("show_raw", 1, { min: 0, max: 1, description: "Write the raw RSI as well as the smoothed one" });
// source("ohlcv", currentSymbol, currentExchange).close became one input per field read.
input("close", ohlcv.close);
// source("funding_rate", ...) became a funding input; it aligns to the close's grid.
input("funding", funding.rate_close, { description: "Funding rate, as of the bar" });
output("rsi", line, lower, { unit: "%", color: "#7c3aed", width: 2, description: "RSI (14)" });
output("smoothed", line, lower, { unit: "%", color: "#38bdf8", width: 1, description: "RSI smoothed by a 5-bar EMA" });
output("funding_pct", line, overlay, { unit: "%", color: "#f59e0b", description: "Funding as a percent, on the price pane" });
// print("alpha", ...) became a debug output: never drawn, readable in the legend and as a metric.
output("debug_alpha", none, lower, { description: "The EMA alpha in use" });

let rsi = new Rsi(14);
const ema = new Ema(5);
let showRaw: bool = true;
let value: f64 = NaN;
let smoothed: f64 = NaN;
let fundingRate: f64 = NaN;

export function init(): void {
  rsi = new Rsi(i32(p_period()));
  showRaw = p_show_raw() > 0.5;
}

export function state(): i32 {
  value = rsi.update(in_close());
  fundingRate = in_funding();
  smoothed = isNaN(value) ? NaN : ema.update(value);
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  out_rsi(showRaw ? value : NaN);
  out_smoothed(smoothed);
  out_funding_pct(fundingRate * 100.0);
  out_debug_alpha(2.0 / 6.0);
  emitRow();
}

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

A `0` in `show_raw` writes `NaN` to the raw line, which draws nothing:
that is what a boolean toggle on a plot ports to.

<!-- snippet: code-first -->
## The declaration grammar

Instead of hand-writing `wrun/metadata.json`, declare params, inputs, and
outputs as typed top-level statements of `src/indicator.ts`, imported
from `./sdk/declare`:

```typescript
import { input, line, lower, ohlcv, output, overlay, param } from "./sdk/declare";

param("period", 14, { min: 2, max: 200, description: "Lookback window" });
input("close", ohlcv.close);
input("btc_close", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES" });
output("value", line, lower, { unit: "score" });
```

The build extracts the declarations statically (the code never runs at
build time), derives the sheet, and generates the accessors from the
same in-memory object, so the two cannot disagree. The grammar is static
and literal-only:

- `param(name, default, options?)` with options `required`, `min`, `max`,
  `description`.
- `input(name, source.field, options?)` with options `exchange`, `symbol`,
  `interval` (`MINUTE`, `FIVE_MINUTES`, `FIFTEEN_MINUTES`,
  `THIRTY_MINUTES`, `HOUR`, `FOUR_HOURS`, `DAY`, `WEEK`), `outcome`,
  `binding`, `tenor`, `side`, `token`, `missing` (`"carry"`, `"nan"`, or
  `"zero"`; on the first input `"nan"`/`"zero"` densify the request
  grid), `description`. Sources are bare member references: `ohlcv`,
  `trades`, `funding`, `oi`, `liquidations`, `implied_volatility`, `skew`,
  `token_supply`, `odds`, `time` (metric composition inputs stay
  metadata-first and are refused by name).
- `output(name, plot?, panel?, options?)` with plots `line`, `bar`, `area`,
  `histogram`, `candle`, `shape`, `scatter`, `none` (data-only), panels
  `overlay`, `lower`, and options `description`, `unit`, `color`, `colors`
  (the color_by palette, an array of string literals), `width`, `opacity`,
  `line_style`, `color_by`, `shape_where`, `displacement_bars` (an integer
  in -500..500; negative literals such as `-26` are fine), `width_by`, and
  `widths` (the per-bar width ladder, an array of numeric literals;
  `width_by` and `widths` go together, like `color_by` and `colors`).
  `output(...)` returns a handle; bind it with a top-level `const` when a
  `box` or `segment` needs to name it.
- `range(upper, lower, options?)` declares a sheet-level band between two
  rendered outputs with options `color`, `colors`, `color_by`,
  `edge_width`, `edge_line_style`, `smooth` (presentation-plane like
  `fills`, and ABI-neutral: declaring one never flips the sheet to
  `"wrun-2"`). Ranges never dedupe: repeat the declaration for several
  bands, the same pair included (the sheet schema imposes no uniqueness).
- `box(name, options)` and `segment(name, options)` declare per-bar
  shapes over output HANDLES ([Drawing objects](drawing-objects.md)). Box
  options: `top`, `bottom` (handles, required), `from`, `to` (bar
  offsets: integer literals in -500..500 or handles, default 0), `when`
  (a gate handle), `panel` (`"overlay"` or `"lower"`), `color`,
  `borderColor`, `opacity`, `borderWidth`. Segment options: `yFrom`,
  `yTo` (handles, required), `from`, `to`, `when`, `panel`, `color`,
  `width`, `lineStyle`. The derived sheet records output NAMES under the
  snake_case fields (`x_from`, `x_to`, `border_color`, `border_width`,
  `y_from`, `y_to`, `line_style`). ABI-neutral like `range`. A handle
  that binds no `output(...)` declaration, a string literal where a
  handle goes, a non-integer offset literal, or a `panel` outside the two
  names is a named build error.

The second runtime contract's vocabulary has declaration forms too, and
deriving a sheet that uses any of them stamps `abi_version: "wrun-2"`
automatically (scalar-only declarations keep the first contract):

- Celled inputs: `input(name, <class>.cells, options)` with classes
  `volume_profile`, `book`, `trade_volume_by_size`. `max_cells` is
  REQUIRED; `block_size` (required on `book`) and `max_depth` are the
  book fetch facets; `symbol` + `exchange` pin together; `description`.
  Scalar-feed knobs (`interval`, `side`, ...) are refused on celled
  inputs.
- String slots: `string(name, { max_bytes, description? })`. The
  declaration is named `string`, which shadows the type name in a file
  that imports it; `import { string as slot }` keeps the type
  ([String functions](string-functions.md)).
- 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? })`. Numeric references name declared outputs; `text` and table
  `cells` name declared string slots.
- 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? })` where
  `points` is a flat `["x0", "y0", "x1", "y1", ...]` list of output-name
  pairs, `draw.label(name, { x, y, text, color? })`.

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), 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.

The derived sheet records `generated_from: "declarations"` plus a
`source_digest` (sha256 of the source), serializes canonically (an
unchanged source rewrites nothing), and is DERIVED state from then on:
`om wrun source set` refuses with `wrun_source_set_generated` and the
agent's metadata argument refuses with `wrun_metadata_generated`, both
pointing at the declaration to edit instead. A sheet claiming generated
provenance over a declaration-free source blocks the build naming both
ways out (restore the declarations, or delete `generated_from` and
`source_digest` to hand-edit again). A workspace with no declarations
stays metadata-first: byte-identical behavior to a hand-written sheet,
and the supported mode for languages without an extractor (Rust, Zig, a
pre-built module).

<!-- end snippet: code-first -->

## The sheet dialect

`wrun/metadata.json` (the sheet) declares what the module reads and
writes: params, inputs and their sources, outputs and their looks, and
the ABI the module runs under. It is validated at author, build, install,
and publish with the same schema, so a broken sheet fails early and names
the field. Every top-level field in one sheet:

```json
{
  "id": "context-skeleton",
  "name": "Context Skeleton",
  "description": "Every top-level field in one sheet.",
  "overlay": "offchart",
  "abi_version": "wrun-1",
  "warmup_bars": 3,
  "params": [
    { "name": "period", "default": 14, "required": false, "min": 2, "max": 200, "description": "Lookback bars" }
  ],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "btc_close": { "source": "ohlcv", "field": "close", "symbol": "BTCUSDT", "exchange": "BINANCE_FUTURES" },
    "funding_rate": { "source": "funding", "field": "rate_close" }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "btc_close", "description": "Fixed BTC reference market" },
    { "index": 2, "name": "funding_rate" }
  ],
  "outputs": [
    { "index": 0, "name": "value", "plot": "line", "panel": "lower", "unit": "score" },
    { "index": 1, "name": "raw", "plot": "" },
    { "index": 2, "name": "lagging", "plot": "line", "displacement_bars": -26 }
  ]
}
```

Field by field:

- `id`: the sheet's short name (`[a-z0-9][a-z0-9._-]*`). The installable
  package id comes from `om-package.json`'s `name` (`@scope/short-name`); the
  metric ids are `wrun/@scope/short-name/<output>`.
- `name`, `description`: display strings for listings and chart legends.
- `overlay`: `"onchart"` or `"offchart"`, a legacy default-placement hint;
  per-output `panel` is the modern control.
- `abi_version`: `"wrun-1"` or `"wrun-2"`; ABSENT means `"wrun-1"`. This
  field is the SOLE ABI authority (see below).
- `wasm_sha256`: stamped by the build/export; installs and every run verify
  the module against it.
- `warmup_bars`: a positive-integer hint for how many bars the module needs
  before its first ready row.
- `params`: an ARRAY of `{name, default, required?, min?, max?, options?,
  description?}` objects with unique names. Each compute param is read in
  `init()` through its `p_<name>()` accessor; underneath, values reach the
  module positionally in declaration order. A param with a `style` binding is
  a style-only knob: no accessor, its slot stays zero-filled, and its default
  may be a string ([Styling](styling.md)). Compute params need numeric defaults.
- `inputSources`: keyed BY INPUT NAME; every input must have a matching
  entry and vice versa. Each entry is a feed source (`source` + `field` +
  optional pins), a metric composition reference, or the `time` source
  ([Data sources](../core-concepts/data-sources.md) lists every source,
  field, and knob).
- `inputs`: `{index, name, description?}`, indexes contiguous from 0. Index 0
  is the PRIMARY input: it sets the request grid (symbol, exchange, interval)
  every other input aligns to. A `time` source cannot be primary.
- `outputs`: `{index, name, description?, plot?, panel?, unit?,
  displacement_bars?, ...styling}`, indexes contiguous from 0, unique names.
  `plot: ""` declares a data-only output: computed, never drawn
  ([Styling](styling.md) has the full vocabulary, including `fills` at the
  top level). `displacement_bars` (integer, -500..500, default 0) is a
  DECLARED display offset: the value emitted at bar i is displayed at bar
  `i + displacement_bars` (negative = drawn earlier/behind, the lagging
  span of an Ichimoku; positive = drawn ahead). The runtime never shifts
  rows: `om metric get` / `om metric series` return the value at the bar it
  was computed on, and the field rides the sheet unchanged through metric
  catalogs, installs, the code-first build, and the render selection
  (per-row renderer kinds carry their value output's offset), so chart
  hosts and the parity comparator apply the shift themselves. `om chart
  indicator preview` does not yet honor it: an output with a nonzero
  `displacement_bars` is refused by name
  (`wrun_preview_displacement_unsupported`) because the preview wire
  carries no displacement field and the line would render on the bar it
  was computed on. Preview an undisplaced output instead. The styling
  vocabulary (the `color_by`/`colors` and `width_by`/`widths` per-bar
  ladders included) and the top-level `fills`, `ranges`, `boxes`, and
  `segments` arrays are on the [Styling](styling.md) page; all four arrays
  are ABI-neutral and presentation-plane (chart hosts render them, scalar
  surfaces ignore them). `boxes` and `segments` have their own field list
  below.

Sheets on the second contract may additionally declare `string_slots`,
`renderers`, and `drawings` (their own section below); under the first
contract the three fields are refused, never ignored.

## Boxes and segments (per-bar shapes)

Two top-level arrays legal on both contracts alike: sheet-only (no new
wire records, the referenced outputs already stream) and evaluated on
EVERY bar row by chart hosts, ignored by scalar surfaces. The semantics
and worked examples are on the [Styling](styling.md) and
[Drawing objects](drawing-objects.md) pages; the fields:

- `boxes` (at most 16), each `{name, top, bottom, x_from?, x_to?, when?,
  panel?, color?, border_color?, opacity?, border_width?}`. `top` and
  `bottom` name declared outputs (data-only allowed); `x_from` / `x_to` are
  bar offsets, an integer in -500..500 or the name of an output whose
  truncated per-bar value is the offset (default `0`); `when` names a gate
  output (the bar is skipped unless the value is finite and nonzero);
  `panel` is `overlay` or `lower` (default: the panel of `top`'s output);
  `opacity` 0..1 (default 0.2); `border_width` 0..10 (default 1); `color`
  and `border_color` default to `top`'s output color.
- `segments` (at most 16), each `{name, y_from, y_to, x_from?, x_to?,
  when?, panel?, color?, width?, line_style?}`. `y_from` / `y_to` name
  declared outputs; offsets and `when` as on boxes; `width` 0.5..20
  (default 1); `line_style` `solid`, `dashed`, or `dotted` (default
  `solid`); `color` defaults to `y_from`'s output color and `panel` to its
  panel.
- `name` follows the output grammar and is unique across outputs, boxes,
  segments, renderers, and drawings. Every refusal names the field
  (`boxes.0.top`, `segments.1.x_to`, ...).

## ABI versions: the four contracts

`abi_version` in the sheet decides the contract the module is validated and
run under; nothing else does (no manifest copy, no CLI flag, no inference
from the module's imports). The first contract (`"wrun-1"`) is FROZEN:
published packages on it keep running bit-identically forever. Each later
contract is ADDITIVE over the one before, the same four exports and scalar
block plus one channel family, and freezes in turn: the third (`"wrun-3"`)
adds the handle-keyed draw channel, the strategy channel and the last-bar
flag ([Drawing objects](drawing-objects.md), [Strategy functions](strategy-functions.md)),
the fourth (`"wrun-4"`) the frame channel and series inputs
([Drawing primitives](drawing-primitives.md)). The second (`"wrun-2"`)
adds the cell and string channels ([Quick reference](../reference/quick-reference.md)),
the renderer and drawing vocabulary (next section), and these sheet
additions:

```json
{
  "id": "orderflow-context",
  "abi_version": "wrun-2",
  "warmup_bars": 1,
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "profile": { "source": "volume_profile" },
    "book": { "source": "book", "block_size": 10, "max_depth": 40 }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "profile", "cellType": "array", "max_cells": 256 },
    { "index": 2, "name": "book", "cellType": "array", "max_cells": 80 }
  ],
  "outputs": [{ "index": 0, "name": "imbalance", "plot": "line", "panel": "lower" }]
}
```

- `inputs[].cellType: "array"` marks a celled input. It requires
  `abi_version: "wrun-2"` (refused under the first contract naming the way
  in) and requires `max_cells` beside it (a positive integer counting the
  class's TUPLES, the host-enforced cap; an oversize bar refuses the
  evaluation, never truncates). `max_cells` without `cellType` is refused
  as meaningless on a scalar input.
- A celled input reads a celled source class (`volume_profile`, `book`,
  `tape`, `trade_volume_by_size`; the catalog and each class's knobs are in
  [Data sources](../core-concepts/data-sources.md)), and the pairing is
  checked both ways: a celled class under a scalar input is refused, and
  `cellType` on a scalar-feed source is refused at fetch. A celled input
  keeps its positional slot in the scalar argument block (the slot's value
  is `NaN`) and keeps an ordinary `inputSources` entry.
- A celled class can never be the primary input (index 0): the primary must
  stay a fetched scalar series that defines the request grid. Celled
  sources may pin `symbol` + `exchange` together like any feed source, but
  never `interval`: cell blocks exact-join the primary grid row for row
  (alignment below), so there is no independent clock to pin.
- `generated_from: "declarations"` and `source_digest` are provenance
  markers written by the code-first pipeline. The schema accepts and
  preserves them on any sheet; a sheet carrying them is treated as derived
  state (direct sheet edits are refused, see above).
- A first-contract sheet over a module that imports the cell or string
  functions is refused at validation, install, and build; a second-contract
  sheet over a scalar-only module is legal and computes bit-identically.

Cell 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 the way sparse coarse scalars are (a
replayed block would double-count volume). Celled inputs never abstain a
row: warmup policy stays the module's `state()` decision.

Where celled packages run: alerts, `om metric get` / `om metric series`, and
chart previews evaluate them like any other package (the daemon fetches
`volume_profile` and `book` live). Backtests and screens REFUSE celled
packages by name (`wrun_celled_metric_unsupported`): their replay and
fan-out paths carry no cell blocks yet.

## String slots, renderers, drawings

Outputs stay numbers; the second contract adds three top-level arrays that
turn some of those numbers (and per-bar text) into chart decorations. All
three are refused under the first contract, and every reference is checked
at validation: numeric coordinates name declared OUTPUTS, text fields name
declared STRING SLOTS, and the two namespaces never substitute for each
other.

- `string_slots`: `{index, name, max_bytes, description?}`, indexes
  contiguous from 0, unique names, at most 64 slots, `max_bytes` 1..4096.
  Slots are written per ready bar during `finalize()` through the generated
  senders; a slot not written that bar is ABSENT, distinct from a written
  empty string. Slots are not outputs and never become metrics.
- `renderers` (at most 32 per package), each `{kind, name, ...}`:
  - `text`: `{y, text, color?, size?}`. One mark per ready row whose slot
    is present, at (bar time, `y`); optional `color` (string) and `size`
    (an integer pixel count, 6..64, refused outside) style the text.
  - `label`: `{x, y, text, color?, size?}`. ONE label at (`x`, `y`); `x` is
    a numeric output in epoch seconds; optional `color`/`size` as on
    `text`. Lifecycle: the LAST ready row whose slot is present and
    nonempty and whose `x`/`y` are finite wins; later rows with an absent
    or empty slot do not clear an earlier winner.
  - `table`: `{rows, cols, cells, position?}` with `cells` = exactly
    `rows * cols` string-slot names, row-major; caps `rows` <= 32, `cols`
    <= 8. Lifecycle: the LAST ready row where EVERY cell slot is present
    (empty strings included) wins; no such row, no table.
  - `shape`: `{output, shape, where?, color?, width?}` with `shape` one of
    `circle`, `cross`, `triangle_up`, `triangle_down`, `diamond`,
    `arrow_up`, `arrow_down`, `flag`, `square`; optional `color` (string)
    and `width` (positive number) style the mark on the chart. One mark per
    ready row where `output` is finite, gated (when `where` is declared) to
    rows where the gate is finite and nonzero.
  - `stats_row`: `{output, title?, format?, polarity?}`. The output as a
    stats strip, one entry per ready row.
  - `bgcolor`: `{where, color?, color_by?, colors?}`. One background tint
    per ready row where the `where` gate output is finite and nonzero.
    `color_by` (a data-only output) indexes `colors` per bar (floor;
    finite out-of-range clamps to entry 0; a non-finite ladder value, `NaN`
    or infinite alike, = no tint that bar, the static `color` never
    substitutes); `color` is the static tint when no ladder is declared;
    each ladder half without the other is refused by name.
- `drawings` (at most 64 per package), kinds `line` (`x1, y1, x2, y2`),
  `box` (`left, top, right, bottom`), `polyline` (`points`, at most 64
  `{x, y}` pairs), `label` (`x, y, text`, plus optional `size`, a positive
  number for the rendered text size), plus optional `color` / `width` /
  `line_style` per kind. Every coordinate names a numeric output; x-axis
  values are epoch SECONDS (the `time` source feeds them). Lifecycle is
  deterministic with no backward scan: only the LAST ready row is
  evaluated; every declared coordinate finite there (and, for `label`, its
  text slot present) means the object exists with `createdBar` = that
  row's index, while any `NaN` coordinate or absent slot there means NO
  object, regardless of earlier rows.

One more run-level cap: the expanded render selection (every selected entry,
number, and string) must stay under 2 MiB per run, refused as
`wrun_render_result_too_large`.

A worked example: a 2x2 session-stats table plus a box drawn around the
session's price range. The coordinate outputs are data-only (`plot: ""`),
the box's x coordinates come from the `time` source (epoch seconds), and
the four table cells are rewritten every ready bar so the table always
shows the last complete row:

```json
{
  "id": "session-table",
  "name": "Session Range Table",
  "abi_version": "wrun-2",
  "warmup_bars": 1,
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "bar_t": { "source": "time", "field": "bar_open_sec" }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "bar_t" }
  ],
  "outputs": [
    { "index": 0, "name": "range_pct", "plot": "line", "panel": "lower", "unit": "%" },
    { "index": 1, "name": "left", "plot": "" },
    { "index": 2, "name": "top", "plot": "" },
    { "index": 3, "name": "right", "plot": "" },
    { "index": 4, "name": "bottom", "plot": "" }
  ],
  "string_slots": [
    { "index": 0, "name": "close_label", "max_bytes": 16 },
    { "index": 1, "name": "close_text", "max_bytes": 32 },
    { "index": 2, "name": "range_label", "max_bytes": 16 },
    { "index": 3, "name": "range_text", "max_bytes": 32 }
  ],
  "renderers": [
    {
      "kind": "table",
      "name": "session_stats",
      "rows": 2,
      "cols": 2,
      "cells": ["close_label", "close_text", "range_label", "range_text"],
      "position": "top_right"
    }
  ],
  "drawings": [
    {
      "kind": "box",
      "name": "session_zone",
      "left": "left",
      "top": "top",
      "right": "right",
      "bottom": "bottom",
      "color": "#f59e0b"
    }
  ]
}
```

```typescript
import { in_bar_t, in_close } from "./gen/inputs";
import { emitRow, out_bottom, out_left, out_range_pct, out_right, out_top } from "./gen/outputs";
import {
  sb_clear,
  sb_f64,
  sb_text,
  str_close_label,
  str_close_text_sb,
  str_range_label,
  str_range_text_sb,
} from "./gen/strings";

let close: f64 = NaN;
let high: f64 = NaN;
let low: f64 = NaN;
let left: f64 = NaN;
let right: f64 = NaN;

export function init(): void {}

export function state(): i32 {
  close = in_close();
  if (isNaN(high) || close > high) high = close;
  if (isNaN(low) || close < low) low = close;
  const t = in_bar_t();
  if (isNaN(left)) left = t;
  right = t;
  return low > 0.0 ? 1 : 0;
}

export function finalize(): void {
  const range = (100.0 * (high - low)) / low;
  out_range_pct(range);
  out_left(left);
  out_top(high);
  out_right(right);
  out_bottom(low);
  str_close_label("close");
  sb_clear();
  sb_f64(close, 2);
  str_close_text_sb();
  str_range_label("range");
  sb_clear();
  sb_f64(range, 2);
  sb_text("%");
  str_range_text_sb();
  emitRow();
}

export function reset(): void {
  close = NaN;
  high = NaN;
  low = NaN;
  left = NaN;
  right = NaN;
}
```

Because every coordinate output is finite on the last ready bar, the box
exists with `createdBar` = that bar; a module that wants a drawing REMOVED
emits `NaN` through its coordinate outputs on the newest bar. Renderers and
drawings are decorations, never pane anchors: the chart lane renders them
from the package's single overlay, and `om metric get` / `om metric series`
on the same package keep returning the numeric outputs unchanged (the
declarations never alter a metric value).

## Pins: fixed markets, fixed intervals

A non-odds, non-time feed source may pin `symbol` and `exchange` TOGETHER so
a secondary input reads a fixed reference market while the package follows
the selector. The pair rule is SCHEMA-ENFORCED: a lone `symbol` or a lone
`exchange` is refused with the issue on the missing half (symbols are
venue-native strings, so half a pin names a market that does not exist).
`interval` is an independent pin on any feed source, and it is legal: a
coarser source contributes to a primary row only as-of its candle CLOSE
(`candle.ts + sourceSec <= min(now, row.ts + primarySec)`), so a forming 4h
candle never leaks its final value into the 1h rows under it, live or
historical. Sparse coarse observations carry the latest CLOSED observation
forward; equal-or-finer sources align by bar open, row for row.

- `missing` (scalar alignment policy, `"carry"` | `"nan"` | `"zero"`): on a
  secondary source, absent or `"carry"` keeps the default above, the latest
  eligible value carried forward. `missing: "nan"` delivers `NaN` on primary
  bars that bring no observation of their own and never gates row
  readiness: a naturally sparse series (liquidations) stays sparse and the
  module decides what missing means. `missing: "zero"` is the same policy
  delivering 0 instead of `NaN`, the mapping most kScript ports apply to a
  sparse column. Closed coarse candles still carry as-of close: a coarse
  source is never "missing" between its closes. Refused on celled classes
  (a bar with no observation is a present empty block) and on the `time`
  source.
- Primary densification: on the PRIMARY input (index 0), `"nan"` and
  `"zero"` change what the request grid IS. Instead of the rows the fetch
  returned, the package computes on a synthesized dense grid of bar-open
  timestamps at the primary's interval, in the phase the fetched rows sit
  on (epoch-aligned for crypto intraday bars, Monday opens for crypto WEEK
  bars; never an assumed epoch multiple), covering the last `windowBars`
  bars and ending at the bar containing `now` (the closed-bar cut and the
  forming-bar rules apply to the synthesized rows exactly as to fetched
  ones). Every fetched observation lands on its own grid bar; bars the
  fetch did not return deliver the fill (0 or `NaN`). An empty primary
  fetch refuses by name (`wrun_densify_empty_fetch`: no row, no phase to
  derive), and fetched rows that disagree on phase refuse too
  (`wrun_densify_mixed_phase`). Densification requires a fixed-UTC bar
  phase: a session-aligned grid whose UTC open shifts at a DST transition
  (CME DAY/WEEK bars) spans two phases across the transition and refuses
  as `wrun_densify_mixed_phase` by design; such grids are not supported.
  Secondaries align onto the synthesized grid under their own policy
  (`carry` abstains rows before its first observation, `nan`/`zero` fill).
  This is how a sparse series (liquidations) becomes a dense primary.
  `"carry"` on the primary is refused (there is no earlier grid bar to
  carry from), `missing` is refused on `metric` composition sources
  altogether (the inner package's rows are already a computed grid), and
  an absent policy keeps the fetched-rows grid byte-identical.

```json
{
  "id": "dense-liquidations",
  "abi_version": "wrun-1",
  "warmup_bars": 20,
  "params": [],
  "inputSources": {
    "liqs": { "source": "liquidations", "field": "liquidations", "missing": "zero" },
    "close": { "source": "ohlcv", "field": "close" }
  },
  "inputs": [
    { "index": 0, "name": "liqs", "description": "Total liquidations, 0 on quiet bars, one row per bar" },
    { "index": 1, "name": "close", "description": "Close, carried onto the dense grid" }
  ],
  "outputs": [{ "index": 0, "name": "liq_ratio", "plot": "histogram", "panel": "lower" }]
}
```

```json
{
  "id": "btc-4h-context",
  "abi_version": "wrun-1",
  "warmup_bars": 1,
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "btc_4h": { "source": "ohlcv", "field": "close", "symbol": "BTCUSDT", "exchange": "BINANCE_FUTURES", "interval": "FOUR_HOURS" }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "btc_4h", "description": "BTC on its own 4h grid, as-of close" }
  ],
  "outputs": [{ "index": 0, "name": "ratio", "plot": "line", "panel": "lower" }]
}
```

Authoring policy the schema cannot check: keep the primary input (index 0)
selector-following (a fully pinned package computes the same value for every
selector symbol, which mislabels screens and legends); cross-symbol
arithmetic is only dimensionally sane under the shared default USD quote.
What a coarse pin costs: the fetch widens by two source intervals, and the
value steps once per source candle. The chart reads its own market and
interval for every input today, so a pinned package is a package for your
machine ([Multi-timeframe](../core-concepts/multi-timeframe.md)).

## Odds sources: pinned or bindable

An `odds` source reads Polymarket probabilities. `field` defaults to `close`;
`exchange` is implicit (and refused if written); the market key is the
conditionId (`0x` + 64 hex), never the market slug. Three modes:

- Selector-following: no `symbol`, no `binding`; the input inherits a
  conditionId selector.
- Pinned: `symbol` carries the conditionId, `outcome` is `YES` or `NO`
  (`NO` only on the `close` field). `om wrun source set` repoints pinned odds
  inputs by market query or conditionId.
- Bindable: `binding: "required"` INSTEAD of a symbol; the market arrives per
  use (`sourceBindings` on the alert, signal, query, or `om metric get
  --bind input=0x...`). Never both on one input; a bindable input's outcome
  comes from the binding (metadata `outcome` refused); screens refuse
  bindable packages (no per-row market exists).

```json
{
  "id": "pm-mom",
  "abi_version": "wrun-1",
  "warmup_bars": 1,
  "params": [{ "name": "period", "default": 5, "min": 1, "max": 200 }],
  "inputSources": { "yes_odds": { "source": "odds", "binding": "required" } },
  "inputs": [{ "index": 0, "name": "yes_odds" }],
  "outputs": [{ "index": 0, "name": "momentum", "plot": "line", "panel": "lower" }]
}
```

The matching source, complete (`Roc` over the bound market's YES
probability):

```typescript
import { in_yes_odds } from "./gen/inputs";
import { emitRow, out_momentum } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Roc } from "./sdk/ta";

let roc = new Roc(5);
let value: f64 = NaN;

export function init(): void {
  roc = new Roc(i32(p_period()));
}

export function state(): i32 {
  value = roc.update(in_yes_odds());
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  out_momentum(value);
  emitRow();
}

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

## Metric composition sources

`{ "source": "metric", "metric": "wrun/@scope/name/output", "params": {...} }`
feeds another installed package's output in as an input (the referenced
package must be installed). Composition inputs keep open-stamp alignment, so
a non-primary metric source pinned COARSER than the primary grid is refused
(the inner value would be read before its bar closed); the refusal names the
input and both intervals. Do not pin `symbol`/`exchange` on metric sources.
Composition is metadata-first only: the declaration grammar refuses it by
name ([Libraries](libraries.md)).

## Validation, in one list

The schema refuses, naming the path: non-contiguous or duplicate input and
output indexes and names; duplicate param names; an input without a source
entry or a source entry without an input; a `time` primary; a lone pin half;
`outcome`/`binding` outside odds; a conditionId that is not `0x` + 64 hex;
missing `tenor`/`side`/`token` on sources that require them; a `missing`
value outside `carry`/`nan`/`zero`, `missing: "carry"` on the primary
input, or any `missing` on a celled source, a `metric` source, or the
`time` source; a non-integer `displacement_bars` or one outside -500..500;
`width_by` without `widths` or `widths` without `width_by`, a `width_by` or
`color_by` naming no declared output or naming its own output, `widths`
outside 1..10 entries of 0.5..20; equal or non-rendered `ranges` sides, a
bgcolor `colors`/`color_by` ladder half without the other, a range
`color_by` without `colors` (bare range `colors` is legal: the band sign
palette), `edge_width` outside integers 1..10, an `edge_line_style` outside
solid/dashed/dotted; a bgcolor `where` naming no declared output; a
text/label renderer `size` outside integers 6..64; unknown fields per
source; `cellType` without the second contract or without `max_cells`;
`max_cells` without `cellType`; a celled source class under a scalar input,
as the primary input, or with a `field`, an `interval` pin, or any
scalar-feed knob; `book` without `block_size`, and `block_size`/`max_depth`
anywhere but `book`; `string_slots`/`renderers`/`drawings` under the first
contract; non-contiguous or duplicate string-slot indexes and names,
`max_bytes` outside 1..4096, more than 64 slots; more than 32 renderers or
64 drawings; table `cells` whose length is not `rows * cols`, `rows` > 32,
`cols` > 8; polylines with more than 64 points; renderer and drawing
references to outputs or string slots that do not exist; style and fill
references to outputs that do not exist; more than 16 boxes or 16 segments;
a box or segment coordinate, offset, or `when` gate naming no declared
output; a literal `x_from`/`x_to` that is not an integer or lies outside
-500..500; a box or segment name already used by an output, renderer,
drawing, box, or segment; box `opacity` outside 0..1 or `border_width`
outside 0..10; segment `width` outside 0.5..20; a `line_style` outside
solid/dashed/dotted or a `panel` outside overlay/lower on either. Unknown
TOP-LEVEL sheet fields are warned about and ignored, never fatal, so newer
sheets degrade gracefully on older readers. The messages themselves are in
[Common errors](../faq/common-errors.md).
