Evolution policy

Extensions and versioning

Forma v1 defines a stable baseline for interoperability. This section defines how the spec evolves and how implementations may add extensions without breaking compatibility.

Versioning

  • Forma uses semantic versioning.
  • Patch versions fix editorial or validation issues without changing meaning.
  • Minor versions may add optional fields or enums in backward-compatible ways.
  • Major versions may introduce breaking changes.

Extension model

  • Extensions must not redefine the meaning of base fields.
  • Extensions should live under an extensions object or vendor-prefixed keys.
  • Consumers must ignore unknown extensions safely.
A Forma v1 consumer must continue to accept all valid Forma documents for the v1 minor line(s) it implements (e.g. 1.0.x). New minor versions may add optional fields and may extend enums; a consumer or validator that has not implemented the newer minor version may treat documents requiring those additions as invalid.

AI-Native extension (v1.1)

This optional extension adds a semantic + behavior layer intended for AI consumption. It MUST NOT change the meaning of core fields and MUST be safely ignorable by consumers that do not implement it.

Location

The extension lives under extensions.ai_native on the document root.

Fields

  • intent: map from scope → purpose string (why a node/component exists)
  • flows: named multi-step interaction sequences
  • stateMachine: explicit allowed state transitions (reduce behavior hallucination)
  • constraints: conditional rules for safe, accessible, and legal usage
  • contextRoles: map from scope → allowed/recommended roles and contexts
  • dataBindings: map UI nodes/components to dynamic data sources
  • scopeAliases: optional map of stable alias scopes to either a concrete scope id or a selector object (enables semantic addressing beyond ids)
  • dataClassification: optional map of scope → data sensitivity annotations (supports trace redaction and user-confirmation gating)
  • actions: named action definitions (navigate, submit, open/close modal, setState, emit)
  • capabilities: optional registry of invoke targets with input/output contracts (reduces ambiguity across tools)
  • events: optional registry of emitted event names with payload contracts
  • eventHandlers: declarative event → action routing (what happens on click/submit/change, etc.)
  • focusOrder: optional explicit focus navigation order as an array of scopes (agent + accessibility interoperability)
  • readingOrder: optional explicit reading order as an array of scopes (screen readers and agent summarization)
  • a11y: optional accessibility annotations (label/hint/role/required/live) keyed by scope
  • validation: optional machine-readable validation constraints keyed by scope (required, format, pattern, min/max)
  • confidence: marks inferred/uncertain areas to prompt clarification

Scope conventions (for interoperability):

  • When referencing a concrete Forma node, scopes MUST use that node’s id.
  • If extensions.ai_native.scopeAliases is present, producers MAY reference alias ids anywhere a scope string is accepted.
  • Alias values MAY be a concrete scope id or a selector object. In v1.1, the only selector kind is {"kind":"byRole", ...}, which resolves by selecting the Nth node (pre-order traversal) whose resolved role matches.
  • When referencing a step that is not a Forma node (a “virtual step”), scopes SHOULD use the prefix @ (example: @showResult).
  • Consumers MUST treat unknown scopes as ignorable strings.

Interaction conventions (for interoperability):

  • actions is a map of action id → action object.
  • eventHandlers[].do is an ordered list of action ids to execute.
  • Actions MAY include a typed payload object. For example, navigate may specify payload.mode (push/replace) and payload.query.
  • For deterministic replay, invoke MAY declare payload.argsFromState and MAY bind results via payload.bindResultTo / payload.bindErrorTo. submit MAY specify payload.bodyFromState.
  • Actions MAY define then, catch, and finally as ordered lists of action ids for deterministic success/error/cleanup continuations.
  • If capabilities is present, invoke.target SHOULD reference a key in that registry.
  • If events is present, emit.payload.event SHOULD reference a key in that registry.
  • Event handlers MAY specify preventDefault, stopPropagation, debounceMs, and priority to reduce ambiguity in downstream execution.
  • Producers SHOULD keep action ids stable across exports.

