# The `.pumapack` file format (PumaBoard, schema 1)

This document describes the files PumaBoard imports, in enough detail to
**edit an export** or **generate one from scratch** so that it imports cleanly:
no error, no silently hidden cards, nothing dropped. It is written for a
reader, human or AI, who has no access to the app's source.

PumaBoard reads two shapes of file. They are different, so check which one you
have:

| Shape | How you get one | File name | Covered in |
|---|---|---|---|
| **Backup file** | The topbar **Export** button, `Cmd/Ctrl+S`, or a board tab's right-click **Export this board (JSON)** | `pumaboard-2026-09-28.json`, `pumaboard-backup.json`, `pumaboard-<board>-<date>.json` | §2 to §8 and the example in §12 |
| **`.pumapack`** | A board tab's right-click **Export as .pumapack** | `pumaboard-<board>-<date>.pumapack` | §9 |

**Use the backup file** when you want an AI to read or edit your boards. It is
PumaBoard's own shape, it maps one-to-one onto what you see, and every field
survives a round trip. The `.pumapack` is a translation into a task-list shape
that PumaTracker can also read; it round-trips too, but it is harder to write
by hand.

Both are UTF-8 JSON. PumaBoard imports either one in either of two ways:

- from the topbar **Import** button;
- by dropping the file anywhere on the window.

PumaBoard also imports PumaTracker workspace files, and `.pumapack` files made
by PumaTracker, PumaLogger, PumaNoter and PumaGRC2, converting each into a
board. Those formats belong to the other apps and are not described here.

---

## 1. The short version

If you only read one section, read this one.

1. Wrap your boards in the envelope from §2: `"app": "pumaboard"` and a
   `boards` array. A bare board object is rejected.
2. Write **every field** of every record, using the shapes in §4. Use `""`,
   `[]`, `false` or `null` exactly where §4 says. **Never put `null` inside an
   array** and never give an array field a non-array value; either one can
   stop the import with no message and, with Replace all, lose the user's
   existing boards (§10).
3. Every card's `listId` must be the `id` of a list **on the same board**, or
   the card is stored but never shown.
4. Every entry in a card's `labelIds` must be the `id` of a label in the same
   board's `labelPalette`.
5. Order is set by `position` numbers, not by array order: lists left to
   right, cards top to bottom within their list. Use `1`, `2`, `3` … (§6).
6. Due dates are `"YYYY-MM-DD"` or `null`. Nothing else.
7. There is no "done" flag on a card. A card is done because of the list it
   sits in (for example a list called "Done"). Checklist items do have `done`.
8. Every id on a board, list, card, checklist and checklist item is **replaced
   with a new random id** on import. Only label ids are kept. Ids just need to
   be unique and consistent within your file (§7).
9. Importing always asks **Merge** or **Replace all**. Replace all deletes
   every board the user has, including boards that are not in the file (§3).
10. Check the result against the checklist in §11.

§12 is a complete, valid example you can copy and adapt.

---

## 2. The envelope (backup file)

```json
{
  "app": "pumaboard",
  "version": 1,
  "exported": "2026-09-28T09:00:00.000Z",
  "deviceId": "",
  "theme": "dark",
  "accent": null,
  "activeBoardId": "b-garden",
  "boards": [ { "...one board object, see §4.1..." } ]
}
```

| Key | Value | Notes |
|---|---|---|
| `app` | `"pumaboard"` | **Required**, exactly this string. |
| `boards` | array of board objects | **Required**, must be an array. See §4.1. |
| `version` | `1` | The data schema. Not checked on import; write `1`. |
| `exported` | ISO 8601 datetime | Informational; ignored on import. |
| `deviceId` | string | The exporting browser's random id. Ignored on import; `""` is fine. |
| `theme` | `"dark"` or `"light"` | Applied only on **Replace all**. Any other value is ignored. |
| `accent` | a hex color such as `"#e07830"`, or `null` | The app-wide accent. Applied only on **Replace all**. `null` leaves it alone. |
| `activeBoardId` | a board id | Ignored on import (§3). |

The full backup (topbar Export, `Cmd/Ctrl+S`) writes all eight keys. A
single-board export writes only `app`, `version`, `exported`, `deviceId` and a
one-element `boards`. Both import the same way.

What the importer actually requires:

