feature

Definition examples

Three runnable definition scripts, simple → complex. Each marks setups for Collect — they do not measure outcomes (that is a study).

In Code the API word is flow (kind: "flow"). In the product UI the noun is always Definition.

Simple → complex

#

Example

Teaches

1

Bull bar

events.declare, required label, one-line condition

2

EMA cross

indicators.declare, cross detection with accum

3

Session opening-range break

Session object, OR state, oncePer


1. Bull bar

Marks every bar that closes above its open. Payload carries bar range for later studies.

What it defines

A Collectable event bull_bar whenever close > open.

Key decisions

Piece

Why it matters

kind: "flow"

Enables emit / Collect (not an indicator).

events.declare + payload schema

Contract for Collect and studies.

Required label on ctx.emit

Events without labels are dropped at runtime.

Early return on bear bars

Keep onBar cheap when the setup is false.

Full script

meta({
  name: "Bull bar",
  kind: "flow",
  slug: "bull-bar",
  shortName: "BullBar",
});

events.declare({
  id: "bull_bar",
  intent: "Mark closes above open",
  payload: {
    range: { type: "number", description: "High minus low" },
    close: { type: "number", description: "Close price" },
  },
  paint: "pin",
});

outputs.declare({
  id: "close_line",
  kind: "line",
  name: "Close",
  color: "#64b5f6",
});

export function onBar(ctx) {
  ctx.plot("close_line", ctx.close);

  if (ctx.close <= ctx.open) return;

  const range = ctx.high - ctx.low;
  ctx.emit(
    "bull_bar",
    { range, close: ctx.close },
    {
      label: `Bull · R=${range.toFixed(2)}`,
      subtitle: `close ${ctx.close.toFixed(2)}`,
      price: ctx.close,
      timeStart: ctx.time,
    }
  );
}

2. EMA cross

Fires when a fast EMA crosses above a slow EMA. Uses the public Moving Average indicator as a dependency instead of re-implementing EMA math.

What it defines

Event ema_cross_up on the bar where fast MA crosses from at-or-below slow MA to above it.

Key decisions

Piece

Why it matters

Two indicators.declare with different as / settings

Same slug (ma) twice with locked periods.

Guard fast == null \|\| slow == null

Deps are empty during warmup — skip, do not emit.

Read-then-write ctx.accum

Identity read returns last bar’s values; second call stores today’s.

oncePer: { keys: ["session"], windowBars: 1 }

At most one emit per session window (optional throttle).

Full script

meta({
  name: "EMA cross up",
  kind: "flow",
  slug: "ema-cross-up",
  shortName: "EmaXUp",
});

indicators.declare({
  slug: "ma",
  as: "fast",
  version: 1,
  settings: { period: 9, maType: "EMA" },
});

indicators.declare({
  slug: "ma",
  as: "slow",
  version: 1,
  settings: { period: 21, maType: "EMA" },
});

events.declare({
  id: "ema_cross_up",
  intent: "Fast EMA crosses above slow EMA",
  payload: {
    fast: { type: "number", description: "Fast EMA" },
    slow: { type: "number", description: "Slow EMA" },
    close: { type: "number", description: "Close at cross" },
  },
  paint: "pin",
  oncePer: { keys: ["session"], windowBars: 1 },
});

outputs.declare({ id: "fast_line", kind: "line", name: "Fast", color: "#26a69a" });
outputs.declare({ id: "slow_line", kind: "line", name: "Slow", color: "#ef5350" });

