---
title: "First steps"
description: "Write your first Indicator and see a line appear on the chart. No prior experience with Indicators, or with kScript (legacy), is needed: by the end of this…"
order: 5
section: "getting-started"
---

<!-- source: docs/indicators/getting-started/primer-first-steps.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# First steps

Write your first Indicator and see a line appear on the chart. No prior
experience with Indicators, or with kScript (legacy), is needed: by the end
of this short page you will have a moving average drawing on a live chart,
compiled in your own browser, and you will know the three parts every
Indicator shares.

## What is an Indicator?

An Indicator is one TypeScript file. You write it in the chart's editor,
**Run** compiles it in your browser and draws it on the chart, and the same
file publishes to the registry and runs on your own machine unchanged.
Every output is a number series: a line, a shape, a box coordinate, a
metric an alert can watch. The module has no filesystem, no network, and
no order capability; the host feeds it one bar at a time and reads the
numbers it writes back.

Nothing to install: you write the file in the browser and it runs against
live and historical market data immediately (the terminal path, the same
file with the `om` CLI, is `quick-start.md`).

## Where it lives

1. On the chart toolbar, open **Indicators**. The dialog opens on the
   **Indicators** tab: the packages you can add straight to the chart.
2. The **kScript (legacy)** tab holds **My Scripts**, **Community**, and
   **Marketplace**: the scripts that run on the kScript engine.
3. **Build** opens the editor. In its sidebar, **New indicator** starts a
   TypeScript Indicator; **New kScript (legacy)** starts a kScript.

A kScript row that has a proven Indicator port shows a **New engine**
badge. Adding that row mounts the Indicator by default; **Use kScript
engine** on the row adds the kScript version instead and keeps the kScript
engine for that indicator.

Everything happens in the editor. Type the file, press **Run**, and your
Indicator appears on (or below) the chart right away. Change a line, press
Run again, and the chart updates. That tight loop is the whole workflow:
write, run, look at the chart, repeat.

## The shape of every Indicator

Before writing anything, here is the mental model. Every Indicator has the
same three moving parts:

1. **Declarations.** A few plain statements at the top of the file say what
   the Indicator reads and writes: `param(...)` for a setting the chart
   user can change, `input(...)` for a per-bar number the chart feeds in,
   `output(...)` for a per-bar number the Indicator sends back and the
   chart draws. No output, nothing on the chart.
2. **State.** Module-level variables hold whatever must survive from one
   bar to the next: a moving average's window, the previous close, a
   counter. There is no history array to index; what you want to remember,
   you keep.
3. **Four functions.** `init()` runs once before the first bar, `state()`
   runs once per bar, `finalize()` writes the outputs for a bar that has a
   value, and `reset()` puts the state back to its starting point. The
   host calls them; you never call them yourself.

There is one more rule, and it is easy: **the first line of the file is the
language marker `//@lang=wrun-ts`.** It is how the editor knows the tab
holds an Indicator rather than a kScript. Leave it alone; everything you
write goes under it.

## Step 1: New indicator

The editor opens a file whose first line is the language marker:

```text
//@lang=wrun-ts
```

Everything under it is the `sma-codefirst` template, a complete moving
average in 25 non-blank lines, a comment on every statement. Read it top to
bottom, then press Run:

```typescript
// A simple moving average, declared and computed in one file. Read it top to bottom.
import { input, line, ohlcv, output, overlay, param } from "./sdk/declare"; // the declaring words
import { in_close } from "./gen/inputs"; // generated: an in_ reader for each input declared below
import { emitRow, out_sma } from "./gen/outputs"; // generated: an out_ writer per output, plus emitRow
import { p_period } from "./gen/params"; // generated: a p_ reader for each param declared below
import { Sma } from "./sdk/ta"; // a moving-average helper that keeps its own window of values

// A param is a setting the chart user can change: its name, its default, then the allowed range.
param("period", 20, { min: 1, max: 200 });
// An input is a per-bar number the chart feeds in; ohlcv.close is each candle's close price.
input("close", ohlcv.close);
// An output is a per-bar number the Indicator sends back, here drawn as a line on the price panel.
// output() returns a handle; bind it with a const only when a box() or segment() needs to name it.
output("sma", line, overlay);

let sma = new Sma(20); // module state lives between bars; init() rebuilds it at the chosen period
let value: f64 = NaN; // this bar's average (f64 is the number type), NaN until the window is full

// init() runs once before the first bar: read each setting through its p_ reader (i32() = whole bars).
export function init(): void { sma = new Sma(i32(p_period())); }
// state() runs once per bar: read the bar through its in_ reader and fold it into the window.
// Warmup rows are NaN: until the window holds period bars there is no average yet, so return 0
// and the chart draws nothing for that bar; return 1 once the value is real.
export function state(): i32 { value = sma.update(in_close()); return isNaN(value) ? 0 : 1; }
// finalize() runs after state() returns 1: write each output through its out_ writer, then emitRow().
export function finalize(): void { out_sma(value); emitRow(); }
// reset() runs when the chart restarts the series: put the state back to what init() built.
export function reset(): void { sma.reset(); value = NaN; }
```