Deterministic execution (AI-native deterministic profile):

  • Consumers implementing deterministic execution MUST interpret eventHandlers as the sole authoritative behavior graph.
  • Execution model: the deterministic profile is single-threaded and run-to-completion. Within a single trigger, consumers MUST produce the same handler selection, ordering, and action trace given the same document and the same starting state.
  • For an input event, consumers MUST select matching handlers where on matches the event type and scope matches the target scope.
  • If a handler includes when, the handler matches only when the predicate evaluates truthy. If a consumer does not support the predicate shape, it MUST treat the predicate as false and SHOULD emit an issue (unsupported deterministic predicate).
  • Predicate evaluation is deterministic and MUST use the following evaluation context object: {state, event:{on, scope, name, detail}}. Value expressions inside predicates MAY reference values from this context via {"ref":"/path"} where ref is a JSON Pointer.
  • Supported predicate operators in the v1.1 deterministic profile: all, any, not, exists, eq, ne, gt, gte, lt, lte, matches, and ref. Unknown operators MUST be treated as false.
  • Binary operators (ne, gt, gte, lt, lte, matches) require exactly 2 arguments; other arities MUST evaluate to false.
  • For emitted events, consumers MUST support eventHandlers with on: "event" and MUST match eventHandlers[].event to the emitted emit.payload.event.
  • If multiple handlers match the same event and scope, consumers MUST order them by priority (higher first), then by id (lexicographic). To avoid ambiguity, producers SHOULD assign stable id values.
  • If multiple handlers match the same trigger and one or more handlers are missing id, consumers MUST treat ordering as ambiguous and SHOULD emit an ai_handler_order_ambiguous issue.
  • A handler’s do list MUST execute in-order. If an action defines then, catch, or finally, those action-id lists MUST execute immediately after that action completes, before continuing with the next action in the containing do list.
  • Continuation ordering is deterministic: if an action completes successfully, execute then (if present). If it fails, execute catch (if present). In both cases, execute finally (if present) after then/catch completes.
  • For invoke, if payload.argsFromState is present, the consumer MUST read the referenced state value and use it as the base arguments object. If both payload.argsFromState and payload.args are present, payload.args MUST override overlapping keys.
  • For invoke, if payload.bindResultTo or payload.bindErrorTo is present, the consumer MUST write the returned result/error value into the referenced state location.
  • For submit, if payload.bodyFromState is present, the consumer MUST serialize the referenced state value as the request body.
  • For setState, consumers MUST apply payload.patch to the named store (payload.store or legacy payload.state). If payload.mode is merge (default), patch is a JSON Merge Patch. If payload.mode is replace, the referenced location is replaced with patch.
  • For setState, if payload.pointer is present, consumers MUST apply the patch/replace to that JSON Pointer location within the store instead of replacing the entire store.
  • If an action fails and no catch continuation is defined, consumers MUST stop executing the containing handler.
  • emit is synchronous in the deterministic profile: when an emit action executes, consumers MUST immediately dispatch matching on: "event" handlers (ordered by priority/id) and run them to completion before continuing with the next action.
  • In the deterministic profile, navigate, openModal, closeModal, emit, and setState MUST be treated as synchronous state transitions (they only “fail” on malformed documents). invoke and submit MAY fail due to external dependencies; deterministic verification SHOULD use fixtures or recorded responses.
  • If debounceMs is present, consumers MUST suppress repeated triggers of that handler within the specified time window.
  • Debounce timing: consumers MUST use a monotonic clock. The debounce window is keyed by (handler.id). If id is absent, debounce behavior is non-interoperable and consumers SHOULD treat it as a no-op and emit an ambiguity issue.

Contracts and fixtures (capabilities/events):

  • If present, contractDialect specifies how to interpret capability/event contract objects. In v1.1, the dialect is jsonschema-2020-12.
  • Capability input/output and event payload contracts MUST be self-contained JSON Schema fragments (no remote $ref).
  • For deterministic verification, producers MAY provide capabilities.*.fixtures and actions MAY reference a fixture via invoke.payload.fixtureId. Consumers implementing deterministic replay SHOULD prefer fixtures over live tool calls.
  • If invoke.payload.fixtureId is not present, consumers MAY select the first fixture whose args deep-equals the computed invocation arguments.
  • Capability metadata (recommended for agent safety): producers SHOULD annotate capabilities with sideEffects (none/read/write/external), idempotent, requiresUserConfirmation, and optional privacy tags so agents can make safe tool-selection decisions.
  • Fixture execution is deterministic: if the selected fixture has a result, the invoke action completes successfully with that value. If the selected fixture has an error, the invoke action MUST be treated as failed and MUST execute catch (if present), followed by finally (if present).

State and data bindings:

  • state is an optional registry of named state stores with initial values. Consumers implementing deterministic replay MUST initialize state stores from state.*.initial.
  • dataBindings MAY bind a component property (e.g. value) to a state location. For deterministic replay, producers SHOULD use the structured binding source {"kind":"state","store":"…","pointer":"…"} where pointer is a JSON Pointer.
  • When a binding has direction of out or inout, consumers MUST apply change events to the bound state before executing any matching change handlers.

Trace format (deterministic replay):

  • Consumers SHOULD emit a deterministic trace as an array of events capturing state initialization, input events, handler selection, action execution, emitted events, and state writes.
  • In v1.1, the trace event taxonomy includes state.init, event.input, handler.selected, action.start, action.end, event.emitted, and state.write.
  • Producers MAY redact values in traces based on dataClassification annotations.

Focus and reading order (agent interoperability):

  • By default, consumers SHOULD derive both focus navigation and reading order from the node tree using each node’s children ordering (pre-order traversal).
  • If focusOrder is present, it MUST be treated as the authoritative focus order for scopes listed. Unknown scopes MUST be ignored.
  • If readingOrder is present, it MUST be treated as the authoritative reading order for scopes listed. Unknown scopes MUST be ignored.

