# The Manifest (/concepts/manifest)





An OpenIslands app is a typed **manifest**: a JSON declaration of reusable visual
**islands** bound to **typed data contracts** built from files you own. You edit the
manifest. You never write rendering code; the runtime owns that. The payoff is durability:
a manifest is small, declarative, and validated, so an agent can maintain it for months
without the dashboard rotting.

## The skeleton [#the-skeleton]

Every manifest has the same spine: a version, a title, the datasets it reads, and the pages
that render them.

```jsonc title="manifest.json"
{
  "version": 1,
  "title": "Finance Overview",
  "icon": "wallet",                                          // optional, the app's tile icon
  "datasets": {
    "net_worth": { "source": "data/net_worth_monthly.csv" } // a file you own (see Data Contracts)
  },
  "pages": [
    {
      "id": "overview",                                      // one sidebar entry per page
      "icon": "house",
      "islands": [
        {
          "type": "metric.kpi",
          "title": "Net worth",
          "dataset": "net_worth",                            // names a dataset above
          "value": "net_worth_eur",                          // a field that must exist in it
          "compareTo": "prev",
          "format": "eur",
          "span": 3
        }
      ]
    }
  ]
}
```

That single KPI island, fed its data, renders exactly like this. The docs run the real
renderer, not a screenshot:

<LiveIsland type="metric.kpi" config="{ title: &#x22;Net worth&#x22;, value: &#x22;net_worth_eur&#x22;, compareTo: &#x22;prev&#x22;, format: &#x22;eur&#x22; }" data="netWorthByMonth" height="160" />

## Pages and groups [#pages-and-groups]

Each page is one entry in the left sidebar (a single-page app stays chrome-free). A page
holds **either** a flat list of `islands` **or** tabbed `groups`, never both. Groups render
as tabs under the page header, deep-linked via `?group=<id>`:

```jsonc title="manifest.json"
{
  "id": "holdings",
  "groups": [
    { "id": "positions", "title": "Positions", "islands": [ /* ... */ ] },
    { "id": "activity",  "title": "Activity",   "islands": [ /* ... */ ] }
  ]
}
```

A page may also declare shared `filters` rendered in the page header — a `daterange` over a
date column (optionally opening on a `default` period like `last-90-days`, resolved live against
today), or a `select` that narrows a categorical column (single or `multiple`, its choices
drawn from the column's live distinct values) — each re-querying every island bound to the
filtered column at once. A filter only touches the datasets named in its `bind` map; an island
whose dataset isn't bound (e.g. a SQL transform you add to a filtered page later) keeps showing
all-time until you add it to `bind`. See the [Manifest Reference](/reference/manifest.md) for the
full shape.

## Spans and the grid [#spans-and-the-grid]

Every page (and every group) is a 12-column grid. An island's `span` is how many columns it
takes, from 1 to 12. Each island type carries three bounds:

* A **minimum span** below which it stops being legible — a `table.grid` needs 5, most charts
  need 4, a `metric.kpi` needs 2. Below it, `validate&#x60; rejects the manifest with a named error
  like &#x2A;"span 1 is below the minimum 4 for timeseries.line."*
* A **maximum span*&#x2A; above which it only stretches into dead space — and the over-max error is
  the mirror of the under-min one: &#x2A;"span 12 exceeds the maximum 6 for funnel.steps — it only
  stretches into empty space past that width."* Compact, single-value islands (KPIs, funnels,
  gauges, pies, radars) are capped well below full width; data-dense islands (tables, charts,
  feeds, calendars) run the full 12.
* A **recommended span** in between — what the island renders at when you omit `span`. A
  `funnel.steps`, for instance, is min 3, recommended 4, max 6.

`validate` (and the MCP edit path) also runs an advisory **layout linter** that emits
non-blocking warnings for composition smells — a lone `metric.kpi` (group them or use
`metric.scorecard`), or a compact island stretched past its recommended width. The dashboard
still renders; the warning just nudges you toward a tidier layout.

The schema is the source of truth for all three numbers: see `ISLAND_MIN_SPAN`,
`ISLAND_DEFAULT_SPAN`, and `ISLAND_MAX_SPAN` in `packages/schema/src/index.ts`, or read an
island's range straight from `oi.app().getIslandSchema(type)` over the MCP. The runtime also floors
spans responsively, so a tile never renders narrower than its usable width on a small screen.

## Fail loudly [#fail-loudly]

The manifest's job is to make a broken dashboard &#x2A;*impossible to ship silently.** Every
island binds to named fields, and `validate` (and the agent's `oi.app().replaceManifest`) checks
each one against the live, DuckDB-inferred columns of the data:

* Bind to a field that exists → the island renders.
* Bind to a field that **doesn't** → the build fails and &#x2A;*names the page, the island, and
  the missing field.** You never get a silently-wrong chart pointed at a column that isn't
  there.

That check is the safety net the whole design rests on. Keep the manifest declarative, with
no data transforms inside island configs (shaping lives in the
[data layer](/concepts/data-contracts.md)), and the net stays taut.

## Where to go next [#where-to-go-next]

* [Data Contracts](/concepts/data-contracts.md): datasets, SQL transforms, and the binding check
  in detail.
* [Islands](/islands/overview.md): every built-in island and its required fields, with live
  previews.
* [Manifest Reference](/reference/manifest.md): the exhaustive, schema-generated field list.


---

*This is one page of the OpenIslands docs. Every page in one file: [/llms-full.txt](/llms-full.txt). Page index: [/llms.txt](/llms.txt). Links above point to `.md` siblings — append `.md` to any page URL for its raw markdown.*
