# Records

What your fetcher prints, one JSON object a line: values, events and items. Their keys and dates, every rule a run is held to, and how each change is kept.

A record is one JSON object on a line of your fetcher's output, in a series your
manifest declares, of that series' kind. The examples are Harbour Almanac's,
checked against [its manifest](/developers/plugins/examples/harbour-almanac/)
on every build.

| Kind | What it is | Its key |
|---|---|---|
| `value` | Numbers on a date | series and date, and `period` when dated by publication |
| `event` | Something scheduled, that happened, or that was cancelled | series, date and `id` |
| `item` | A dated title with an https link | series, date and `id` |

## Values

```jsonl records manifest=plugins/harbour-almanac/plugin.json today=2026-10-01
{"kind":"value","series":"tide","date":"2026-10-01","fields":{"high":2.2,"low":0.63}}
{"kind":"value","series":"catch","date":"2026-08-10","period":"2026-07","fields":{"landed":466}}
{"kind":"value","series":"catch","date":"2026-09-10","period":"2026-07","fields":{"landed":461.6},"revises":"2026-08-10"}
```

`fields` holds 1 to 16 finite numbers, each a field the series declares; one
left out is not drawn. An **observation** date is the day described: when the
source changes that day's number, print the new one under the same key. A
**publication** date is the day published, `period` says what it covers
(`2026-07`, `2026-Q3`), and a correction is a new record on its own day with
`revises` naming the date it corrects: July's first figure stays on 10 August,
the day it was known. Use observation for rates and readings, publication for
statistics revised in later releases. `period` and `revises` on an observation
refuse the batch.

## Events and items

```jsonl records manifest=plugins/harbour-almanac/plugin.json today=2026-10-01
{"kind":"event","series":"council","id":"meeting-2026-07-1","date":"2026-07-02","state":"happened","title":"Harbour council: berth fees","value":11.5}
{"kind":"event","series":"council","id":"meeting-2026-10-3","date":"2026-10-15","state":"scheduled","title":"Harbour council meeting"}
{"kind":"item","series":"notices","date":"2026-10-01","id":"nm-2026-10-01-1","title":"Tide table for October 2026 published","url":"https://notices.harbour-almanac.example/2026/10/01/1"}
```

- An event's `state` is `scheduled`, `happened` or `cancelled`; when it takes
  place, print it again with the same id and date as `happened`. Its `value`,
  optional, is a number it decided, in the series' `unit`.
- `id` is 1 to 64 characters without spaces, the same every run: the source's
  own, or made from something that stays when the event moves.
- **An event's id on a new date means it moved**, and the old date reads
  `Moved to Thu 22 Oct`. Print each id once a run. An item's id on another date
  is another record.
- A `title` is plain text, at most 200 characters, never markup; an item needs
  one. An item's `url` is https, to a source's host or one `links.hosts`
  declares. Anywhere else refuses the batch:

```jsonl records manifest=plugins/harbour-almanac/plugin.json today=2026-10-01 error=PLG-RECORD-LINK
{"kind":"item","series":"notices","date":"2026-10-01","id":"nm-2026-10-01-1","title":"Tide table for October 2026 published","url":"https://example.com/tides"}
```

## The rules every batch is held to {#rules}

