# Quickstart

Ten minutes from nothing to a plug-in that passes its checks: download the tool, start a folder, teach it your file, check it, preview your days, pack it.

You will build Harbour Tides: the day's high and low water on your days. You
need Node 24 and a terminal. Every command here runs on every build of this
site, and the output below is what it printed.

## 1. Download the tool

```sh download
curl -O https://kanboombo.com/developers/plugins/kanboombo-plugin.mjs
curl -O https://kanboombo.com/developers/plugins/kanboombo-plugin.mjs.sha256
shasum -a 256 -c kanboombo-plugin.mjs.sha256
```

`shasum` should say `kanboombo-plugin.mjs: OK`.

## 2. Start a plug-in

```sh run
node kanboombo-plugin.mjs init harbour-tides --timezone Pacific/Auckland
```

```text output
Created harbour-tides/, a value plug-in in Pacific/Auckland:
  plugin.json                the manifest: what it downloads, what it writes, when it runs
  fetch.mjs                  the fetcher: reads data.csv, prints records
  fixtures/sources/data.csv  a stand-in for your source
  fixtures/context.json      the moment check runs at: 2026-10-01 in Pacific/Auckland
  fixtures/records.jsonl     what fetch.mjs prints for it: 5 records
  README.md                  what to change next
  AGENTS.md                  the road to the store, for your agent
  CLAUDE.md                  points Claude Code at AGENTS.md
Next: node kanboombo-plugin.mjs check harbour-tides
```

