Developers · Get started

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
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
node kanboombo-plugin.mjs init harbour-tides --timezone Pacific/Auckland
text
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.

3. Check it

sh
node kanboombo-plugin.mjs check harbour-tides
text
✔ 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
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
{ "$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
#!/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
rm harbour-tides/fixtures/sources/data.csv node kanboombo-plugin.mjs check harbour-tides
text
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
node kanboombo-plugin.mjs check harbour-tides --update
text
✔ harbour-tides: 5 records, contract 1 digest 100cf8a2f84e184343f9afe5bf5d681b9301178c1627e73eba146c046e8e27a7

Five records, not six: the prediction stayed out.

5. See your days

sh
node kanboombo-plugin.mjs preview harbour-tides --days 2026-09-30..2026-10-02
text
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
node kanboombo-plugin.mjs pack harbour-tides
text
✔ 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: the portal asks for at least one. Publish.

Next

Harbour Almanac uses all of contract 1; Dates and time zones holds the mistakes everyone makes once.