Nothing is after `today` in your zone, except a `scheduled` event
([predictions](/developers/plugins/concepts/dates-and-time-zones/#predictions-are-not-observations)).
Every date is a real `YYYY-MM-DD`. Each kind has its own keys, and any other is
refused, so a misspelt `feilds` never passes quietly. A run prints at most
250,000 records. The Server reads the whole batch against the
approved manifest before writing any: one bad line refuses every line, and the
run fails with its reason. Here two lines share a key:

```jsonl records manifest=plugins/harbour-almanac/plugin.json today=2026-10-01 error=PLG-RECORD-REPEATED
{"kind":"value","series":"tide","date":"2026-10-01","fields":{"high":2.2,"low":0.63}}
{"kind":"value","series":"tide","date":"2026-10-01","fields":{"high":2.21,"low":0.63}}
```

## Revisions and withdrawal {#revisions}

Your fetcher needs no memory: print what the source shows now. In the
transaction that writes the run, the Server compares it with what it stored,
and nothing a reader once saw is overwritten without a trace.

| The run prints | It becomes |
|---|---|
| A new key | A new record |
| A stored key, same content | Nothing |
| A stored key, different content | A revision: the new content, the old kept beside it |
| A stored event `id` on another date | The old date marked moved to the new one |
| A withdrawn key | The record back, as a revision |

Content is a value's fields and `revises`, an event's state, value and title,
an item's title and link. Each record keeps the run, version and digest that
last wrote it.

### Snapshot windows {#snapshot-windows}

A source also changes its mind by leaving things out. Kanboombo notices only
inside a series' `snapshot`, where your run is the whole truth:
`{ "back": "30d", "ahead": "0d" }` is every record from 30 days ago to today.
Inside it, a stored record a successful run does not print counts one miss
(failed runs count nothing); printed again, its count starts over; on the third
miss in a row it is **withdrawn**, hidden with its history kept, and it comes
back as a revision if shown again. A moved event is not counted on its old
date. Outside a window, or with none, nothing is counted missing: Harbour
Almanac's notices have none on purpose. Keep the window inside what your source
shows ([why](/developers/plugins/concepts/dates-and-time-zones/#keep-your-window-inside-your-file)).

### Many at once wait for a person {#held}

A damaged file can look like a source that withdrew a great deal. When one run
would withdraw more than 10 records of a series, or more than 1% of a window
holding 100 or more, none are withdrawn: the rest is written, the plug-in shows
`Held` with the first missing dates, and a person chooses **Accept removals** or
**Keep them** (their count starts over). Until then they stay on readers' days.

## Harbour Almanac, a week later {#a-week-later}

`fixtures/runs/later/`, 8 October, corrects a provisional tide, records a
meeting's outcome and moves another, three revisions:

```jsonl records manifest=plugins/harbour-almanac/plugin.json today=2026-10-08
{"kind":"value","series":"tide","date":"2026-09-28","fields":{"high":2.46,"low":0.35}}
{"kind":"event","series":"council","id":"meeting-2026-10-1","date":"2026-10-01","state":"happened","title":"Harbour council: berth fees","value":11.75}
{"kind":"event","series":"council","id":"meeting-2026-10-3","date":"2026-10-22","state":"scheduled","title":"Harbour council meeting"}
```

A faulty reading gone from inside the window counts one miss. Records stay
when a reader turns a plug-in off or a person removes it; only a
[purge](/developers/plugins/plan/data-rights/#purge) deletes them.

## Every field {#every-field}

### PluginRecord {#pluginrecord}

One line your fetcher prints: a value, an event or an item.

One of: [PluginValueRecord](/developers/plugins/concepts/records/#pluginvaluerecord), [PluginEventRecord](/developers/plugins/concepts/records/#plugineventrecord), [PluginItemRecord](/developers/plugins/concepts/records/#pluginitemrecord).

### PluginValueRecord {#pluginvaluerecord}

Numbers on a date.

| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"value"` | yes | value |
| `series` | [PluginKey](/developers/plugins/reference/manifest/#pluginkey) | yes | A value series the manifest declares. |
| `date` | [PluginDate](/developers/plugins/concepts/records/#plugindate) | yes | The date it describes (observation) or was published (publication). Never after today. |
| `period` | string; at most 32 characters |  | For publication dates: what it describes, such as 2026-08 or 2026-Q3. Empty for observations. |
| `fields` | object of number | yes | The series' fields and their numbers. Finite numbers only. |
| `revises` | [PluginDate](/developers/plugins/concepts/records/#plugindate) |  | For publication dates: the date of the earlier record this one corrects. |

### PluginEventRecord {#plugineventrecord}

Something scheduled, that happened, or that was cancelled.

| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"event"` | yes | event |
| `series` | [PluginKey](/developers/plugins/reference/manifest/#pluginkey) | yes | An event series the manifest declares. |
| `id` | string; 1 to 64 characters | yes | Your id for the event, stable across runs: the same id on a new date moves it. 1 to 64 characters, no spaces. |
| `date` | [PluginDate](/developers/plugins/concepts/records/#plugindate) | yes | The day it happens. Only a scheduled event may be after today. |
| `state` | [PluginEventState](/developers/plugins/concepts/records/#plugineventstate) | yes | scheduled, happened or cancelled. |
| `value` | number |  | A number it decided, such as a rate. Optional. |
| `title` | string; at most 200 characters |  | Plain text, 200 characters or fewer. Never markup. |

### PluginItemRecord {#pluginitemrecord}

A dated link with a title, such as a press release.

| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"item"` | yes | item |
| `series` | [PluginKey](/developers/plugins/reference/manifest/#pluginkey) | yes | An item series the manifest declares. |
| `date` | [PluginDate](/developers/plugins/concepts/records/#plugindate) | yes | The day it was published. Never after today. |
| `id` | string; 1 to 64 characters | yes | Your id for it, stable across runs. 1 to 64 characters, no spaces. |
| `title` | string; 1 to 200 characters | yes | Plain text, 1 to 200 characters. |
| `url` | string; at most 1,000 characters | yes | An https link to a source's host or one links.hosts declares. |

### PluginEventState {#plugineventstate}

scheduled may be dated in the future; happened and cancelled may not.

- Type: `"scheduled"`, `"happened"`, `"cancelled"`

### PluginDate {#plugindate}

A calendar date, YYYY-MM-DD. It is never converted between time zones.

- Type: string
- Pattern: `^[0-9]{4}-[0-9]{2}-[0-9]{2}$`
- Example: `2026-09-28`