The time zone decides your plug-in's today: Auckland's 1 October starts while
it is still 30 September in UTC, so `context.json` says both. `AGENTS.md` is
for [your agent](/developers/plugins/#agent).

## 3. Check it

```sh run
node kanboombo-plugin.mjs check harbour-tides
```

```text output
✔ harbour-tides: 5 records, contract 1
  digest 24baf84eec683978e967b7ebd8eaaf0a274308267ce5781db042bad5cd58a406
```

It ran `fetch.mjs` on the stand-in with no network, checked every record, and
matched `fixtures/records.jsonl`. The digest is what an approval covers: one
changed character of code changes it.

## 4. Teach it your file

Your source is a tide table, a row a day. Save a real copy as the fixture,
the file every check reads instead of the network:

```csv file=harbour-tides/fixtures/sources/tides.csv
date,high,low
2026-09-27,2.51,0.34
2026-09-28,2.47,0.35
2026-09-29,2.43,0.42
2026-09-30,2.29,0.50
2026-10-01,2.20,0.63
2026-10-02,2.06,0.74
```

The last row predicts tomorrow. No record may be dated after today, so the
fetcher will leave it out. Now say what you download and write:

```json file=harbour-tides/plugin.json
{
  "$schema": "https://kanboombo.com/developers/plugins/plugin.schema.json",
  "contract": 1,
  "id": "harbour-tides",
  "name": { "en": "Harbour Tides", "es": "Mareas del puerto" },
  "version": "0.1.0",
  "timezone": "Pacific/Auckland",
  "audience": "private",
  "schedule": "15 6 * * *",
  "backfill": "1y",
  "sources": [{ "name": "tides", "url": "https://example.com/tides/{year}.csv", "maxBytes": 65536 }],
  "series": [
    {
      "key": "tide",
      "kind": "value",
      "cadence": "daily",
      "dates": "observation",
      "name": { "en": "High and low tide", "es": "Pleamar y bajamar" },
      "unit": "m",
      "fields": ["high", "low"],
      "fieldNames": {
        "high": { "en": "High", "es": "Pleamar" },
        "low": { "en": "Low", "es": "Bajamar" }
      },
      "change": "high",
      "snapshot": { "back": "30d", "ahead": "0d" }
    }
  ],
  "licence": { "holder": "Your harbour", "attribution": "Harbour", "terms": null, "retain": "unknown" }
}
```

And a fetcher that reads it:

```js file=harbour-tides/fetch.mjs
#!/usr/bin/env node
// Harbour Tides: reads tides.csv (date,high,low) and prints one value record a day.
import { readFileSync } from "node:fs";

function fail(reason) {
  process.stderr.write(`harbour-tides: ${reason}\n`);
  process.exit(2);
}

const context = JSON.parse(readFileSync(0, "utf8"));
const { today } = context; // the date in Pacific/Auckland, worked out for you
const file = context.sources.tides; // read the path you are given; never build it
if (!file) fail("the run context names no source called tides");

const [header, ...rows] = readFileSync(file, "utf8").trim().split("\n").map((line) => line.split(","));
const at = Object.fromEntries(header.map((name, index) => [name.trim(), index]));
for (const column of ["date", "high", "low"]) if (at[column] === undefined) fail(`tides.csv has no column named ${column}`);

for (const cells of rows) {
  const date = cells[at.date];
  const high = Number(cells[at.high]);
  const low = Number(cells[at.low]);
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(high) || !Number.isFinite(low)) fail(`tides.csv has a row it cannot read: ${cells.join(",")}`);
  if (date > today) continue; // a prediction, not an observation
  process.stdout.write(`${JSON.stringify({ kind: "value", series: "tide", date, fields: { high, low } })}\n`);
}
```

Remove the old stand-in, and check again:

```sh run exit=1
rm harbour-tides/fixtures/sources/data.csv
node kanboombo-plugin.mjs check harbour-tides
```

```text output
Kanboombo plug-in checks: harbour-tides
✖ PLG-FIXTURE-MISMATCH  fixtures/records.jsonl: fixtures/records.jsonl line 1: expected {"kind":"value","series":"main","date":"2026-09-27","fields":{"value":10.2}}, got {"kind":"value","series":"tide","date":"2026-09-27","fields":{"high":2.51,"low":0.34}}
  Fix: If the change is intended, run check with --update and read the difference before you keep it.
  https://kanboombo.com/developers/plugins/publish/submission-errors/#plg-fixture-mismatch
```

The check shows the first line that changed. You meant it, so keep it:

```sh run
node kanboombo-plugin.mjs check harbour-tides --update
```

```text output
✔ harbour-tides: 5 records, contract 1
  digest 100cf8a2f84e184343f9afe5bf5d681b9301178c1627e73eba146c046e8e27a7
```

Five records, not six: the prediction stayed out.

## 5. See your days

```sh run
node kanboombo-plugin.mjs preview harbour-tides --days 2026-09-30..2026-10-02
```

```text output
Harbour Tides: Wed 30 Sep to Fri 2 Oct 2026 (language en)
5 records from fixtures/sources; today is 2026-10-01 in Pacific/Auckland.

Wed 30 Sep
  High and low tide · High 2.29, Low 0.50 · −5.76% · Harbour

Thu 1 Oct · today
  High and low tide · High 2.20, Low 0.63 · −3.93% · Harbour

Fri 2 Oct
  (nothing on this day)
```

These are the app's rows, worked out by its own code: the figures under their
names, the change of `high` (never coloured good or bad), your attribution, and
nothing after today. Add `--language es` for your Spanish names, or
`--html days.html` for a page you can open.

## 6. Pack it

```sh run
node kanboombo-plugin.mjs pack harbour-tides
```

```text output
✔ Packed harbour-tides 0.1.0: 8 files, 5 records
  harbour-tides-0.1.0.zip …
  sha256 …
  digest 100cf8a2f84e184343f9afe5bf5d681b9301178c1627e73eba146c046e8e27a7
⚠ README.md is still init's template; write your own before you upload.
```

`pack` checks first and refuses a plug-in that fails. The same files make the
same ZIP on the same Node (size and hash, which follow its compression, are
elided). Before you upload it at `kanboombo://publish`, add a
[case that must fail](/developers/plugins/build/fixtures/): the portal asks for
at least one. [Publish](/developers/plugins/publish/).

## Next

[Harbour Almanac](/developers/plugins/examples/harbour-almanac/) uses all of
contract 1; [Dates and time zones](/developers/plugins/concepts/dates-and-time-zones/)
holds the mistakes everyone makes once.