- The file is valid JSON.
- `app` is `"pumaboard"` and `boards` is an array.

If either fails, and the file is not a `.pumapack` or PumaTracker file either,
the toast reads: *"Invalid file (not a PumaBoard export, PumaTracker file, or
.pumapack)."* The same toast is shown for a file that is not JSON at all.

The importer does **not** check anything inside `boards`. A malformed board is
only discovered part-way through the import (§10).

---

## 3. What the user sees on import: Merge or Replace all

Every successful read opens a dialog, **Import boards**: *"This file contains
N board(s). How should they be imported?"*, with three buttons.

| Button | What happens | Toast |
|---|---|---|
| **Cancel** | Nothing. | none |
| **Merge** | Each board in the file is added as a new board after the existing ones. Its name gets **" (imported)"** appended. The board the user was looking at stays active. `theme` and `accent` are ignored. | *"Boards merged."* |
| **Replace all** | **Every existing board is deleted first**, then the file's boards are added under their own names, numbered 1, 2, 3 … in file order. The first board in the file becomes the active one. `theme` and `accent` are applied if present. | *"Workspace replaced."* |

In both cases:

- every board, list, card, checklist and checklist item gets a **new random
  id**, and every card's `listId` is rewritten to match its list's new id;
- label ids are kept exactly as written;
- the board's `position` is overwritten (see above);
- the board's `schemaVersion` is set to `1`;
- everything else, including `createdAt`, `updatedAt` and any unknown keys, is
  stored exactly as written. Nothing is re-stamped.

A first-time user opens PumaBoard to two sample boards ("Personal Life" and
"Side Project"). Replace all clears those along with everything else.

---

## 4. Record shapes

### Conventions for every record

- **`id`** is any non-empty string, unique within its board. It is replaced on
  import, so readable ids (`ls-todo`, `cd-soil`) are fine.
- **Timestamps** (`createdAt`, `updatedAt`) are full ISO 8601 datetimes, e.g.
  `"2026-09-28T09:00:00.000Z"`.
- **Nothing is defaulted.** The importer does not fill in missing fields. A
  missing field stays missing, and the app copes with some omissions but not
  all (§10). Write every field.
- **Enum-like values are not validated.** A color that is not a color, or a
  date in the wrong format, is kept as written and displays wrongly.

### 4.1 Board

```json
{
  "id": "b-garden",
  "name": "Community garden build",
  "accentColor": "#5ecc94",
  "position": 1,
  "createdAt": "2026-09-14T10:00:00.000Z",
  "updatedAt": "2026-09-27T16:30:00.000Z",
  "schemaVersion": 1,
  "labelPalette": [ ],
  "lists": [ ],
  "cards": [ ]
}
```

| Field | Type | Meaning |
|---|---|---|
| `id` | string | Replaced on import. |
| `name` | string | Shown on the board's tab. |
| `accentColor` | hex color | The board's color: tab underline and the stripe under the toolbar. The app offers `#2cb39a`, `#5b8af0`, `#a880e8`, `#e07830`, `#5ecc94`, `#d4a464`, `#e05050`, `#4ec9b0`; any `#rgb` or `#rrggbb` works. |
| `position` | number | Tab order. **Overwritten on import** (§3). |
| `createdAt`, `updatedAt` | ISO datetime | Kept as written. The app stamps `updatedAt` whenever the board is edited. |
| `schemaVersion` | `1` | Set to `1` on import whatever you write. |
| `labelPalette` | array of labels | The board's label set. See §4.2. `[]` for a board with no labels. |
| `lists` | array of lists | See §4.3. **Must be an array.** |
| `cards` | array of cards | Every card on the board, in every list, archived or not. See §4.4. **Must be an array.** |

### 4.2 `labelPalette[]`

```json
{ "id": "lb-materials", "name": "materials", "color": "#d4a464" }
```

| Field | Type | Meaning |
|---|---|---|
| `id` | string | **Kept on import.** Cards refer to it in `labelIds`. Unique within the board. |
| `name` | string | Shown on hover and in the filter chips, Markdown and CSV exports. Short, lower case by convention. |
| `color` | hex color | The label chip color. |

Labels belong to one board. Two boards can use the same label ids without
conflict.

### 4.3 `lists[]`

```json
{ "id": "ls-doing", "name": "Doing", "position": 2, "wipLimit": 3 }
```