export function onBar(ctx) {
  const fast = ctx.indicators.fast?.value;
  const slow = ctx.indicators.slow?.value;
  if (fast == null || slow == null) return;

  ctx.plot("fast_line", fast);
  ctx.plot("slow_line", slow);

  // Identity update returns the stored prior values without changing them.
  const prevFast = ctx.accum("prevFast", null, (prev) => prev);
  const prevSlow = ctx.accum("prevSlow", null, (prev) => prev);

  if (prevFast != null && prevSlow != null && prevFast <= prevSlow && fast > slow) {
    ctx.emit(
      "ema_cross_up",
      { fast, slow, close: ctx.close },
      {
        label: "EMA cross ↑",
        subtitle: `${fast.toFixed(2)} > ${slow.toFixed(2)}`,
        price: ctx.close,
        timeStart: ctx.time,
      }
    );
  }

  ctx.accum("prevFast", null, () => fast);
  ctx.accum("prevSlow", null, () => slow);
}

Pin version to the library MA version you resolve in the builder when you can — agents should call resolve_dependency_contract before inventing shapes. See Declaring indicator dependencies.


3. Session opening-range break

Builds a session opening range from the first N bars after session open, then emits once when price breaks the high (or low) after the range is locked.

What it defines

Events or_break_up / or_break_dn with the OR high/low and break price — one break direction per session via oncePer.

Key decisions

Piece

Why it matters

ctx.session({…})

Definition session object (didOpen, key, …) — not indicator isStart.

OR state in ctx.accum

Remembers high/low/bar count across bars; resets on didOpen.

Emit only after range locked

Avoid firing during the building window.

Separate event ids + oncePer on session

One up and one down break max per session, no hand-rolled dedupe.

Full script

meta({
  name: "OR break",
  kind: "flow",
  slug: "or-break",
  shortName: "ORBreak",
});

events.declare({
  id: "or_break_up",
  intent: "Close breaks above session opening range",
  payload: {
    orHigh: { type: "number", description: "Opening range high" },
    orLow: { type: "number", description: "Opening range low" },
    close: { type: "number", description: "Break close" },
    sessionKey: { type: "string", description: "Session key" },
  },
  paint: "pin",
  oncePer: { keys: ["session"] },
});

events.declare({
  id: "or_break_dn",
  intent: "Close breaks below session opening range",
  payload: {
    orHigh: { type: "number", description: "Opening range high" },
    orLow: { type: "number", description: "Opening range low" },
    close: { type: "number", description: "Break close" },
    sessionKey: { type: "string", description: "Session key" },
  },
  paint: "pin",
  oncePer: { keys: ["session"] },
});

outputs.declare({ id: "or_high", kind: "line", name: "OR high", color: "#26a69a" });
outputs.declare({ id: "or_low", kind: "line", name: "OR low", color: "#ef5350" });

const OR_BARS = 5;

export function onBar(ctx) {
  const s = ctx.session({});

  const st = ctx.accum("or", null, (prev) => {
    if (s.didOpen || prev == null) {
      return {
        key: s.key,
        bars: 1,
        high: ctx.high,
        low: ctx.low,
        locked: false,
      };
    }
    if (prev.locked) return prev;
    const bars = prev.bars + 1;
    const high = Math.max(prev.high, ctx.high);
    const low = Math.min(prev.low, ctx.low);
    return {
      key: s.key,
      bars,
      high,
      low,
      locked: bars >= OR_BARS,
    };
  });

  if (!st.locked) return;

  ctx.plot("or_high", st.high);
  ctx.plot("or_low", st.low);

  const base = {
    orHigh: st.high,
    orLow: st.low,
    close: ctx.close,
    sessionKey: st.key,
  };

  if (ctx.close > st.high) {
    ctx.emit("or_break_up", base, {
      label: "OR break ↑",
      subtitle: `H ${st.high.toFixed(2)}`,
      price: ctx.close,
      timeStart: ctx.time,
    });
  } else if (ctx.close < st.low) {
    ctx.emit("or_break_dn", base, {
      label: "OR break ↓",
      subtitle: `L ${st.low.toFixed(2)}`,
      price: ctx.close,
      timeStart: ctx.time,
    });
  }
}

Tune SessionOpts in IntelliSense for your market (exchange RTH vs named London/NY sessions). See Sessions.

Next

  1. Emit and labels

  2. Events and snapshots

  3. First definition guide