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 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

ExportRunsDo this hereNever here
init(): voidonce per evaluation, before any barread params through p_<param>(), size buffers, construct TA objectsread an input (the in_ accessors trap outside state())
state(): i32once per bar, oldest firstread 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(): voidafter each bar whose state() returned 1write every output through out_<output>(value), write string slots, then emitRow() LASTread an input
reset(): voidwhen the host replays the forming barreassign every module-level variable, call .reset() on every TA objectallocate

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

AccessorPhaseMeaning
p_<param>(): f64init()the param's value: its default, or the setting the chart user chose
in_<input>(): f64state()this bar's scalar input; NaN on a celled input's slot
out_<output>(value: f64): voidfinalize()writes one output; emitRow(): void commits the row
in_<input>_cells(): i32state()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): i32state()copies the block into module memory at ptr; returns bytes written, 0, or -1
in_<input>_max_cells: i32constantthe declared max_cells, in source tuples
in_<input>_capacity: i32constantthe 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)

DeclarationSignature
Paramparam(name, default, { required?, min?, max?, description? })
Inputinput(name, <source>.<field>, { symbol?, exchange?, interval?, side?, tenor?, token?, outcome?, binding?, missing?, description? })
Celled inputinput(name, <class>.cells, { max_cells, block_size?, max_depth?, symbol?, exchange?, description? })
Outputoutput(name, plot?, panel?, { description?, unit?, color?, colors?, width?, opacity?, line_style?, color_by?, shape_where?, displacement_bars?, width_by?, widths? }), returns an OutputHandle
Rangerange(upper, lower, { color?, colors?, color_by?, edge_width?, edge_line_style?, smooth? })
Boxbox(name, { top, bottom, from?, to?, when?, panel?, color?, borderColor?, opacity?, borderWidth? })
Segmentsegment(name, { yFrom, yTo, from?, to?, when?, panel?, color?, width?, lineStyle? })
String slotstring(name, { max_bytes, description? })
Renderersrender.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? })
Drawingsdraw.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:

SlotValues
plotline, bar, area, histogram, candle, shape, scatter, none (data-only: computed, never drawn)
paneloverlay (the price pane), lower (its own pane)
line_style / lineStyle / edge_line_stylesolid, dashed, dotted
shape (renderer)circle, cross, triangle_up, triangle_down, diamond, arrow_up, arrow_down, flag, square
missingcarry (default on a secondary), nan, zero; on the primary input nan and zero densify the grid
sideBUY, SELL
tenorONE_D, THREE_D, ONE_W, ONE_M, TWO_M, THREE_M, SIX_M, ONE_Y
outcomeYES, NO (NO on the close field only)
bindingrequired (the market arrives per use, never together with symbol)
Box and segment offsetsinteger literals in -500..500, or an output handle whose per-bar value truncates to the offset
Colorsany string on outputs and segments; a box fill needs hex, rgb(), or hsl()

Sources and fields

SourceFieldsKnobs
ohlcvopen, high, low, close, volumepins only
tradesopen, high, low, close, volumeside REQUIRED
fundingrate_open, rate_high, rate_low, rate_close, predicted_open, predicted_high, predicted_low, predicted_closepins only
oiopen, high, low, closepins only
liquidationsliquidationsside optional (absent = both sides summed); sparse, so a primary usually declares missing: "zero"
implied_volatilityimplied_volatilitytenor REQUIRED
skewskewtenor REQUIRED
token_supplymarketcap, first_marketcap, marketcap_dominance_percent, circulating_supply, total_supply, max_supply, total_value_locked, fully_diluted_valuation, cg_marketcap_rank, total_volume, usd_pricetoken REQUIRED
oddsopen, high, low, close (default), volumeconditionId as symbol plus outcome, or binding: "required"; exchange is implicit and refused
timebar_open_secno knobs, never the primary input
metric (sheet only)metric: "wrun/@scope/name/output", paramsno 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")