| Field | Type | Meaning |
|---|---|---|
| `id` | string | Replaced on import. Cards refer to it in `listId`. |
| `name` | string | The column heading. |
| `position` | number | Left-to-right order: lower is further left. See §6. |
| `wipLimit` | positive integer, or `null` | Work-in-progress limit. The count badge shows `visible/limit` and turns red when the list holds more non-archived cards than the limit. Nothing is blocked. `null` means no limit. |

### 4.4 `cards[]`

```json
{
  "id": "cd-soil",
  "listId": "ls-todo",
  "position": 1,
  "title": "Order raised-bed soil",
  "description": "",
  "labelIds": ["lb-materials"],
  "dueDate": "2026-10-09",
  "checklists": [],
  "notesLog": "",
  "archived": false,
  "coverColor": null,
  "createdAt": "2026-09-14T10:05:00.000Z",
  "updatedAt": "2026-09-26T08:12:00.000Z"
}
```

| Field | Type | Meaning |
|---|---|---|
| `id` | string | Replaced on import. |
| `listId` | list id | The list the card sits in. **Must match a list on the same board**, or the card is never shown (§10). |
| `position` | number | Top-to-bottom order within its list: lower is higher. See §6. |
| `title` | string | The card's one-line title. `"(untitled)"` is what the app writes for a blank one. |
| `description` | string | Markdown: headings, bold, italics, lists, links, block quotes, tables, fenced code. Use `\n` for line breaks. `""` for none. |
| `labelIds` | array of label ids | Labels on the card, in display order. Each must match an `id` in the board's `labelPalette`. `[]` for none. |
| `dueDate` | `"YYYY-MM-DD"` or `null` | See §6. |
| `checklists` | array of checklists | See §4.5. `[]` for none. |
| `notesLog` | string | A running, timestamped notes log. See §6. `""` for none. |
| `archived` | boolean | `true` hides the card from the board (§6). |
| `coverColor` | hex color, or `null` | A colored stripe across the top of the card. `null` for none. |
| `createdAt`, `updatedAt` | ISO datetime | Kept as written. The app stamps `updatedAt` when the card is edited. |

### 4.5 `checklists[]` and `items[]`

```json
{
  "id": "ck-build",
  "name": "Build steps",
  "items": [
    { "id": "it-cut", "text": "Cut boards to length", "done": true },
    { "id": "it-frame", "text": "Screw the frames together", "done": false }
  ]
}
```

| Field | Type | Meaning |
|---|---|---|
| `id` | string | Replaced on import. |
| `name` | string | The checklist's heading, e.g. `"Checklist"`, `"Acceptance criteria"`. |
| `items` | array | The items in display order (array order; items have no `position`). |
| `items[].id` | string | Replaced on import. |
| `items[].text` | string | The item. |
| `items[].done` | boolean | Ticked or not. |

A card can hold several checklists. The card on the board shows one combined
count, `☑ done/total`, across all of them.

---

## 5. Cross-references

All references stay within one board:

| From | Field | To |
|---|---|---|
| card | `listId` | `lists[].id` on the same board |
| card | `labelIds[]` | `labelPalette[].id` on the same board |

There are no references between boards, and none from lists or checklists.
A list or card id is rewritten on import together with every reference to it,
so the references only have to agree with each other inside your file.

---

## 6. Ordering, dates and other semantics

### Positions

- Lists are drawn in ascending `position`; cards within a list likewise.
- Array order does **not** matter for lists or cards; `position` does. (It
  does matter for checklist items and labels, which have no `position`.)
- Positions are numbers, not necessarily whole numbers. When the user drags a
  card between two others, the app gives it the midpoint (`1.5`). Write `1`,
  `2`, `3` … in a generated file.
- Card positions only compete within one list. Every list can start at `1`.
- Archived cards keep their positions.

### Due dates

- Exactly `YYYY-MM-DD`, e.g. `"2026-10-09"`, or `null`.
- A due date is a calendar day in the **viewer's local time**, with no time of
  day.
- The app shows it relative to today: `today`, `tomorrow`, `yesterday`, or
  `Oct 9`, colored as overdue (before today), due today, due this week (within
  the next seven days), or later. The toolbar's due filter uses the same bands.
- Any other format is kept, but shows as `undefined NaN` on the card.

### Archived cards

