OpenIslands

The 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.

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

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

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:

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>:

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 for the full shape.

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 rejects the manifest with a named error like "span 1 is below the minimum 4 for timeseries.line."
  • A maximum span above which it only stretches into dead space — and the over-max error is the mirror of the under-min one: "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

The manifest's job is to make a broken dashboard 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 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), and the net stays taut.

Where to go next

  • Data Contracts: datasets, SQL transforms, and the binding check in detail.
  • Islands: every built-in island and its required fields, with live previews.
  • Manifest Reference: the exhaustive, schema-generated field list.

On this page