ClassTupleWidthKnobsServed by the daemon
volume_profile[low, high, buy, sell] per price bucket4 f64optional symbol + exchangeyes
book[price, size, side] per level, side +1 bid / -1 ask, bids descending then asks ascending3 f64block_size REQUIRED (om block-sizes), optional max_depth, optional pin pairyes
tape[offsetMs, price, size, side] per live print, side +1 BUY / -1 SELL4 f64min_size REQUIRED, history: "0" default, optional pin pairlive buffer only
trade_volume_by_sizedeclared for cross-host paritynoneno: 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.

GroupClassesPer bar
AveragesSma, Ema, Rma, Wma, Hma, Alma, Swma, Linregupdate(x); Vwma is update(x, volume)
Statistics and seriesSum, Median, Percentile, Variance, Stdev, Zscore, Change, Mom, Roc, Cum, Fixnanupdate(x); Correlation is update(a, b)
OscillatorsRsi, Cmo, Tsi, Macdupdate(x)
Oscillators over OHLCCci, Wpr, Stoch, Stochasticupdate(high, low, close); Mfi is update(high, low, close, volume); Obv is update(close, volume)
Ranges and bandsTr, Atrupdate(high, low, close); Bb is update(x), Keltner is update(x, high, low, close), Donchian is update(high, low)
Window extremesHighest, Lowest, HighestBars, LowestBarsupdate(x) (pass the high or the low, the column kScript reads by default)
Trend systemsAdx, Ichimoku, Psar, Supertrendupdate(high, low, close); Vwap is update(open, high, low, close, volume, tsMs), tsMs the bar's open time in milliseconds
EventsRising, Falling, PivotHigh, PivotLowupdate(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.

Commands

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
CommandWhat it does
om wrun templateslists 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 seriesthe newest value; one [barOpenSec, value] pair per bar (--bars 1..500, default 30)
om chart indicator previewdraws 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 accessconsumers, 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.

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

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:

{
  "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" }
  ]
}
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; the styling ladders, fills, and style knobs (hand-written sheets only) are in Styling.

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

{
  "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" }]
}
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. 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:

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 and Drawing objects); the worked table example in Script definition 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:

NeedForm
Numbersf64 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 valueNaN, 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 historya 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.
CollectionsStaticArray<T> and Array<T> allocated at module start or in init(), never in state(); Map<K, V> for keyed state, sized up front.
MathMath.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.
Stringsonly in string slots; build with sb_text / sb_int / sb_f64, never + on a string per bar (that allocates).
Control flowif / else, for, while, break, continue, switch on integers; a number is not a truth value (if (value > 0.0), not if (value)).
Functionsfunction 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.
Typesclass Zone { top: f64 = NaN; bottom: f64 = NaN; alive: bool = false; }, constructed in init(); a type alias for readability.
Timeinput("bar_t", time.bar_open_sec) gives epoch seconds; sessions are integer math on it (UTC).
Colorsnever 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):

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:

FieldMeaningPage
id, name, description, overlaythe sheet's short name, display strings, the legacy placement hintScript definition
abi_versionwrun-1 (absent means this) or wrun-2; the sole ABI authorityScript definition
wasm_sha256, warmup_barsthe build stamp installs verify; the warm-up hint the fetch planner readsScript definition
params[]{name, default, required?, min?, max?, description?}; a style binding makes a style-only knobStyling
inputSources{}, inputs[]sources keyed by input name; {index, name, description?, cellType?, max_cells?}Data sources
outputs[]{index, name, plot?, panel?, unit?, displacement_bars?, ...styling}Plotting
fills[], ranges[]shaded band between two rendered outputs; banded range with edges and a ladderStyling
boxes[], segments[]per-bar shapes over outputs (16 each)Drawing objects
string_slots[], renderers[], drawings[]the second ABI's text and decoration vocabularyScript definition
generated_from, source_digestprovenance of a sheet derived from declarationsthis page

The full validation list, every refusal by path, is at the end of Script definition; the messages you will actually meet, with their fixes, are in Common errors.