- `archived: true` hides the card from its list. The list footer shows
  *"1 archived — show"*, and the gear menu's **Show archived cards** reveals
  them.
- Archived cards stay in the backup, the `.pumapack` and the CSV export (with
  an archived flag); they are left out of the Markdown and print exports.
- There is no archived flag on boards or lists.

### Done

- Cards have no completion state. Model progress with lists ("To do",
  "Doing", "Done").
- Checklist items have `done`, and the card shows the combined count.

### Notes log

- `notesLog` is plain text. When the user adds a note in the app, it is
  appended as a bracketed local date and time, a newline, then the text, with
  a blank line between entries:
  `"[Sat, Sep 26, 2026, 4:30 PM]\nTwo frames done."`
- The format is a convention, not parsed. Any text is kept.
- The app caps the log at 100,000 characters when it is next edited in the
  app. A longer log imports whole.

### Colors

- `accentColor`, label `color` and `coverColor` should be `#rgb` or
  `#rrggbb` hex. They are not validated on import; a value that is not a color
  simply shows no color.

---

## 7. Editing an existing export

The safest workflow is: export the **full backup** from the topbar, edit it,
and import it with **Replace all**. The result is the same boards with your
edits, apart from ids.

- **Keep the envelope** as it is. `exported`, `deviceId` and `activeBoardId`
  are ignored, so leave them.
- **Ids.** Keep them or change them; the app replaces every board, list,
  card, checklist and item id on import either way. Label ids are kept.
  What matters is that every `listId` and `labelIds` entry still matches
  something in the same board. When you add a card, give it any new id not
  already used on that board.
- **Links break.** A bookmarked link to a board or card (`#/board/<id>`)
  stops working after any re-import, because the ids are new.
- **Timestamps** are not recomputed on import. If you change a card, you may
  set its `updatedAt` to the current time; the app does not care either way.
- **Board `position`** is overwritten on import, so reordering tabs in the file
  has no effect.
- **Unknown keys** you add to a board, list, card, label or checklist item are
  kept, but have no effect.
- **Merge versus Replace all.** Merge never changes an existing board: it adds
  a copy named "… (imported)" beside the original. To update boards in place,
  use Replace all, which also **deletes any board not in the file**. So:
  - if you edited a **full backup**, use Replace all;
  - if you edited a **single-board** export, use Merge, then delete the old
    board in the app (right-click its tab) and rename the new one if you want.

Nothing in the file is a hash or a checksum, and nothing is derived and
stored: WIP counts, checklist totals and due-date status are all computed on
screen.

---

## 8. What the app does not do on import

So that a generated file does not rely on it:

- It does not add default lists, labels or fields to a board.
- It does not fix `listId` or `labelIds` references that point nowhere.
- It does not renumber card or list positions.
- It does not merge an imported board into an existing board of the same
  name. Merge always adds a new board.

---

## 9. The `.pumapack` shape

This is what a board tab's **Export as .pumapack** writes. It carries one
board as a list of tasks, so that PumaTracker can read it. A PumaBoard-made
pack round-trips cleanly: importing it gives back the same board apart from
ids and the board's two timestamps (see below). Prefer the backup file for
editing; use this section when you have been handed a `.pumapack`.

### 9.1 A minimal valid pack

```json
{
  "puma": { "app": "pumaboard", "format": 1, "exportedAt": "2026-09-28T09:00:00.000Z", "title": "Garden jobs" },
  "data": {
    "workspace": { "id": "b-jobs", "name": "Garden jobs", "accent_color": "#5ecc94" },
    "columns": [
      { "id": "c-list", "role": "bucket", "type": "select", "options": [
          { "id": "o-list-ls-todo", "value": "To do", "aliases": ["ls-todo"] },
          { "id": "o-list-ls-done", "value": "Done", "aliases": ["ls-done"] } ] },
      { "id": "c-labels", "role": "tags", "type": "multiselect", "options": [
          { "id": "o-label-lb-tools", "value": "tools", "color": "#d4a464", "aliases": ["lb-tools"] } ] },
      { "id": "c-due", "role": "due", "type": "date" }
    ],
    "tasks": [
      { "id": "cd-rake", "parent_id": null, "archived": false, "name": "Buy a rake", "description": "",
        "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z",
        "cells": { "c-list": "o-list-ls-todo", "c-labels": ["o-label-lb-tools"], "c-due": "2026-10-02" } },
      { "id": "it-shop", "parent_id": "cd-rake", "position": 0, "name": "Compare prices", "cells": { "c-done": true } }
    ]
  }
}
```