Canonical naming (recommended):

  • Capabilities SHOULD use dotted namespaces, e.g. auth.validateCredentials, network.request, analytics.track.
  • Events SHOULD use dotted namespaces, e.g. auth.signedIn, form.submitted, ui.toastShown.

Benefits

  • Keeps the core spec clean: non-AI tools adopt v1 without AI overhead
  • Optional AI layer: only AI pipelines and advanced tooling consume it
  • Backward-compatible: future extensions can be added under extensions
  • Clear roadmap: semantics, behavior, and context are standardized but optional
Example: extensions.ai_native (v1.1)JSON
{
  "version": "1.1.0",
  "source": {"tool": "figma", "fileId": "sample-form-ai-native"},
  "nodes": [
    {"id": "scr_signup", "type": "screen", "name": "Signup", "children": ["cnt_form"], "role": "container"},
    {"id": "cnt_form", "type": "container", "name": "Form", "parentId": "scr_signup", "children": ["cmp_email", "cmp_submit"], "layout": {"type": "stack", "direction": "column", "gap": "{space.3}"}},
    {"id": "cmp_email", "type": "component", "name": "Input/Email", "role": "input", "parentId": "cnt_form"},
    {"id": "cmp_submit", "type": "component", "name": "Button/SignUp", "role": "button", "parentId": "cnt_form"}
  ],
  "extensions": {
    "ai_native": {
      "version": "1.1.0",
      "intent": {
        "cmp_submit": "trigger main user action",
        "cmp_email": "collect user email input"
      },
      "flows": {
        "signup": {"steps": ["cmp_email", "cmp_submit", "@showResult"]}
      },
      "stateMachine": {
        "cmp_submit": {
          "default": ["hover", "focus", "disabled"],
          "hover": ["active", "default"],
          "disabled": []
        }
      },
      "constraints": [
        {"scope": "cmp_submit", "if": {"intent": "danger"}, "then": {"confirmationRequired": true}},
        {"scope": "cmp_email", "if": {"required": true}, "then": {"mustValidateEmailFormat": true}}
      ],
      "contextRoles": {
        "cmp_submit": ["form.submit"],
        "cmp_email": ["form.input"]
      },
      "dataBindings": [
        {"component": "cmp_email", "source": "user.email"}
      ],
      "actions": {
        "act_submit": {"type": "submit", "target": "scr_signup", "payload": {"method": "POST", "endpoint": "/signup"}, "description": "submit the signup form"},
        "act_showResult": {"type": "navigate", "target": "@showResult", "payload": {"mode": "push"}}
      },
      "capabilities": {
        "auth.validateCredentials": {
          "description": "Validate credentials and return an auth result",
          "input": {"type": "object", "properties": {"mode": {"enum": ["strict", "lenient"]}}},
          "output": {"type": "object", "properties": {"ok": {"type": "boolean"}, "reason": {"type": "string"}}}
        }
      },
      "events": {
        "form.submitted": {
          "description": "Form submitted",
          "payload": {"type": "object", "properties": {"scope": {"type": "string"}, "status": {"type": "string"}}}
        }
      },
      "eventHandlers": [
        {"id": "eh_submit", "on": "click", "scope": "cmp_submit", "preventDefault": true, "do": ["act_submit", "act_showResult"]}
      ],
      "a11y": {
        "cmp_email": {"label": "Email", "required": true},
        "cmp_submit": {"label": "Sign up"}
      },
      "validation": {
        "cmp_email": {"required": true, "type": "string", "format": "email"}
      },
      "confidence": {
        "componentClassification": 0.82,
        "layoutInference": 0.7
      }
    }
  }
}

Token override modes (v1.1)

This optional extension standardizes theme/mode token overrides (e.g. light/dark) without changing core token semantics. It MUST be safely ignorable by consumers that do not implement it.

For interoperability, override modes SHOULD only override values for token keys already declared in the document-level tokens dictionary.

Location

The extension lives under extensions.token_overrides on the document root.

Example: extensions.token_overrides (v1.1)JSON
{
  "version": "1.1.0",
  "source": {"tool": "figma", "fileId": "sample-themed"},
  "tokens": {
    "color.bg": {"type": "color", "value": "#FFFFFF"},
    "color.fg": {"type": "color", "value": "#0F172A"}
  },
  "nodes": [],
  "extensions": {
    "token_overrides": {
      "version": "1.1.0",
      "defaultMode": "light",
      "modes": {
        "light": {"tokens": {"color.bg": {"type": "color", "value": "#FFFFFF"}}},
        "dark": {"tokens": {"color.bg": {"type": "color", "value": "#0B1220"}}}
      }
    }
  }
}