---
title: Framewerk 1 specification
description: The rules of the Framewerk 1 document format, stated as what a document, a reader and a validator must do, each matched to what validate() checks.
status: draft
url: https://getairframe.com/framewerk/specification
---

> Part of the Framewerk docs. Index of every page: /framewerk/llms.txt

# Framewerk 1 specification

This page states the rules of Framewerk 1: what a document must contain, what a reader must do with it, and what a validator must report.

The other pages teach the format by example. This one is the reference a second implementation is written against. Where the two disagree, this page is wrong and should be reported.

## Status of this page

This is a working draft. It is not linked from the rest of the docs yet.

Two kinds of rule are on it, and each rule says which kind it is:

- Rules that `validate()` in `@framewerk/spec` enforces today.
- Rules that are part of the format and that `validate()` does not check yet. They are listed so that nobody mistakes a missing check for permission.

The [rule index](#rule-index) at the end lists every rule with what `validate()` does when a document breaks it. Each row of that index is run against the real validator in the package's tests.

## Conformance

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as described in RFC 2119 and RFC 8174: they carry those meanings only when written in capitals.

Three things can conform:

| | Conforms when |
| --- | --- |
| A **document** | it breaks no MUST rule addressed to documents |
| A **reader** | it is a program that takes a document in (a renderer, a code generator, an agent tool) and follows every MUST rule addressed to readers |
| A **validator** | it is a reader that reports problems, and it reports an error for every broken MUST rule this page marks as checked |

A rule is addressed to documents unless it names a reader or a validator.

A validator MAY report more warnings than this page lists. It MUST NOT report an error for a document that breaks no MUST rule.

## The document

A Framewerk document is one JSON value, as defined by RFC 8259, encoded as UTF-8.

**DOC-1.** The document MUST be a JSON object.

**DOC-2.** The document MUST NOT be a JSON array. `validate()` does not check this yet: it reports a string, a number or `null` at the root, and lets an array through.

The members of the object are the parts of the document. Every part is optional, and an empty object is a conforming document.

| Member | JSON type | Holds | Stability |
| --- | --- | --- | --- |
| `project` | object | name, theme, personas, defaults | stable |
| `screens` | array | the screens | stable |
| `components` | object | reusable subtrees, by name | stable |
| `styles` | array | token sets and rules | stable |
| `brands` | array | brand identity a style is derived from | stable |
| `flows` | array | screen-to-screen edges | stable |
| `flowPackages` | array | named journeys through several screens | stable |
| `content` | object | localized labels and fixture data | stable |
| `pages` | array | grouping for large projects | stable |
| `aliases` | object | redirects from a renamed id to the current one | stable |
| `requirements` | array | what the product must do, linked to screens | draft |

**DOC-3.** A member in the table, when present, MUST have the JSON type the table gives.

**DOC-4.** `project`, when present, MUST be an object and not an array. `validate()` does not check this yet.

**DOC-5.** A reader MUST pass over a top-level member it does not know. It MUST NOT treat one as an error.

A tool that reads a document and writes it back SHOULD keep the members it does not know.

## Versioning

A document says which version of the format it was written against in one top-level member, `formatVersion`. The value is a string of two numbers, `major.minor`. This page describes version `1.0`.

```json
{
  "formatVersion": "1.0",
  "screens": [
    {
      "id": "menu",
      "layout": {
        "_id": "menu-root",
        "type": "column",
        "gap": 12,
        "padding": 16,
        "children": [
          { "_id": "title", "type": "text", "variant": "heading", "content": "Coffee Cart" }
        ]
      }
    }
  ]
}
```

**VER-1.** `formatVersion`, when present, MUST be a string of the form `major.minor`, where each part is a non-negative whole number with no leading zero.

**VER-2.** A reader MUST refuse a document whose major version it does not know. A reader MUST accept any minor version of a major version it knows, and pass over what that minor added.

**VER-3.** A document SHOULD carry `formatVersion`. A reader MUST read a document that has none as version `1.0`, and a validator reports a warning for it, never an error. Every document written before the member existed has none.

**VER-4.** A document that states a `formatVersion` and uses a draft part MUST name that part in a top-level `drafts` array of strings. The only draft part of version 1 is `requirements`, and a document uses it when its `requirements` array has at least one entry. A reader MUST pass over a name in `drafts` that it does not know.

```json
{
  "formatVersion": "1.0",
  "drafts": ["requirements"],
  "screens": [
    {
      "id": "menu",
      "layout": {
        "_id": "menu-root",
        "type": "column",
        "gap": 12,
        "padding": 16,
        "children": [
          { "_id": "title", "type": "text", "variant": "heading", "content": "Coffee Cart" },
          { "_id": "order", "type": "button", "variant": "primary", "content": "Order", "action": "navigate(order)" }
        ]
      }
    },
    {
      "id": "order",
      "layout": {
        "_id": "order-root",
        "type": "column",
        "padding": 16,
        "children": [
          { "_id": "confirmed", "type": "text", "content": "One espresso, on its way." }
        ]
      }
    }
  ],
  "requirements": [
    {
      "id": "order-a-coffee",
      "title": "A customer orders a coffee from the menu",
      "type": "user-story",
      "scope": "capability",
      "source": "stakeholder",
      "priority": "P0",
      "status": "implemented",
      "linkedScreens": ["menu", "order"],
      "criteria": [
        { "id": "order-button", "text": "The menu has an Order button", "verified": true }
      ]
    }
  ]
}
```

A document has one version. Draft parts are named beside it and are not versioned on their own.

A document with no `formatVersion` predates both members, so it is not held to VER-4: it gets the one warning of VER-3.

A writer can leave both members to the package: `withFormatVersion(document)` from `@framewerk/spec` returns the document stating the current version and naming every draft part it uses.

`project.version` is the version of the project the document describes. It has nothing to do with the format version.

## Identifiers and references

Parts of a document point at each other by id. An id is a JSON string. Comparison is exact: case matters and no whitespace is trimmed.

**ID-1.** Every screen MUST have an `id` that is a non-empty string, and no two screens may share one.

**ID-2.** Within one screen's layout tree, no two nodes may share an `_id`. Two screens may use the same `_id`.

**ID-3.** Every node SHOULD carry an `_id`. A node without one cannot be addressed by a patch, an anchor or a test, and a tool that fills ids in will not give the same node the same id twice. `validate()` does not report a missing `_id`.

`aliases` maps an old id to the current one, so a rename does not break every reference to the old name.

**ID-4.** Every alias value MUST be a non-empty string. An alias MUST NOT name the id of a live screen or component as its key. A chain of aliases MUST resolve within three steps and MUST NOT loop.

A reader resolves a screen or component id through `aliases` before it decides the id names nothing.

## Screens and the layout tree

A screen is an object with an `id` and a `layout`. The layout is a tree of nodes. Each node has a `type`, its own props, and for a container type a `children` array of nodes.

**SCR-1.** Every entry of `screens` MUST be an object.

**SCR-2.** Every node MUST have a `type`, and the type MUST be one in the node catalog. The catalog is the [node reference](/framewerk/reference/nodes.md).

**SCR-3.** `children`, when present, MUST be an array of nodes.

**SCR-4.** A prop that takes a closed list of values MUST take one of them. The node reference gives the list for each such prop.

**SCR-5.** A reader MUST pass over a node prop it does not know. It MUST NOT treat one as an error, and it MUST still read the rest of the node.

A reader that meets a node type it does not know is reading a document that breaks SCR-2. A renderer SHOULD draw a visible placeholder in its place and continue with the rest of the tree.

The props each node takes, how the tree is laid out, and devices are described in [The layout tree](/framewerk/core/layout-tree.md), [Screens and devices](/framewerk/core/screens-and-devices.md) and the node reference. Those pages are the definition of each node until its rules are written here.

## Components

A component is a named subtree in `components`. A node of type `component` places one, and names it in `ref`.

**CMP-1.** A `component` node MUST have a `ref` that is a non-empty string.

**CMP-2.** `ref` MUST name a key of `components`, directly or through `aliases`. `validate()` does not check this yet.

Props, slots and states of components are described in [Components](/framewerk/core/components.md).

## Actions

An action is a string on a node that says what happens when it is pressed. Version 1 has six: `navigate(screenId)`, `back()`, `setState(key, value)`, `toggleState(key)`, `scrollTo(anchor)` and `openUrl(address)`.

**ACT-1.** An action SHOULD be one of the six. A validator reports any other string as a warning. A renderer MUST do nothing when a node with such an action is pressed, and MUST NOT evaluate the string in any other way.

**ACT-2.** The target of `navigate()` SHOULD be the id of a screen in the document. A validator reports a target that names nothing as a warning.

Both are warnings because the document still draws correctly. They are the most expensive mistakes to miss, so a tool that gates on validation SHOULD treat warnings as failures.

The grammar of actions and of the expressions in `showWhen` is described in [Actions](/framewerk/core/actions.md) and [State and expressions](/framewerk/core/state-and-expressions.md).

## Flows

A flow is one edge from a screen to a screen. A flow package is a named journey through several screens.

**FLOW-1.** Every flow MUST be an object with a `from` and a `to`, and each MUST be the id of a screen in the document.

**FLOW-2.** Every flow package MUST have an `id` and a `name` that are non-empty strings, and no two flow packages may share an `id`.

**FLOW-3.** Every entry of a flow package's `screens` SHOULD be the id of a screen in the document. A validator reports one that names nothing as a warning.

## Project and styles

**PROJ-1.** Every entry of `project.personas` MUST have an `id` and a `name` that are non-empty strings.

**PROJ-2.** No two personas may share an `id`. `validate()` does not check this yet.

**PROJ-3.** `project.theme`, when present, MUST be one of `wireframe`, `blueprint` or `high-fidelity`.

**STY-1.** Every style MUST have an `id` and a `name` that are non-empty strings, and no two styles may share an `id`.

Tokens, rules and modes are described in [Styles and tokens](/framewerk/core/styles-and-tokens.md).

## Requirements

This part is draft. See [Stability](#stability) for what that means.

A requirement says what the product must do, in the same document as the screens that answer it. [Requirements](/framewerk/core/requirements.md) teaches the part by example.

**REQ-1.** Every entry of `requirements` MUST be an object.

**REQ-2.** A requirement MUST have an `id` and a `title` that are non-empty strings, and a `type`, a `scope`, a `source` and a `status`. No two requirements may share an `id`.

**REQ-3.** A requirement MUST have a `priority`. `validate()` does not report a requirement without one yet; it reports a priority outside the list.

**REQ-4.** These members take one value from a closed list:

| Member | Values |
| --- | --- |
| `type` | `user-story`, `requirement`, `acceptance-criteria`, `bug`, `feedback`, `qa-note` |
| `scope` | `capability`, `task` |
| `source` | `stakeholder`, `research`, `agent`, `audit` |
| `priority` | `P0`, `P1`, `P2` |
| `status` | `proposed`, `accepted`, `in-progress`, `implemented`, `blocked`, `verified` |
| `severity` | `critical`, `major`, `minor`, `cosmetic` |
| `resolution` | `fixed`, `wont-fix`, `duplicate`, `cannot-reproduce`, `superseded` |

`scope` is the level, and it is always written. A `capability` says what the product does and never closes. A `task` is a piece of the work that makes a capability true. A document MUST NOT leave `scope` out and expect a reader to assume one.

**REQ-5.** A link out of a requirement MUST name something in the same document:

| Member | Each entry names |
| --- | --- |
| `linkedScreens` | the `id` of a screen |
| `linkedFlows` | the `id` of a flow package |
| `personas` | the `id` of an entry of `project.personas` |
| `parentCapabilityId` | the `id` of another requirement |

**REQ-6.** `parentCapabilityId` SHOULD name a requirement whose `scope` is `capability`, and SHOULD be carried only by a requirement whose `scope` is `task`. A validator reports either the other way round as a warning.

**REQ-7.** `supersededBy` MUST be carried only with `resolution` set to `superseded`, and MUST NOT name the requirement that carries it.

**REQ-8.** `supersededBy` SHOULD name a requirement in the document. A validator reports one that names nothing as a warning, because a replacement that was later removed should not make the whole document invalid.

**REQ-9.** `criteria`, when present, MUST be an array of objects. Each criterion MUST have an `id` and a `text` that are non-empty strings, and no two criteria of one requirement may share an `id`. `verified`, when present, MUST be `true` or `false`.

**REQ-10.** A requirement whose `status` is `blocked` SHOULD carry a `blockerReason`.

**REQ-11.** `createdAt`, `updatedAt`, `startedAt` and `closedAt`, when present, MUST be strings.

**REQ-12.** Each of those four SHOULD be an ISO 8601 date with a time, such as `2026-10-13T09:30:00Z`. A validator reports a date with no time as a warning.

**REQ-13.** A reader MUST pass over a member of a requirement that it does not know. A tool MAY add its own members to a requirement, and SHOULD choose names unlikely to be taken by a later version of the format.

`status` says how far the work got. `resolution` says why a requirement stopped being live. They are separate members: a requirement that shipped and was later replaced keeps its `status` and gains `resolution: "superseded"`.

The remaining optional members are `feature`, `description`, `tags`, `blockerReason`, `refCode`, `reporter` and `assignee`. `reporter` and `assignee` are names to show. A document MUST NOT put an account id from a tool's own user table in either.

## Validation results

A validator takes a document and returns three things: whether it is valid, a list of errors and a list of warnings.

- A document is **valid** exactly when the list of errors is empty.
- An **error** is a broken MUST rule: something a reader cannot make sense of.
- A **warning** is a broken SHOULD rule, or anything else worth a person's attention. A document with warnings is valid.

Each problem has a `path` and a `message`.

The `path` says where the problem is. It starts at the root with `/`, and each step is a member name or an array index counted from zero: `/requirements/0/linkedScreens/1` is the second linked screen of the first requirement. A problem with the document as a whole has the path `/`.

The `message` is an English sentence for a person. Its wording is not part of the format and changes without notice. A tool MUST NOT decide what a problem is by matching the message.

A stable code for each kind of problem is planned, with a published list. Until it ships, a tool that needs to tell problems apart can only use the path.

## Stability

Each part of the format is either stable or draft. The table in [The document](#the-document) says which.

### Stable

A stable part changes in a breaking way only with a new major version. These are breaking:

- removing a member, a node type, a prop or a value from a closed list
- narrowing what a member accepts
- making an optional member required
- turning a warning into an error
- changing what a reader must do with a document that was already valid

These are not breaking, and may arrive in a minor version:

- a new optional member, node type or prop
- a new warning

Whether a new value in a closed list is breaking has not been decided. Until it is, no value is added to a closed list of a stable part in a minor version.

### Draft

A draft part may change in any minor version, in breaking ways as well. Every such change is listed in the changelog of `@framewerk/spec`, marked as a change to the format.

A document that uses a draft part says so in `drafts` (see VER-4), so a reader can tell before it reads the part that the shape is not yet promised.

### Leaving draft

A part leaves draft on a named condition, not on a judgement that it feels finished. Either condition is enough:

- an implementation other than `@framewerk/spec` reads the part, or
- a set period passes with no breaking change to the part.

The length of that period has not been decided. It will be written here before any part is proposed for stable.

### Proposing a change

A change to the format is proposed as an issue on the public repository. The maintainers decide. The decision is recorded on the issue, and a change that is accepted is listed in the changelog.

## What the format does not carry

A document is a record. Programs that do work on one are outside the format: the tool that stores it, the agents that write it, anything that compares it with code or walks a running app against it. A document that conforms does not depend on any of them.

Planning records other than requirements are not part of version 1: feature briefs, test plans, roadmaps, releases and boards. A tool MAY carry them in its own top-level members, which a reader passes over (DOC-5).

## Rule index

What `validate()` in `@framewerk/spec` does today with a document that breaks each rule.

- **error**: the document is invalid, and an error names the place.
- **warning**: the document is valid, and a warning names the place.
- **passed over**: the rule is on the reader. Nothing is reported, which is the rule being followed.
- **not checked**: the rule is part of the format and `validate()` does not enforce it yet.

| Rule | Level | `validate()` | What it says |
| --- | --- | --- | --- |
| **DOC-1** | MUST | error | The document is a JSON object |
| **DOC-2** | MUST | not checked | The document is not an array |
| **DOC-3** | MUST | error | Each known member has its JSON type |
| **DOC-4** | MUST | not checked | `project` is an object, not an array |
| **DOC-5** | MUST | passed over | A reader passes over an unknown top-level member |
| **VER-1** | MUST | error | `formatVersion` is a `major.minor` string |
| **VER-2** | MUST | error | A reader refuses an unknown major version |
| **VER-3** | SHOULD | warning | A document carries `formatVersion`; none is read as `1.0` |
| **VER-4** | MUST | error | A versioned document names the draft parts it uses in `drafts` |
| **ID-1** | MUST | error | Screen ids are non-empty and unique |
| **ID-2** | MUST | error | A node `_id` is unique within its screen |
| **ID-3** | SHOULD | not checked | Every node carries an `_id` |
| **ID-4** | MUST | error | Aliases resolve, do not loop, and do not shadow a live id |
| **SCR-1** | MUST | error | A screen is an object |
| **SCR-2** | MUST | error | A node has a type from the catalog |
| **SCR-3** | MUST | error | `children` is an array |
| **SCR-4** | MUST | error | A closed-list prop takes a listed value |
| **SCR-5** | MUST | passed over | A reader passes over an unknown node prop |
| **CMP-1** | MUST | error | A component node has a non-empty `ref` |
| **CMP-2** | MUST | not checked | `ref` names a component in the document |
| **ACT-1** | SHOULD | warning | An action is one of the six |
| **ACT-2** | SHOULD | warning | `navigate()` targets a screen in the document |
| **FLOW-1** | MUST | error | A flow has a `from` and a `to` that name screens |
| **FLOW-2** | MUST | error | A flow package has an `id` and a `name`; ids are unique |
| **FLOW-3** | SHOULD | warning | A flow package's screens name screens |
| **PROJ-1** | MUST | error | A persona has an `id` and a `name` |
| **PROJ-2** | MUST | not checked | Persona ids are unique |
| **PROJ-3** | MUST | error | `project.theme` is a listed value |
| **STY-1** | MUST | error | A style has an `id` and a `name`; ids are unique |
| **REQ-1** | MUST | error | A requirement is an object |
| **REQ-2** | MUST | error | A requirement has `id`, `title`, `type`, `scope`, `source`, `status`; ids are unique |
| **REQ-3** | MUST | not checked | A requirement has a `priority` |
| **REQ-4** | MUST | error | Closed-list members take a listed value |
| **REQ-5** | MUST | error | Links name something in the document |
| **REQ-6** | SHOULD | warning | A parent is a capability, carried by a task |
| **REQ-7** | MUST | error | `supersededBy` goes with `resolution: superseded`, never to itself |
| **REQ-8** | SHOULD | warning | `supersededBy` names a requirement in the document |
| **REQ-9** | MUST | error | Criteria have unique ids, a text, and a boolean `verified` |
| **REQ-10** | SHOULD | warning | A blocked requirement says why |
| **REQ-11** | MUST | error | The four dates are strings |
| **REQ-12** | SHOULD | warning | The four dates are ISO 8601 with a time |
| **REQ-13** | MUST | passed over | A reader passes over an unknown requirement member |