This imports as one board, "Garden jobs", with lists "To do" and "Done", one
label, and one card carrying a checklist named "Subtasks" with one ticked item.
The app's own export writes more keys than this (`$schema`, `appVersion`,
`slug`, `schema_version`, `views`, `groups`, `settings` and a done column);
they are harmless, and the ones that matter are described below.

### 9.2 What the importer reads

**Envelope**

| Key | Rule |
|---|---|
| `puma.app` | `"pumaboard"` (or `"pumatracker"`). Another app's name gets *"That's a PumaRisk pumapack — PumaBoard doesn't know how to read it. Open it in PumaRisk instead."* (with that app's name). |
| `puma.format` | Must be the **number** `1`. Anything else: *"Import failed: Unsupported .pumapack format: 1"* (showing the value you wrote). |
| `puma.exportedAt` | Becomes the board's `createdAt` **and** `updatedAt`, and the fallback for any task without timestamps. |
| `data.workspace` | Must be an object. `name` becomes the board name; `accent_color` its accent (default `#2cb39a`). |
| `data.columns` | Must be an array. If `workspace` or `columns` is missing: *"Import failed: Pumapack data missing workspace or columns"*. |
| `data.tasks` | Array of rows. Missing means an empty board. |

**Columns.** Three are recognized:

| Column | Chosen as | Becomes |
|---|---|---|
| List column | the column named by the active view's `group_by` (`workspace.active_view_id` → `workspace.views[].group_by`); else the first with `"role": "bucket"`; else the first `"type": "select"` with `options` | one list per option, in option order. The list's id is the option's `aliases[0]`, its name the option's `value`. |
| Label column | the first with `"role": "tags"`, else the first `"type": "multiselect"` | one label per option: id from `aliases[0]`, name from `value`, color from `color` (default `#7a7f8e`). **If there are no label options, the board gets PumaBoard's seven default labels** (urgent, errand, phone-call, paperwork, physical, financial, nice-to-have). |
| Due column | the first with `"role": "due"`, else the first `"type": "date"` | the card's `dueDate`, `YYYY-MM-DD`. |

Any other column, apart from one with `"role": "done"`, is appended to each
card's description under a `## From PumaTracker` heading, one
`**Column label**: value` line per non-empty value.

**Tasks.** A row with no `parent_id` is a card; a row whose `parent_id` is a
card's `id` is a checklist item on that card.

| Row field | Card | Checklist item |
|---|---|---|
| `id` | card id (replaced on import) | item id (replaced) |
| `name` | `title` (blank becomes `"(untitled)"`) | `text` |
| `description` | `description` | ignored |
| `cells[<list column id>]` | the list, matched by **option id**. No match puts the card in the **first list** (unlike the backup file, where it would be hidden). | ignored |
| `cells[<label column id>]` | `labelIds`, matched by option id; unknown ids dropped | ignored |
| `cells[<due column id>]` | `dueDate` | ignored |
| `cells["c-done"]` | ignored: cards have no done state | `done`. This key is read literally as `c-done`, whatever the done column is called. |
| `archived` | `archived` | ignored |
| `created_at`, `updated_at` | kept | ignored |
| `position` | ignored (see sidecar) | item order |

A checklist item whose parent is itself an item is dropped.

**The sidecar.** `data.workspace.settings.pumaboard_meta` carries what a task
list cannot express. Every part is optional:

| Key | Shape | Effect |
|---|---|---|
| `positions.lists` | `{ "<list id>": number }` | List order. Missing: option order. |
| `positions.cards` | `{ "<card id>": number }` | Card order within a list. Missing: task order. A position of `0` counts as missing. |
| `wip_limits` | `{ "<list id>": number }` | Each list's WIP limit. |
| `checklists` | `{ "<card id>": [ { "id", "name", "item_ids": [ … ] } ] }` | Groups a card's items into named checklists. Items not listed go into an extra checklist called "Other". Without an entry, all of a card's items form one checklist called "Subtasks". |
| `card_extras` | `{ "<card id>": { "notes_log": "", "cover_color": null } }` | The card's `notesLog` and `coverColor`. |

