Every ceiling an Indicator runs under, in one place: the declaration caps the sheet validator enforces, the runtime ceilings the sandbox enforces on every evaluation, the data-plane bounds, and the package bounds. Each row gives the exact number, what it protects, the shape that hits it, and the refusal it produces. kScript (legacy) documents its sandbox the same way; the mapping between the two lists is at the end, because most kScript limits count objects a script creates at run time and an Indicator declares almost everything once.
Declaration caps
Checked by the same schema at author, build, install, and publish, so a
sheet over a cap fails in the Problems lane or on om wrun build, never on
a chart. Every refusal names the path (boxes.0.top, renderers.2.size).
| Limit | Value | What it caps | When you hit it | Refusal |
|---|---|---|---|---|
| Boxes | 16 per sheet | boxes[] entries, each drawn once per bar | one declaration per occurrence instead of one gated declaration | boxes must declare at most 16 entries |
| Segments | 16 per sheet | segments[] entries, each drawn once per bar | the same shape | segments must declare at most 16 entries |
| Renderers | 32 per package | renderers[] (text, label, table, shape, stats_row, bgcolor) | a footprint-style sheet with a text tile per price level | renderers must declare at most 32 entries |
| Drawings | 64 per package | drawings[] (line, box, polyline, label), the declared kind placed from the newest bar | many run-level objects; a growing list is handles instead | drawings must declare at most 64 entries |
| Polyline points | 64 pairs | points on one declared polyline drawing (a polyline handle takes 256) | a curve traced point by point in a declaration | polyline points must be <= 64 pairs |
| Handle defaults | width 0.5..20, border_width 0..10, opacity 0..1, size 6..64, line_style solid/dashed/dotted, extend none/left/right/both, align left/center/right (label only), panel overlay/lower | one entry per kind in handles (line, box, label, polyline); an unknown kind is refused, never dropped | handles.line.width: width must be <= 20, unknown handle kind 'circle'; the handle kinds are line, box, label, polyline | |
| Handle labels need a slot | at least 1 string slot | handles.label | enabling label handles in a sheet with no string_slots | handles.label needs at least one string slot: a handle label takes its text from a declared slot ... |
| Third-contract fields | "abi_version": "wrun-3" | handles (even empty), a label renderer's position, a text renderer's style | a sheet under the second contract that adds one of them | handles needs abi_version "wrun-3" (wrun-2 has no draw channel; wrun-1 and wrun-2 are frozen) |
| Table geometry | 32 rows x 8 cols | one table renderer; cells must list exactly rows * cols slot names | a dashboard dump | table rows must be <= 32, table cols must be <= 8, table 'stats' declares 2x2 = 4 cells but lists 3 |
| String slots | 64 per package | string_slots[] | one slot per table cell on a big table | string_slots must declare at most 64 slots |
| Slot bytes | 1..4096 per slot | max_bytes on one slot | a long per-bar readout | max_bytes must be <= 4096 (the per-slot byte cap) |
| Text size | 6..64 px | size on text and label renderers | size must be >= 6, size must be <= 64 | |
| Display offset | -500..500 bars, integer | displacement_bars on an output | a lagging span longer than 500 bars | displacement_bars must be >= -500, displacement_bars must be <= 500, displacement_bars must be an integer count of bars |
| Shape offset | -500..500 bars, integer literal | x_from / x_to (code first from / to) on a box or segment | a zone anchored further back than 500 bars | a literal bar offset must be within -500..500 bars of the current bar |
| Width ladder | 1..10 entries, each 0.5..20 | widths beside width_by | widths must list at most 10 entries, widths entries must be <= 20 | |
| Line width | 0.5..20 | width on an output or a segment | width must be <= 20 | |
| Border width | 0..10 | border_width on a box | border_width must be <= 10 | |
| Edge width | 1..10, integer | edge_width on a range | edge_width must be <= 10 | |
| Opacity | 0..1 | opacity on an output or a box (box default 0.2) | named by path | |
| Color string | 1..64 chars | any color on a box, segment, renderer, or drawing | color must be a non-empty string of at most 64 chars | |
| Box fill color | hex, rgb(), or hsl() | a box color (the fill takes the opacity) | a named color such as "red" | color must be a hex, rgb() or hsl() color (the fill takes the opacity) |
| Palette | at least 2 entries | colors beside color_by | color_by needs 'colors' with at least 2 entries | |
| Celled cap | required, positive integer, counts TUPLES | max_cells on a cellType: "array" input | a celled input declared without it | input 'profile' declares cellType "array" without max_cells; guests preallocate max_cells * 8 bytes and the host refuses bigger blocks, so the cap is required |
| Outputs | 1..256 | outputs[]; the runtime tracks 256 output slots per run | a sheet past 256 outputs, or a raw write past the tracked slots | WRUN output count must be an integer from 1 to 256, WRUN output index 300 out of bounds for 256 tracked outputs |
Names share one namespace across outputs, boxes, segments, renderers, and
drawings; color_by, width_by, and shape_where cannot name their own
output; each ladder half without the other is refused by name. The full
validation list is at the end of
Script definition.
Runtime ceilings
Enforced on every evaluation by the sandbox, on the chart and on your machine alike. A breach refuses the WHOLE evaluation by name; nothing is truncated, clamped, or silently dropped.
| Limit | Value | What it protects | When you hit it | Refusal |
|---|---|---|---|---|
| Module memory | 4 MiB (4,194,304 bytes); OM_WRUN_MEMORY_LIMIT_BYTES overrides on your machine | the linear memory a module declares and uses; validated from the binary's memory section before instantiation, then re-checked live | a buffer sized far past any param's max | WRUN declared memory 0 initial 8388608 bytes (128 pages) exceeds limit 4194304 bytes, WRUN memory 8388608 bytes exceeds limit 4194304 bytes |
No growth after init() | 0 bytes of growth | flat per-bar memory across a long history and the forming bar's replays | an array that grows per bar, string concatenation in finalize() | WRUN memory grew from 65536 to 131072 bytes after init; allocate persistent state in init and keep per-bar state/finalize work allocation-free |
| Output values | finite or NaN | a chart and a metric that mean something | a division by zero written as Infinity | WRUN output 0 is Infinity; guard the calculation and emit NaN when the value is undefined |
| Evaluation deadline | 30 s of wall clock per run by default (pack, queue, execute, unpack); a lane may arm its own deadline; there is no instruction metering | one runaway module never holds a worker | unbounded loops, deep recursion | WRUN execution timeout after 30000ms; the worker is killed and replaced |
| Cell block | max_cells tuples per bar, per celled input | the buffer the module preallocated from in_<input>_capacity | a deeper book than the cap; a profile with more price buckets than the cap | wrun_cell_block_too_large: input 'book' bar at ts 1725580800 has 48 cells (tuples); max_cells is 40. Blocks are never truncated, then the two ways out (raise max_cells, or cap max_depth on a book) |
| Cell read bounds | inside exported memory | a copy that would overrun the module's memory | a pointer outside the preallocated buffer | WRUN wrun_arg_bytes(1, 65536): 1536 bytes do not fit the module's exported memory of 65536 bytes; reserve max_cells * 8 bytes per celled input and pass a pointer inside that buffer |
| Cell phase | state() only | the cell accessors read the bar being evaluated | a cell read in init() or finalize() | WRUN wrun_arg_len is callable during state() only (called during finalize); read cell blocks inside state() and carry what finalize() needs in module state |
| String phase | finalize() only | slots belong to the row being committed | a string sent from state() | WRUN wrun_output_str is callable during finalize() only (called during state) |
| String slot bytes | max_bytes per slot (at most 4096) | the buffer each host reserves per slot | a line longer than the slot | WRUN string slot 0 write of 40 bytes exceeds the slot's max_bytes 32; strings are never truncated; shorten the text or raise max_bytes in metadata.string_slots |
| String bytes per row | 65,536 (64 KiB) | one bar's text across all slots | many slots near their cap on one bar | WRUN string writes for one row total 70000 bytes, over the per-row limit 65536; emit less text per bar |
| String bytes per run | 8,388,608 (8 MiB) | one evaluation's text across all bars | long readouts on every bar of a long history | WRUN string writes for this run total 8400000 bytes, over the per-run limit 8388608; emit less text or fewer rows |
| UTF-8 | valid sequences only | a slot never carries a replacement character silently | raw bytes sent through the host import | WRUN string slot 0 write of 5 bytes is not valid UTF-8; wrun_output_str carries UTF-8 text only (encode before writing) |
| Expanded render result | 2 MiB (2,097,152 bytes) | the selection the chart receives after the run: 16 bytes per selected entry, 8 per carried number, the UTF-8 bytes of every carried string (table cells charge 8 more per cell; a live handle charges 16 plus 8 per number, 16 per polyline point, and its strings) | many renderers over a long history with long strings; hundreds of live polylines | WRUN render result exceeds 2097152 bytes expanded (wrun_render_result_too_large): fewer rows, shorter strings, or fewer renderers/drawings |
| Live handles per kind | 500 | the handles of one kind alive at once, counted on every creation (the same ceiling kScript put on each drawing kind) | a zone tracker that never deletes | WRUN wrun_draw_box(7): 501 live box handles would exceed the cap of 500 per kind (wrun_draw_kind_limit); delete handles the Indicator no longer needs |
| Live handles in total | 1500 | every kind together | the same shape across kinds | ... would exceed the cap of 1500 in total (wrun_draw_total_limit) |
| Polyline handle points | 1..256 per call | the points one setPoints sends; the buffer must fit the module's memory | a path that never trims its oldest points | WRUN wrun_draw_polyline(0): 300 points exceed the cap of 256 (wrun_draw_polyline_too_large), wrun_draw_polyline_empty, wrun_draw_polyline_out_of_bounds |
| Draw calls per bar | 4096 | every handle call on one bar | re-sending every live handle on every bar | WRUN draw calls for one row exceed the cap of 4096 (wrun_draw_calls_per_row); emit fewer draw calls per bar |
| Draw phase | finalize() only; bar.isLast() also in state() | handle ops belong to the row being committed | a handle call from state() | WRUN wrun_draw_box is callable during finalize() only (called during state) (wrun_draw_phase) |
| Handle identity | ids >= 0, one space across kinds, kind declared in handles, coordinates finite | the ledger the chart mirrors, kind by kind | a negative id; a label on a box's id; a kind the sheet does not list; a NaN corner; a setter on an id nobody holds | wrun_draw_id_negative, wrun_draw_kind_mismatch, wrun_draw_kind_undeclared, wrun_draw_non_finite, wrun_draw_handle_missing |
| Handle style values | the prop's range, integer where required, legal on the kind | the same setter table the engine enforces | size on a line; width of 30; style of 7 | wrun_draw_prop_unsupported, wrun_draw_style_out_of_range, wrun_draw_prop_unknown |
| Handle label text | a declared slot, written on the bar of the draw call | the text a label carries is the slot's bytes at call time | text(...) before the slot's sender ran that bar | wrun_draw_label_slot_absent, wrun_draw_label_slot_undeclared |
The export contract is checked statically before any of this: the four
exports with exact signatures, exported memory when the cell, string,
or polyline channel is imported, every present draw import with its exact
signature, and an import set limited to the ABI's allowlist (a rejection
lists the legal set and, for a draw import under an older contract, names
abi_version "wrun-3" as the way in). Those messages are in
Common errors.
Data-plane bounds
| Limit | Value | Where |
|---|---|---|
om metric series --bars | 1..500, default 30 | the newest N bars, newest = the still-forming bar |
| Warm-up inference | params up to 500 | the fetch planner widens the window to the largest window-shaped param (period, fast, slow, ...) up to 500; a param above 500 is read as a threshold, not a lookback, so a window longer than that declares warmup_bars |
| Explicit window | at least warmup_bars + 1 | a caller's windowBars below the sheet's warm-up plus one computable row is refused (wrun_window_bars_invalid) |
| Composition depth | 4 packages | a source: "metric" chain (outer to innermost) |
| Coarse pins | as of the coarser candle's close | a pinned interval contributes to a primary row only once its candle has closed; the fetch widens by two source intervals |
| Book depth | max_depth levels per side | bounds the block a book input receives, so max_cells can be 2 x max_depth |
| Metric fetch deadline (strategies) | OM_DEADLINE_METRIC_FETCH, 30 s by default | a strategy's metric fetch chain is aborted past it and the tick records metric_timeout |
| Celled consumers | alerts, om metric get / om metric series, chart previews | backtests and screens refuse celled packages by name (wrun_celled_metric_unsupported); an unserved class refuses before any fetch (wrun_cells_unavailable) |
| Chart preview | one preview per pane; undisplaced outputs only | an output with a nonzero displacement_bars is refused (wrun_preview_displacement_unsupported) |
| Strategy order ids | 64 distinct per run (closeAll counts as one) | the 65th distinct id refuses the run (wrun_strategy_id_limit); an id is a slot, re-issuing one replaces its pending order |
| Strategy id text | 64 bytes of UTF-8 per id, from name or OCA name | longer text refuses by name (wrun_strategy_id_too_long), never truncated; invalid UTF-8 refuses (wrun_strategy_id_invalid_utf8) |
| Strategy calls per bar | 4096 order calls and getter reads per bar | over it refuses (wrun_strategy_calls_per_row); an infinite price refuses (wrun_strategy_non_finite), NaN is absent |
| Closed trades per run | 10000 | the 10001st closed trade refuses on the bar it closes (wrun_strategy_trade_limit); every engine rejection (pyramiding, legs, sizing, margin, the forming bar) is counted in rejectedOrders, never a refusal |
| Strategy result bytes | inside the 2 MiB expanded render result | 16 per trade or order record plus 8 per number, 8 per equity cell (wrun_render_result_too_large) |
| Strategy feed | the price input reads ohlcv on the instance's own market, no symbol or exchange pin | refused at the sheet (wrun_strategy_feed_not_ohlcv); slippage_model: "bookEstimate" refused (wrun_strategy_slippage_model_unsupported); strategy.position and strategy.equity are reserved output names (wrun_strategy_reserved_output) |
Package bounds
| Limit | Value |
|---|---|
| Registry tarball | 200 MiB |
| Extracted package | 512 MiB |
| Archive entries | 4,096 |
max_cells preallocation, per celled input | max_cells x tuple width x 8 bytes (4 f64 per volume_profile or tape tuple, 3 per book tuple), inside the 4 MiB |
| Live tape retention, per market | At most two hours or 200,000 prints, filtered by the smallest active min_size; no REST backfill |
Nothing from a package executes at install time, the module runs
sandboxed (no filesystem, no network, no order capability), and an install
verifies wasm_sha256 before every run.
kScript (legacy) limits, mapped
| kScript (legacy) limit | In Indicators |
|---|---|
| Source budget (10 weighted slots per script) | No slot budget. Every input(...) is one declared fetch, planned and deduplicated by the daemon per source key (symbol + exchange, interval, side, tenor, block_size are facets of that key). The chart reads its own market for every input. |
MAX_OUTPUT_OBJECTS (2,000,000 per run) | No object count. Every output is one number per bar, in at most 256 slots; decorations are declared once. The only size cap on what reaches the chart is the 2 MiB expanded render result. |
MAX_DRAWINGS_PER_KIND (500) and 3,000 total | Handles: 500 live per kind and 1500 in total, refused by name on the creation that would exceed them, so a tracker deletes what it no longer needs exactly as it did in kScript. Declared shapes have no per-kind object count: 16 boxes and 16 segments, each evaluated on every bar (a 500-bar window holds up to 16,000 shapes); 64 run-level drawings evaluated on the newest bar; 32 renderers. |
MAX_TABLE_CELLS (10,000) | 32 x 8 = 256 cells per table renderer, each a string slot; 64 slots per package. |
MAX_COLLECTION_SIZE (100,000 elements) | No element cap. Collections live inside the 4 MiB module memory and are allocated before the first bar. |
MAX_STRUCT_CONSTRUCTION_DEPTH (64), MAX_RECURSION_DEPTH (200) | The compiler's own rules apply; the module's stack lives inside its memory, and the evaluation deadline stops runaway recursion. |
| Backtest grants (1,000 or 20,000 bars, runs per day) | om backtest @scope/name replays a strategy package over the history the data plane serves, through the daemon's own runtime and the engine's broker; the CLI carries no per-tier bar grant. Backtests and screens refuse celled packages. |
Strategy declaration bounds (leverage, fees, funding) | The same bounds, checked at build time with the engine's rules: initial_capital and leverage more than 0, pyramiding an integer of at least 1, maintenance_margin_percent at least 0 and under 100, the fee and slippage rates at least 0, enum fields the engine's literals. A setting linked to a param is checked at init() and falls back to the default when out of range, reported. Writing strategies has the table. |
Notes that save debugging time
max_cellsis a contract, not a hint. Size the buffer from the generatedin_<input>_capacity(tuples x width), and give abookinput amax_depthso the cap is knowable:2 x max_depthtuples.- Strings never truncate. Pick
max_bytesfor the longest line the slot will ever carry; the generated builder counts the bytes a line REQUIRES, so an oversize line refuses by name instead of clipping. - Most caps are counts of declarations. You meet them at build time,
in the Problems lane or on
om wrun build, and the message names the field. A repeating shape is one declaration gated per bar withwhen, never one declaration per occurrence. - The render cap counts expanded bytes. Thirty-two text renderers over a long history with long strings is the shape that reaches 2 MiB; leave slots unwritten on quiet bars so those rows select nothing.
- Delete what you no longer draw. A handle stays alive until
delete(); the per-kind cap is met by the tracker that creates a zone on every pivot and never frees one. Keep a bounded pool of ids and delete the oldest before reusing its id, and re-send only the handles that changed on a bar (the calls-per-bar cap counts every setter). - Allocate once. Buffers are sized at module start or in
init()from a param'smax; per-bar allocation is the one pattern that both slows a long history and trips the no-growth rule.
Example: the caps as contracts
A book-imbalance Indicator that sizes everything from its declarations:
max_depth bounds the fetch, max_cells is exactly twice it, the cell
buffer comes from the generated capacity, and the readout slot is sized
for its longest line:
import { book, input, line, lower, ohlcv, output, render, string } from "./sdk/declare";
import { in_book_capacity, in_book_cells, in_book_read, in_close } from "./gen/inputs";
import { emitRow, out_imbalance } from "./gen/outputs";
import { sb_clear, sb_int, sb_text, str_depth_sb } from "./gen/strings";
input("close", ohlcv.close);
// max_depth caps each side at 20 levels, so a bar can never carry more than 40 tuples:
// max_cells is a contract, and a bigger block would refuse the whole evaluation.
input("book", book.cells, { max_cells: 40, block_size: 10, max_depth: 20 });
output("imbalance", line, lower, { unit: "ratio" });
// The longest line is "20 bids / 20 asks" (17 bytes); 32 leaves room and stays under the 4096 cap.
string("depth", { max_bytes: 32 });
render.text("depth_mark", { y: "imbalance", text: "depth", size: 10 });
// Sized once from the generated capacity: max_cells x 3 cells per [price, size, side] tuple.
const cells = new StaticArray<f64>(in_book_capacity);
let imbalance: f64 = NaN;
let bids: i32 = 0;
let asks: i32 = 0;
export function init(): void {}
export function state(): i32 {
in_close();
const n = in_book_cells();
if (n <= 0) return 0;
if (in_book_read(i32(changetype<usize>(cells))) < 0) return 0;
let bidSize = 0.0;
let askSize = 0.0;
bids = 0;
asks = 0;
for (let i = 0; i + 2 < n; i += 3) {
if (cells[i + 2] > 0.0) {
bidSize += cells[i + 1];
bids += 1;
} else {
askSize += cells[i + 1];
asks += 1;
}
}
const total = bidSize + askSize;
imbalance = total > 0.0 ? (bidSize - askSize) / total : NaN;
return isNaN(imbalance) ? 0 : 1;
}
export function finalize(): void {
out_imbalance(imbalance);
sb_clear();
sb_int(bids);
sb_text(" bids / ");
sb_int(asks);
sb_text(" asks");
str_depth_sb();
emitRow();
}
export function reset(): void {
imbalance = NaN;
bids = 0;
asks = 0;
}Pick the venue's bucket width for block_size first:
om block-sizes --exchange BINANCE_FUTURES