All three parts are there. Let's walk through them.

## Read the template

Three declarations at the top say what the Indicator reads and writes. They
are plain statements, extracted from the file before it compiles, so every
name must be a string literal and every default a literal number:

- `param("period", 20, { min: 1, max: 200 })` is a setting. It shows in
  the overlay's settings dialog with that default and range.
- `input("close", ohlcv.close)` is a feed: the chart's own close price, one
  value per bar.
- `output("sma", line, overlay)` is what the chart draws: a line on the
  price pane. `lower` instead of `overlay` puts it in its own pane; `none`
  instead of `line` computes it without drawing it.

The generated imports (`./gen/params`, `./gen/inputs`, `./gen/outputs`) are
how the code reads and writes those names: `p_period()`, `in_close()`,
`out_sma(value)`. Rename a declaration and its accessor renames with it; the
compiler then points at every stale import.

Four functions do the work, and the build refuses any other signature:

| Function | Runs | Do this here |
| --- | --- | --- |
| `init()` | once, before the first bar | read params, size your averages |
| `state()` | once per bar | read inputs, update your state, return `1` when the bar has a value or `0` while warming up |
| `finalize()` | once per bar that returned `1` | write every output, then `emitRow()` last |
| `reset()` | when the chart replays the forming bar | clear every module-level variable and `.reset()` every TA object |

Every TA class (`Sma`, `Ema`, `Rsi`, `Cross`, and the rest of the kScript
catalog) comes from `./sdk/ta`: construct them in `init()`, feed them one value per bar with
`.update()`, and they return `NaN` until their window is full. Returning `0`
from `state()` on those bars is what keeps the line empty there instead of
drawing garbage (`core-concepts/execution-model.md`).

## Step 2: Run

Press **Run**. The editor compiles the file in your browser (the same
compiler version the CLI uses, so the bytes match) and mounts the draft on
the chart as an overlay. Errors show as squiggles under the offending
characters and as rows in the Problems lane; fix them and Run again. The
build has a few stages, so a message may come from the declaration
extractor, the sheet validator, or the compiler; `faq/common-errors.md`
lists the ones you will meet.

Indicators run in your browser: every input reads the chart's own market
and interval, the forming bar re-evaluates on every tick, and nothing is
sent anywhere to compute.

## What you'll see

A line tracing the 20-bar average of the close, sitting right on the price
chart. The first nineteen bars of loaded history are empty: the window is
not full yet, `state()` returned `0`, and the chart drew nothing. From the
twentieth bar on the line follows price, smoothed. It is not a fancy
indicator, but it proves the whole pipeline works: your code received real
bars, computed a number for each, and the chart drew them. That is the
foundation for everything else.

## One small thing to notice

Look at the `output` line again: `output("sma", line, overlay)`. **Every
output needs a name**, and the name does three jobs at once. It is the
legend entry on the chart, it is the writer you call in `finalize()`
(`out_sma`), and once the Indicator is published and installed it is the
tail of a metric id (`wrun/@you/my-sma/sma`) that an alert, a screen, or
a backtest can read. Names are unique per Indicator and lowercase with
underscores; pick clear ones from the start.

That's it. You have a running Indicator. If your line showed up, the hard
part is over. Everything from here is doing more interesting things with
the same three pieces.

## Next

Now let's turn this into something you would actually use. On the next
page we build a real indicator step by step: a moving-average crossover
signal with a mark on every cross.

[Build your first Indicator](primer-first-indicator.md)