List ids in the sidecar are the options' `aliases[0]` values; card ids are the
task ids as written in the file.

After this translation the pack goes through the same **Merge / Replace all**
dialog as a backup file (§3), with the same id replacement.

---

## 10. Things that go wrong

Every row below was observed by importing a deliberately broken file.

| Mistake | What happens |
|---|---|
| A single board object with no envelope | Rejected: *"Invalid file (not a PumaBoard export, PumaTracker file, or .pumapack)."* |
| `app` missing or misspelled | Rejected with the same toast. |
| Not valid JSON | Rejected with the same toast. |
| `null` inside `boards`, `lists`, `cards` or `checklists` | The import stops part-way with **no message**. With **Replace all**, the user's existing boards have already been deleted, and after a reload they come back empty. Never do this. |
| `null` inside a checklist's `items` | Imported as a blank, unticked item. |
| `lists`, `cards` or `checklists` that is not an array, e.g. `"lists": "To do"` | Same as above: stops silently, and Replace all loses the existing boards. |
| `"boards": []` | Merge: nothing happens (*"Boards merged."*). Replace all: **every board is erased** (*"Workspace replaced."*), and on the next load the sample boards reappear. |
| A card whose `listId` matches no list | Stored, counted in the storage stats and the CSV export, but never shown on the board. |
| A `labelIds` entry that matches no label | Kept in the data, not shown. |
| `dueDate` in another format, e.g. `"10/09/2026"` | Kept; the card shows `⏰ undefined NaN`. |
| A card with no `title` | Shown with a blank title. |
| `version` or `schemaVersion` other than `1` | Ignored; the board is stored as schema 1. |
| Unknown keys on any record | Kept, with no effect. |
| Ids that clash with the user's existing boards | No clash: every id except label ids is replaced on import. |
| Expecting `theme` / `accent` to apply on Merge | They are applied only on Replace all. |
| `.pumapack` with `"format": "1"` (a string) | *"Import failed: Unsupported .pumapack format: 1"* |
| `.pumapack` without `data.columns` | *"Import failed: Pumapack data missing workspace or columns"* |

---

## 11. Checklist before handing a file over

A file that passes all of these imports with no error and nothing hidden.

**Structure**
- [ ] The envelope matches §2: `"app": "pumaboard"` and a non-empty `boards`
      array.
- [ ] Every board has every field from §4.1; `labelPalette`, `lists` and
      `cards` are arrays.
- [ ] Every card has every field from §4.4; `labelIds` and `checklists` are
      arrays; every checklist has an `items` array.
- [ ] No `null` anywhere inside an array. `null` appears only as `dueDate`,
      `coverColor`, `wipLimit` or the envelope's `accent`.
- [ ] Ids are unique within each board.

**References**
- [ ] Every card's `listId` is a list `id` on the same board.
- [ ] Every `labelIds` entry is a `labelPalette` `id` on the same board.

**Values**
- [ ] `position` is a number on every list and card, and the numbers give the
      order you intend.
- [ ] `dueDate` is `YYYY-MM-DD` or `null`.
- [ ] Colors are `#rgb` or `#rrggbb`.
- [ ] Timestamps are full ISO datetimes.
- [ ] Nothing relies on a done flag on a card; progress is shown by list.

**Import**
- [ ] The person importing knows that Replace all removes every board not in
      the file, and that Merge adds copies named "… (imported)".

---

## 12. A complete example

One board with three lists (one with a WIP limit), three labels, a card with a
Markdown description and a due date, a card with a checklist, a notes log and
a cover color, a finished card, and an archived one. It imports with no
warnings.

```json
{
  "app": "pumaboard",
  "version": 1,
  "exported": "2026-09-28T09:00:00.000Z",
  "deviceId": "",
  "theme": "dark",
  "accent": null,
  "activeBoardId": "b-garden",
  "boards": [
    {
      "id": "b-garden",
      "name": "Community garden build",
      "accentColor": "#5ecc94",
      "position": 1,
      "createdAt": "2026-09-14T10:00:00.000Z",
      "updatedAt": "2026-09-27T16:30:00.000Z",
      "schemaVersion": 1,
      "labelPalette": [
        { "id": "lb-materials", "name": "materials", "color": "#d4a464" },
        { "id": "lb-volunteers", "name": "volunteers", "color": "#5b8af0" },
        { "id": "lb-urgent", "name": "urgent", "color": "#e05050" }
      ],
      "lists": [
        { "id": "ls-todo", "name": "To do", "position": 1, "wipLimit": null },
        { "id": "ls-doing", "name": "Doing", "position": 2, "wipLimit": 3 },
        { "id": "ls-done", "name": "Done", "position": 3, "wipLimit": null }
      ],
      "cards": [
        {
          "id": "cd-soil",
          "listId": "ls-todo",
          "position": 1,
          "title": "Order raised-bed soil",
          "description": "Four beds at 2.4 x 1.2 m, 30 cm deep: about **3.5 cubic meters**.\n\n- Ask the supplier about a bulk discount\n- Delivery must fit through the east gate",
          "labelIds": ["lb-materials", "lb-urgent"],
          "dueDate": "2026-10-09",
          "checklists": [],
          "notesLog": "",
          "archived": false,
          "coverColor": null,
          "createdAt": "2026-09-14T10:05:00.000Z",
          "updatedAt": "2026-09-26T08:12:00.000Z"
        },
        {
          "id": "cd-signup",
          "listId": "ls-todo",
          "position": 2,
          "title": "Volunteer sign-up sheet",
          "description": "",
          "labelIds": ["lb-volunteers"],
          "dueDate": null,
          "checklists": [],
          "notesLog": "",
          "archived": false,
          "coverColor": null,
          "createdAt": "2026-09-15T12:00:00.000Z",
          "updatedAt": "2026-09-15T12:00:00.000Z"
        },
        {
          "id": "cd-beds",
          "listId": "ls-doing",
          "position": 1,
          "title": "Build the four cedar beds",
          "description": "Plans are pinned in the shed.",
          "labelIds": ["lb-materials"],
          "dueDate": "2026-10-17",
          "checklists": [
            {
              "id": "ck-build",
              "name": "Build steps",
              "items": [
                { "id": "it-cut", "text": "Cut boards to length", "done": true },
                { "id": "it-frame", "text": "Screw the frames together", "done": false },
                { "id": "it-line", "text": "Line the bases with cardboard", "done": false }
              ]
            }
          ],
          "notesLog": "[Sat, Sep 26, 2026, 4:30 PM]\nTwo frames done. Need more 75 mm screws.",
          "archived": false,
          "coverColor": "#5ecc94",
          "createdAt": "2026-09-16T09:00:00.000Z",
          "updatedAt": "2026-09-27T16:30:00.000Z"
        },
        {
          "id": "cd-plot",
          "listId": "ls-done",
          "position": 1,
          "title": "Confirm the plot with the parks office",
          "description": "",
          "labelIds": [],
          "dueDate": null,
          "checklists": [],
          "notesLog": "",
          "archived": false,
          "coverColor": null,
          "createdAt": "2026-09-14T10:02:00.000Z",
          "updatedAt": "2026-09-18T11:45:00.000Z"
        },
        {
          "id": "cd-flyer",
          "listId": "ls-done",
          "position": 2,
          "title": "Print the first flyer draft",
          "description": "Replaced by the sign-up sheet.",
          "labelIds": ["lb-volunteers"],
          "dueDate": null,
          "checklists": [],
          "notesLog": "",
          "archived": true,
          "coverColor": null,
          "createdAt": "2026-09-14T10:10:00.000Z",
          "updatedAt": "2026-09-20T09:00:00.000Z"
        }
      ]
    }
  ]
}
```

What the app shows after importing this, as a check on your own reasoning.
These were read from the app after importing this exact file:

- One tab, "Community garden build" with Replace all, or "Community garden
  build (imported)" beside the existing boards with Merge.
- **To do** holds two cards: "Order raised-bed soil" (two label chips, a due
  date, a description mark) above "Volunteer sign-up sheet".
- **Doing** shows the count `1/3`: one card against a WIP limit of three.
  "Build the four cedar beds" has a green cover stripe and shows `☑ 1/3`.
- **Done** shows one card and the footer *"1 archived — show"*; the flyer
  card is hidden until archived cards are shown.
- The stored board matches this file field for field, except that the board,
  lists, cards, checklist and items all have new ids (the label ids are
  unchanged), and with Merge the board's name gains " (imported)" and its
  position follows the existing boards.
