---
title: "The .bar Format Specification: v0 Dashboard Standard · Metrix.Bar"
description: "Authoritative reference for the Metrix.Bar .bar v0 format: root <bar> tag, permissions, inputs, JavaScript Worker, lifecycle hooks, host APIs, and UI trees."
canonical: "https://metrix.bar/docs/bar-format"
language: "en"
---

# The .bar Format Specification

> Complete specification for .bar v0 dashboards in Metrix.Bar.

## Overview

A `.bar` file defines a complete dashboard in a single declarative document. It packages network permissions, user configuration parameters, and a persistent JavaScript Worker that fetches data, computes metrics, and renders native tray and panel interfaces.

- **Single file**: Encapsulates metadata, inputs, and rendering logic without auxiliary files.
- **Persistent Worker**: A JavaScript ES module runs on the desktop, schedules updates, and manages state.
- **Native UI Tree**: The Worker returns lightweight JSON tree structures (`{type, props, children}`) that project directly into native desktop components.
- **ESM library imports**: Import modern JavaScript libraries (such as `htm`) from CDN URLs declared in the network whitelist.

## File Structure

A `.bar` document starts with a root `<bar protocol-version="0">` tag containing child blocks for permissions, inputs, setup instructions, and the script Worker:

```bar
<bar protocol-version="0" version="1.0.0" icon="chart.bar" description="Website traffic metrics" homepage="https://agili.app" id="unique-id" title="Dashboard Title" refresh="5m" material="glass">
  <permissions>
    <network origin="https://api.example.com" />
  </permissions>

  <inputs>
    <input name="endpoint" type="string" label="API Endpoint" default="https://api.example.com" />
  </inputs>

  <docs><![CDATA[Configure your API endpoint to start collecting metrics.]]></docs>

  <script>
  let cache = null;

  export default {
    async scheduled(env) {
      const res = await fetch(`${env.inputs.endpoint}/stats`);
      if (res.ok) {
        cache = await res.json();
      }
    },
    render(env) {
      return {
        tray: {
          title: cache ? `${cache.count} ops` : "Loading..."
        },
        panel: {
          type: "Column",
          props: { padding: 16, gap: 12 },
          children: [
            { type: "Text", props: { bold: true }, children: ["System Status"] },
            { type: "Text", children: [cache ? `Active: ${cache.count}` : "Waiting for data..."] }
          ]
        }
      };
    }
  };
  </script>
</bar>
```

| Element         | Required | Format             | Purpose                                                                                                                                        |
| --------------- | -------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `<bar …>`       | Yes      | XML tag attributes | Root definition: `protocol-version`, `id`, optional metadata, and `refresh` interval                                                           |
| `<permissions>` | Optional | XML tags           | Declare allowed network origins for `fetch()` / ESM `import()` and local file paths for `$metrix.readFile()`                                   |
| `<inputs>`      | Optional | XML `<input>` tags | Configurable user settings and credentials                                                                                                     |
| `<docs>`        | Optional | Plain text / CDATA | Plain-text setup and configuration instructions for users, displayed in the catalog, app, and API                                              |
| `<script …>`    | Yes      | Raw JavaScript     | Persistent ES module Worker, optional `apis` attribute declaring extended system capabilities, implementing `scheduled`, `render`, and `event` |

---

## `<bar>` Root Element

The root element declares core dashboard metadata:

| Attribute          | Type     | Required | Description                                                                                                                                                                                                                           |
| ------------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `protocol-version` | string   | **Yes**  | Protocol version; must be `"0"`.                                                                                                                                                                                                      |
| `material`         | string   | No       | Panel content material: `"glass"` (default) or `"solid"`; exact lowercase values only; empty values are rejected. Does not change the Dialog backdrop.                                                                                |
| `placements`       | string   | No       | Author-default entrances: any whitespace-separated subset of `menubar`, `sidebar-left`, `sidebar-right`, `sidebar-bottom`; omitted means `menubar`, an empty string disables all entrances. Unknown or duplicate values are rejected. |
| `open-mode`        | string   | No       | Author-default opening mode: `flyout` (default) or `dialog`. Empty or other values are rejected.                                                                                                                                      |
| `version`          | string   | No       | Dashboard code version, e.g. `"1.0.0"`; independent of the App version.                                                                                                                                                               |
| `icon`             | string   | No       | Image reference: URL, `data:` URI, or named icon.                                                                                                                                                                                     |
| `description`      | string   | No       | Dashboard description.                                                                                                                                                                                                                |
| `license`          | string   | No       | License declaration, such as an SPDX identifier. Omission grants no open-source license.                                                                                                                                              |
| `homepage`         | string   | No       | Developer homepage.                                                                                                                                                                                                                   |
| `category`         | string   | No       | Stable slug grouping the dashboard in registries (`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`); defaults to `other`.                                                                                                                               |
| `id`               | string   | **Yes**  | Unique dashboard identifier containing alphanumeric characters, hyphens, and underscores.                                                                                                                                             |
| `title`            | string   | **Yes**  | Human-readable title displayed in menus and headers.                                                                                                                                                                                  |
| `refresh`          | duration | No       | Default polling schedule. Defaults to `5m`. Minimum is `10s`.                                                                                                                                                                         |

Both materials retain the native NSPopover arrow, corners, and shadow in the macOS tray. The Host owns the shell material; the Worker renders only the inner content. On macOS 14+, the solid shell, including its arrow, uses opaque theme Surface. On macOS 12/13, solid content is opaque while the arrow keeps the system material. Other platforms retain their existing flyout styles.

Only `protocol-version` is checked for protocol support. The legacy `version="0"` root attribute does not declare a protocol version and is rejected without `protocol-version`. Descriptive metadata is stored as strings; `material`, `category`, `placements`, and `open-mode` follow their validation rules above. The main window and dashboard catalog display `icon`: URLs and named icons load asynchronously through the icon cache, while `data:` URIs decode locally. The menu bar uses the separate `tray.icon` returned by `render`. The official dashboard registry at `/dashboards/index.json` distributes the official `.bar` sources (each declaring `license="AGPL-3.0-only"`). Only the GitHub repository dashboard is bundled with the app — every other official dashboard installs from the registry, where the short source `id` is rewritten to a [composite install ID](/docs/registries#install-ids). The local API list and creation summaries expose `protocol_version` and non-empty metadata fields; the public dashboard index also retains `name` and `file` and includes `id`.

For example, `placements="sidebar-bottom" open-mode="dialog"` makes the bottom screen Sidebar the default entrance and opens its panel as a Dialog. The `<bar>` format defines author defaults only via `open-mode`; it does not introduce per-source XML attributes. All official and third-party dashboards support all entrances and shortcuts without per-dashboard allowlists. Users can select any combination of Menu Bar and three screen Sidebars, and choose Flyout or Dialog independently for each of the five trigger sources (Menu Bar, Left Sidebar, Right Sidebar, Bottom Sidebar, and Keyboard Shortcut; for instance, Menu Bar in Flyout alongside Keyboard Shortcut and Right Sidebar in Dialog). Overrides survive source updates; without an override, the author defaults follow the updated definition.

Multiple entrances share one Worker, one panel state, and the existing `render` result `{ tray, panel }`. Sidebar buttons show icons only, falling back from `tray.icon` to `bar.icon` to a generic dashboard glyph. Tooltip text falls back from `tray.tooltip` to `tray.title` to `bar.title`; metric formatting remains in the dashboard script. See [dashboard entrances and opening modes](/docs/dashboards#dashboard-entrances-and-opening-modes) for edge activation, Dialog behavior, and platform availability.

The public dashboard index exposes author defaults as a `placements` array and an `open_mode` string. These fields do not contain a user's settings.

**Duration Format**: Numbers followed by `s`, `m`, `h`, or `d` (for example, `30s`, `5m`, `1h`).

---

## `<permissions>` Block

Declares runtime capabilities granted to the dashboard Worker. Outbound network requests, dynamic ESM imports, and local file reads are blocked unless explicitly declared:

```xml
<permissions>
  <network origin="https://api.github.com" />
  <network origin="https://esm.sh" />
  <file path="{{ inputs.auth }}" />
  <file path="~/.claude/projects/" />
</permissions>
```

### Network Permissions (`<network>`)

- Each `<network origin="..." />` specifies an allowed scheme and host (`https://domain.com`).
- The port may be included if non-standard (such as `http://localhost:8080`).
- Supports `{{ inputs.x }}` template expansion (rules evaluating to empty are ignored).

### File Permissions (`<file>`)

Reading local files must be explicitly declared in `<permissions>`:

- Each `<file path="..." />` grants read access to a specific file or directory. Multiple tags can be declared; `path` is required.
- **Path Expansion**: Supports `~` to expand to the user's home directory and `{{ inputs.x }}` template expansion matching input values. Rules evaluating to empty are ignored. The resolved path must be an absolute path.
- **Directories vs. Single Files**: Paths ending with a trailing slash (`/`) grant read access to the entire directory (including all subdirectories). Paths without a trailing slash permit only that specific file.

---

### External Credentials (`<credential>`)

Declare `<credential service="Example App" account="{{ user.name }}" />` to use `await $metrix.readCredential("Example App", $metrix.userName)`. Each declaration grants one exact service/account pair; separate declarations cannot be combined. Both attributes are required, limited to 256 bytes each, and cannot be blank or contain NUL. Service names cannot contain `/` or `\` or use templates. Accounts accept a static name or the complete `{{ user.name }}` token. The old `source` format is no longer supported.

`$metrix.userName` captures the host username when the Worker starts, using `USER` or the system username. The dashboard chooses service-specific account rules, fallbacks and credential priority. The host reads through MyGO’s platform credential store in the background; the system may ask for authorization. Missing items, denied access and unavailable stores reject the Promise without exposing system errors. The UTF-8 result is limited to 1 MiB and is not copied into Metrix.Bar’s credential store. Service names beginning with `bar.metrix` (case-insensitive) are reserved to protect app and instance secrets.

Fixtures use their own `user_name` (default `fixture-user`) and a `credentials` array of `{service, account, value}` objects. Reads require an exact pair and never fall back to the real system.

## `<inputs>` Configuration

The `<inputs>` element contains XML `<input>` declarations displayed in the dashboard settings form:

```xml
<inputs>
  <input name="repo" type="string" label="Repository" placeholder="owner/repo" default="egoist/mygo" required="true" />
  <input name="token" type="secret" label="Access Token" help="Personal access token with read permissions." />
</inputs>
```

### Supported Input Types

| Type      | Description                                                                                                                                                                                                                |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `string`  | Single-line text input                                                                                                                                                                                                     |
| `integer` | Whole integer value                                                                                                                                                                                                        |
| `number`  | Floating-point number                                                                                                                                                                                                      |
| `bool`    | Toggle checkbox                                                                                                                                                                                                            |
| `secret`  | Sensitive credential stored securely in the OS keychain                                                                                                                                                                    |
| `file`    | Local file path picker. Allows users to select or update a file path. The Worker retrieves the path from `env.inputs.<name>` and passes it to `$metrix.readFile(path)`, declared via `<file path="{{ inputs.<name> }}"/>`. |
| `select`  | Dropdown picker with child `<option value="..." label="..." />` declarations                                                                                                                                               |

### Common Field Attributes

- `name` _(string, required)_: Identifier accessed in code via `env.inputs[name]`.
- `type` _(string, required)_: One of the types listed above.
- `label` _(string)_: Label displayed in the UI form.
- `default` _(any)_: Initial fallback value.
- `required` _(boolean)_: Whether the user must provide a value before running.
- `placeholder` _(string)_: Placeholder text shown in empty inputs.
- `help` _(string)_: Helper text explaining the configuration.

---

## `<docs>` Setup Instructions

The optional `<docs>` element provides plain-text instructions guiding users through required credentials, API keys, or prerequisites:

```xml
<docs><![CDATA[Requires an API key from Settings → API Keys. Paste the key into the API Key input.]]></docs>
```

- **Plain text / CDATA**: Content can be written directly or wrapped in `<![CDATA[ ... ]]>` to avoid XML character escaping issues.
- **Whitespace handling**: The parser strips leading and trailing whitespace while preserving internal indentation and newlines.
- **Strict constraints**: The element accepts no attributes and must not contain nested XML tags. Markdown is not rendered, and text is treated as informational only.
- **No execution**: Commands or code snippets inside `<docs>` are not executed by the application or by AI agents.
- **Propagation**: Documentation flows automatically from `.bar` definitions into the registry `index.json` (`docs` field), the website catalog, the desktop application UI (dashboard catalog detail, install sheets, and input settings drawers), and the Local API snapshots and summaries. If omitted or empty, no documentation section is displayed.

---

## `<script>` Worker Runtime & Lifecycle

The `<script>` block runs in a persistent, Worker-style ES module environment with a dedicated `scheduled`, `render`, and `event` lifecycle:

- **Global Environment**: The global scope is `self` / `globalThis`; there is **no** `window` or `document`. Module-level variables persist across renders, and `localStorage` provides persistent key-value storage across reloads.
- **Standards & Extension Boundaries**: Workers provide a curated subset of Web standard APIs (e.g. `fetch`, streams, `TextEncoder`). APIs such as `localStorage`, `matchMedia`, `screen`, `navigator.getBattery`, and `navigator.clipboard` are Window-style opt-in extensions for desktop dashboards; desktop-specific features are exposed through `$metrix` (core utilities such as `log`, `invalidate`, and `appVersion` are built-in, while system and privileged APIs require corresponding `apis` attributes or `<permissions>` declarations). Timer handles return Node-style objects and `Buffer` is available, but these are non-standard extensions; do not assume full Web Worker or Node.js compatibility.
- **High-Resolution Time**: `performance.now()` returns monotonic floating-point milliseconds; `performance.timeOrigin` is the Unix millisecond timestamp captured when the Worker was created.
- **Snapshots & Constrained Semantics**:
  - `Notification` (requires `apis="notification"`): Provides basic desktop notification delivery (`title`, `body`, `tag`); does not provide `permission`, `requestPermission()`, `close()`, or click/error event handlers.
  - `navigator.getBattery()` (requires `apis="battery"`): Returns a charging status snapshot (`charging: boolean`) with `level` fixed at `1` (placeholder); does not provide battery percentages or level change events.
  - `screen` (requires `apis="screen"`): A static snapshot of the primary display captured at Worker creation (`width`, `height`, `availWidth`, `availHeight`, `colorDepth`, `pixelDepth`); dynamic queries for multiple displays or pointer coordinates must use `$metrix.system('screen.*')`.

The Worker must `export default` an object implementing any of the following handlers:

```javascript
let count = 0;

export default {
  async scheduled(env) {
    // Polling logic triggered on schedule or manual refresh
    count++;
    $metrix.invalidate();
  },

  render(env) {
    // Synchronously returns UI trees for menu bar item and popover panel
    return {
      tray: {
        title: `Count: ${count}`,
        icon: "activity",
      },
      panel: {
        type: "Column",
        props: { padding: 12, gap: 8 },
        children: [
          { type: "Text", props: { text: `Total: ${count}`, fontSize: 16, bold: true } },
          {
            type: "Button",
            props: {
              text: "Reset",
              onClick: () => {
                count = 0;
                $metrix.invalidate();
              },
            },
          },
        ],
      },
    };
  },

  async event(name, payload) {
    // External events posted via local HTTP API
    if (name === "reset") {
      count = 0;
    }
  },
};
```

### Lifecycle Handlers

| Handler                | Description                                                                                                               |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `scheduled(env)`       | Invoked when the dashboard schedule triggers or when manually refreshed. Ideal for network fetches and long computations. |
| `render(env)`          | Returns `{ tray, panel }`. Called whenever the UI needs to update. Should be fast and synchronous.                        |
| `event(name, payload)` | Handles host system events and custom events dispatched via the local API (`POST /v0/dashboards/:id/events/:name`).       |

### Host System Events

The host invokes the Worker's `event(name, payload)` hook for dashboard lifecycle transitions, panel visibility changes, and registered global shortcuts. The handler supports `async`:

| Event                                        | When it fires                                                                                                       |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `event("lifecycle", { type: "start" })`      | Once when an active Worker starts or is rebuilt after a fault.                                                      |
| `event("lifecycle", { type: "stop" })`       | Before destruction when removing a dashboard, replacing its Worker after a successful reload, or quitting normally. |
| `event("lifecycle", { type: "expand" })`     | When the Flyout or Dialog panel becomes visible.                                                                    |
| `event("lifecycle", { type: "collapse" })`   | When the Flyout or Dialog panel becomes hidden.                                                                     |
| `event("shortcut", { accelerator: string })` | When a global shortcut registered by the dashboard is pressed.                                                      |

`lifecycle` replaces the former `panel` event. Showing or hiding the Sidebar rail does not emit panel visibility events. A configured, available Sidebar keeps its dashboard active and refreshing while the rail is hidden. With no available entrance, preview, or open panel, the dashboard becomes idle under the existing activity rules; an unavailable Sidebar alone does not keep it active.

`stop` runs after canceling the current call and has an independent **2-second JS / Promise cleanup budget**. The Worker is closed even if cleanup throws or times out, with no further render. Delivery is not guaranteed after a crash, forced termination, or runtime failure. The budget cannot forcibly interrupt blocking Go host calls.

Normal startup runs `start → scheduled(initial) → render`. Import and reload preflight runs `scheduled(initial) → render` without start / stop events. Successful activation then runs `start → render` without repeating initial. Therefore, `scheduled` must not depend on data initialized by `start`; initialize data in module state or `scheduled`. A successful reload stops and closes the old Worker before starting the new one.

Define the handler alongside the required `render` in the default export:

```js
async event(name, payload) {
  if (name !== "lifecycle") return;
  switch (payload.type) {
    case "start":
      // Set up long-lived resources
      break;
    case "stop":
      // Await cleanup within the 2-second budget
      break;
    case "expand":
      // Panel expanded
      break;
    case "collapse":
      // Panel collapsed
      break;
  }
}
```

---

## Persistent SQLite databases

Declare `<script apis="sqlite">` (or `all`) to open one or more named databases through `$metrix.sqlite`. Databases are isolated by dashboard ID and database name under the app data directory's `SQLite/` folder and survive Worker reloads and app restarts. Names must contain 1–64 ASCII letters, digits, underscores, or hyphens; file paths are not accepted. Databases are not included in `.bar` exports and should not store secrets.

| Method                                                      | Result                                                                                    |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `await $metrix.sqlite.open(name)`                           | A database connection                                                                     |
| `db.prepare(sql)`                                           | Creates a prepared statement synchronously                                                |
| `statement.bind(...params)`                                 | Returns a new bound statement synchronously, preserving the original                      |
| `await statement.all()` / `await statement.run()`           | `{success: true, results: [rowObject, ...], meta: {changes, last_row_id, duration}}`      |
| `await statement.raw({columnNames: true})`                  | An array of row arrays, optionally preceded by column names; names are omitted by default |
| `await statement.first()` / `await statement.first(column)` | The first row object / column value; null for no rows, an error for a missing column      |
| `await db.batch([statement, ...])`                          | An ordered array of all results; all writes roll back on failure                          |
| `await db.close()`                                          | Closes the connection without deleting the database; repeat calls are safe                |

Each SQL string must contain exactly one statement. `all` and `run` have identical behavior, supporting reads, writes, and RETURNING; `results` is `[]` when no rows are returned. `meta.changes` reports affected rows, `last_row_id` is the connection's most recent inserted rowid, and `duration` measures statement execution and result collection in milliseconds. No remote service or read/write row statistics are fabricated. Duplicate column names keep the last column in row objects; use `raw` to preserve every column.

Pass parameters through `bind(...params)`: null, boolean (stored as 0/1), finite number, signed 64-bit BigInt, string, or binary buffer/view values. Binary inputs are copied when bound. BigInt and binary results are extensions to this interface: use BigInt for integers outside JavaScript's safe range; reads also return BigInt for these integers. BLOB results are ArrayBuffers, readable with `new Uint8Array(value)`. `prepare` / `bind` validate input synchronously; SQL parsing and execution happen during asynchronous calls. Invalid input may throw synchronously; database failures reject the Promise. Handle both with try/catch.

Statements can be reused, and `bind` preserves earlier bindings. `batch` only accepts statements created by that connection; another connection to the same named database cannot supply statements. Connection operations are serialized, but independently submitted concurrent calls have no ordering guarantee: await dependent calls and use `batch` for atomic operations. Each execution collects results and completes writes inside a host-managed transaction; SQL errors, cancellation, and result limits roll back writes. `first` still applies the full result budget; use SQL LIMIT when only a few rows are needed.

This complete script saves a sample on refresh and reads the current record count:

```bar
<script apis="sqlite"><![CDATA[
let database;
let count = 0;
async function getDatabase() {
  if (!database) {
    const db = await $metrix.sqlite.open('history');
    await db.prepare('CREATE TABLE IF NOT EXISTS samples (time INTEGER, value REAL)').run();
    database = db;
  }
  return database;
}
export default {
  async scheduled() {
    const db = await getDatabase();
    await db.prepare('INSERT INTO samples VALUES (?, ?)').bind(Date.now(), 42).run();
    count = await db.prepare('SELECT COUNT(*) AS count FROM samples').first('count');
  },
  render() {
    return {tray: {title: String(count)}};
  }
};
]]></script>
```

Open another independent database with `open('archive')`. Limits: 8 persistent names per dashboard; 8 connections and 4 pending operations per Worker; 10 seconds per operation, also subject to hook cancellation; 1 MiB of SQL and parameters per execution or entire batch; 100 statements per batch; 10,000 rows and 8 MiB of results per execution or entire batch, including structural overhead; 32 MiB of pages each for the main database and in-memory temporary database. All dashboards share a 128 MiB SQLite heap budget. This is not a total app memory limit, and other dashboards' queries can consume the shared budget.

This API provides common D1 prepared statement and batch methods. It omits exec, dump, sessions, interactive transactions, and streaming. Prebundled `kysely@0.27.6` + `kysely-d1@0.4.0` has passed table creation and basic CRUD, including RETURNING, in a real Worker. `kysely@0.28.18` and `0.29.7` try to write `Symbol.asyncDispose` during initialization, which the current Worker does not support. `kysely-d1` itself does not support interactive transactions or streaming.

Tables, indexes, JOINs, and aggregates are supported. User PRAGMA, ATTACH/DETACH, VACUUM, extension loading, virtual tables, and direct transaction control are unavailable. Worker shutdown closes its connections. Native development checks use disposable isolated databases; browser Playground / WASM Sandbox does not expose SQLite.

## `<script apis="…">` System Capabilities

By default, Workers run in a secure sandbox. Dashboards requiring system-level integration can request capabilities by declaring them in the `apis` attribute of `<script>` (space-separated):

```xml
<script apis="shell badge shortcut dialog app clipboard match-media">
```

| API Name       | Capability                        | Core Methods & Rules                                                                                                                                                                                                                                                                                                                                            |
| -------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shell`        | External applications and sound   | `$metrix.openURL(url)` (restricted to `http`, `https`, `mailto`)<br />`$metrix.openPath(path)` / `$metrix.showItemInFolder(path)` (path must be granted in `<file>` permissions)<br />`$metrix.beep()` (system alert sound)                                                                                                                                     |
| `badge`        | App icon badge counter            | `navigator.setAppBadge(count)` (sets Dock badge count)<br />`navigator.clearAppBadge()` (clears Dock badge)                                                                                                                                                                                                                                                     |
| `shortcut`     | Global keyboard shortcuts         | `$metrix.registerShortcut(accelerator)` (e.g. `"Cmd+Shift+P"`)<br />`$metrix.unregisterShortcut(accelerator)`<br />Dispatches `event("shortcut", { accelerator })`                                                                                                                                                                                              |
| `dialog`       | System file and message dialogs   | `$metrix.showOpenDialog(options)` (returns paths; selected files temporarily added to `readFile` allowlist)<br />`$metrix.showSaveDialog(options)` (returns destination path)<br />`$metrix.writeFile(path, data, {expectedVersion?}?)` (capped at 8 MB; restricted to confirmed save destination)<br />`$metrix.showMessageBox(options)` (modal message alert) |
| `app`          | Application lifecycle             | `$metrix.quit()` (terminates Metrix.Bar)                                                                                                                                                                                                                                                                                                                        |
| `clipboard`    | System clipboard access           | `navigator.clipboard.read()` / `write(items)`<br />Supports `ClipboardItem` with plain text, HTML, and PNG images                                                                                                                                                                                                                                               |
| `match-media`  | System appearance & media queries | `globalThis.matchMedia(query)`<br />Supports color scheme, reduced motion, reduced transparency, and contrast queries; see below for details                                                                                                                                                                                                                    |
| `screen`       | Primary display static snapshot   | `screen.width` / `height` / `availWidth` / `availHeight` startup snapshot; dynamic multi-display or cursor queries use `$metrix.system('screen.*')`                                                                                                                                                                                                             |
| `battery`      | Power status inspection           | `navigator.getBattery()`<br />Returns charging status snapshot (`charging: boolean`, placeholder `level: 1`); enables `power.*` system commands and power event notifications                                                                                                                                                                                   |
| `notification` | Desktop system notifications      | `new Notification(title, { body, tag })`<br />Basic notification delivery without `permission`, `requestPermission()`, `close()`, or click/error events                                                                                                                                                                                                         |

---

### Media Queries (`match-media`)

When `apis="match-media"` is declared, query system appearance using `globalThis.matchMedia(query)`:

- `(prefers-color-scheme: dark | light)`: Dark / light theme appearance.
- `(prefers-reduced-motion: reduce | no-preference)`: Reduced motion accessibility preference.
- `(prefers-reduced-transparency: reduce | no-preference)`: Reduced transparency accessibility preference.
- `(prefers-contrast: more | no-preference)`: High contrast accessibility preference (only `more` and `no-preference` evaluate against system settings; `less` and `custom` never match).
- Supports live appearance change events via `addEventListener("change", listener)`.

---

### Versioned text and safe saving

Declare `apis="dialog"` for these methods. `readTextFile(path)` returns `Promise<{data:string,version:string}>`: valid UTF-8 text and its lowercase SHA-256 hex digest from the **same read**, at most 8 MiB. It uses `<permissions><file>` or open-dialog read authorization. A save dialog grants no content-read permission. Existing `readFile` retains its 1 MiB limit. `fileInfo(path)` returns `Promise<{size:number,version:string}>` for a read- or save-authorized regular file up to 8 MiB; a missing file rejects.

`writeFile(path,data,{expectedVersion?}?)` accepts a string, ArrayBuffer or TypedArray up to 8 MiB and returns `Promise<{version:string}>`, hashing the bytes actually written. Only this Worker's save-dialog-confirmed destinations are writable; file permissions alone do not grant writes. Omit expectedVersion for overwrite, pass `null` to require absence, or pass a digest to require matching current content. A conflict rejects with `file changed since it was read`. Checks and saves for a target are serialized within the host; this is optimistic pre-write conflict detection, without atomic CAS against other processes.

The following is a complete `<script apis="dialog">` module body. Dialogs and saving run only when the user clicks. It round-trips text; to edit it, connect document.data to an editor's onChange. Saving to the original path uses the same-read version; a different path requires absence. Handle a conflict by asking the user to reload or choose another destination rather than retrying unconditionally.

```js
let document;
export default {
  render() {
    return {
      panel: {
        type: "Column",
        children: [
          {
            type: "Button",
            props: {
              text: "Open",
              onClick: async () => {
                const [path] = await $metrix.showOpenDialog({});
                if (path) document = { path, ...(await $metrix.readTextFile(path)) };
              },
            },
          },
          {
            type: "Button",
            props: {
              text: "Save",
              onClick: async () => {
                if (!document) return;
                const path = await $metrix.showSaveDialog({});
                if (path === null) return;
                // A different destination must not silently overwrite an existing file.
                const expectedVersion = path === document.path ? document.version : null;
                const result = await $metrix.writeFile(path, document.data, { expectedVersion });
                document = { ...document, path, version: result.version };
              },
            },
          },
        ],
      },
    };
  },
};
```

## Host Environment (`env`)

The `env` object passed to handlers provides access to configuration, secrets, and desktop system APIs:

| Property / Method                          | Description                                                                                                                                                                                                                                                               |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `env.inputs`                               | Plain JavaScript object of current input values (`env.inputs.repo`).                                                                                                                                                                                                      |
| `$metrix.secret(name)`                     | Synchronously returns a declared secret input as a string from secure storage; throws on unavailable or undeclared names. Secrets are absent from env.inputs; env has no secrets property.                                                                                |
| `$metrix.readFile(path)`                   | Asynchronously reads the contents of a local file declared in `<permissions>` (returns `Promise<string>`). Accepts direct paths (supports `~`). Resolves `..` and symlinks; restricted to regular files up to 1 MB. Throws if undeclared or outside permitted boundaries. |
| `$metrix.toast(message)`                   | Displays a transient desktop toast notification.                                                                                                                                                                                                                          |
| `$metrix.notify(title, body?)`             | Sends a system desktop notification.                                                                                                                                                                                                                                      |
| `$metrix.log(...args)`                     | Outputs messages to the application log.                                                                                                                                                                                                                                  |
| `$metrix.invalidate()`                     | Triggers an immediate re-render of the dashboard.                                                                                                                                                                                                                         |
| `$metrix.togglePanel()`                    | Toggles this dashboard panel using the effective Flyout or Dialog setting, including when its menu bar entrance is disabled. Requires no opt-in.                                                                                                                          |
| `$metrix.appVersion`                       | Read-only host application version string (e.g. `"0.0.1"`). Requires no `apis` permissions.                                                                                                                                                                               |
| `$metrix.userName`                         | Host username captured when the Worker starts; use it with an explicitly declared credential account. Fixtures default to `fixture-user`.                                                                                                                                 |
| `$metrix.readCredential(service, account)` | Read one declared system credential pair asynchronously; rejects undeclared pairs and reserved app services.                                                                                                                                                              |
| `$metrix.timeZone`                         | Read-only host IANA time zone (e.g. `"Europe/Berlin"`), the same zone `Date` uses locally, ready to pass to servers. Requires no `apis` permissions.                                                                                                                      |

### Local File Reading Example

When reading local files with `$metrix.readFile(path)`, target paths must be explicitly declared in `<permissions>`:

```xml
<permissions>
  <network origin="https://chatgpt.com" />
  <file path="{{ inputs.auth }}" />
  <file path="~/.claude/projects/" />
</permissions>
```

```javascript
// Read a file selected by the user in a file-type input
const raw = await $metrix.readFile(env.inputs.auth);

// Read a file from an explicitly permitted directory
const raw2 = await $metrix.readFile("~/.claude/projects/a.jsonl");
```

---

## Dynamic ESM Imports

You can import modern JavaScript packages from ESM CDNs. Every origin in the redirect chain must be declared in `<permissions>` and use HTTPS.

Importing or creating a dashboard downloads its recursive static imports and re-exports before saving it, without executing top-level JavaScript. This step allows up to 15 seconds, 128 remote modules, and 16 MiB of source in total, with a 1 MiB limit per module. A download or cache write failure cancels the import. Dynamic `import()` downloads and caches its dependencies when first called.

Declare the CDN origins before importing a library:

```bar
<permissions>
  <network origin="https://esm.sh" />
</permissions>

<script>
let html;

export default {
  async scheduled(env) {
    if (!html) {
      const htm = await import("https://esm.sh/htm");
      const h = (type, props, ...children) => ({
        type,
        props: props || {},
        children: children.flat().filter(Boolean)
      });
      html = htm.default.bind(h);
    }
  },

  render(env) {
    return {
      tray: { title: "JSX/HTM Enabled" },
      panel: html`
        <Column padding=${16} gap=${8}>
          <Text text="Rendered with htm" bold=${true} />
        </Column>
      `
    };
  }
};
</script>
```

For a comprehensive selection guide and compatibility details across UI tree libraries (such as Preact, Vue, React, and Snabbdom) and state managers, see the [Recommended Tech Stack](/docs/tech-stack.md) guide.

---

## UI Tree Specification

The `render(env)` hook returns `{ tray, panel }`:

- `tray`: Defines the menu bar / system tray presentation:
  - `title`: Short string shown directly in the menu bar.
  - `icon`: System icon name or remote image URL.
  - `iconTemplate`: Optional boolean. When set to `true`, forces macOS Template image rendering (adapting automatically to dark mode and menu highlights).
  - `tooltip`: Tooltip string shown on mouse hover.
  - `menu`: Context menu items array shown on right-click. Items support:
    - `label`: Menu item label text.
    - `onClick`: Click handler function.
    - `disabled`: Boolean to disable item.
    - `separator`: Boolean separator line.
    - `checked`: Boolean checkmark indicator.
    - `radio`: Boolean radio indicator (exclusivity managed by script).
    - `shortcut`: Shortcut accelerator string (e.g. `"Cmd+Shift+P"`).
    - `tooltip`: Tooltip text.
    - `items`: Recursive array of child menu items for nested submenus.
- `panel`: A tree node representing the popover panel content:
  - `type`: Native component name (see [Components](/docs/components)).
  - `props`: Object of component properties and event callbacks (`onClick`, `onChange`, `onSubmit`).
  - `key`: Optional stable node identifier, unique among siblings. A node used as an `activeDescendant` or `scrollWith` target needs a unique key within the panel.
  - `children`: Array of child tree nodes.

Native input callbacks collected in one UI build are delivered in order, with no render between callbacks once that batch starts executing. A batch accepts at most 32 callbacks. A callback from a view replaced before execution can still expire. See [Components](/docs/components) for text input observers and node references.

## Desktop system commands

`$metrix.system(command, args?)` returns a Promise. Declare the opt-in below in `<script apis="...">`; `all` includes these capabilities. Missing arguments mean `{}`; unknown commands, fields, types, or missing permissions reject.

| Command                                                 | Arguments            | opt-in        |
| ------------------------------------------------------- | -------------------- | ------------- |
| `screen.displays`, `screen.primary`, `screen.cursor`    | —                    | `screen`      |
| `screen.nearest`                                        | `{x,y}`              | `screen`      |
| `screen.matching`                                       | `{x,y,width,height}` | `screen`      |
| `theme.source`, `theme.preferences`, `theme.isDark`     | —                    | `match-media` |
| `network.status`                                        | —                    | —             |
| `power.idleTime`                                        | —                    | `battery`     |
| `power.keepAwake`                                       | `{reason,display?}`  | `battery`     |
| `power.release`                                         | `{token}`            | `battery`     |
| `clipboard.formats`, `clipboard.clear`                  | —                    | `clipboard`   |
| `shell.installedApps`                                   | —                    | `shell`       |
| `shell.launchApp`                                       | `{token}`            | `shell`       |
| `shell.appIcon`                                         | `{token,size}`       | `shell`       |
| `shell.fileIcon`                                        | `{path,size}`        | `shell`       |
| `shell.trash`                                           | `{path}`             | `shell`       |
| `app.locale`, `app.languages`, `app.region`, `app.info` | —                    | `app`         |
| `app.path`                                              | `{name}`             | `app`         |
| `app.focus`, `app.hide`, `app.show`, `app.relaunch`     | —                    | `app`         |
| `watch.start`                                           | `{path,recursive?}`  | `watch`       |
| `watch.stop`                                            | `{token}`            | `watch`       |

Screen coordinates are integers; matching dimensions are positive integers. `network.status` returns `{online,metered}`, and `navigator.onLine` reads the host connection snapshot. Theme queries do not change the global theme. `power.idleTime` returns milliseconds; keepAwake/watch.start return Worker-scoped tokens. Installed applications are capped at 4096; a new enumeration invalidates previous application tokens. Launch and appIcon accept these tokens only. Icon sizes are integers 1–256 and results are PNG data URLs up to 1 MiB. fileIcon/watch require existing file-read permission; directory watching requires full directory authorization. `shell.trash` explicitly rejects because deletion authorization is not implemented.

`app.info` returns `{name,version,packaged}`. app.path names are `home`, `appData`, `userData`, `cache`, `logs`, `resources`, `temp`, `exe`, `desktop`, `documents`, `downloads`, `music`, `pictures`, `videos`; returned paths grant no read/write rights. Some official directory queries create application directories. `app.relaunch` resolves `{accepted:true}` when the request is accepted, before host cleanup and restart.

Worker replacement/closure releases resources. `event("system",payload)` reports `type:"watch"` with `token`, relative `path`, `oldPath`, `kind:"create"|"write"|"remove"|"rename"`, `isDir`; battery opt-in enables `power.suspend`, `power.resume`, `power.lock`, `power.unlock` types. Staging Workers may query but cannot create/release resources or perform lifecycle, launch, or clipboard-clear actions before activation. Remote WebView, Protocol, Terminal and dynamic libraries are not exposed.

## Panel control

`$metrix.panel.open()`, `close()`, `toggle()`, and `state()` return `Promise<{open:boolean}>` without opt-in. They only target the dashboard bound to the current Worker. Opening and toggling use the effective Flyout or Dialog setting, even without a menu bar entrance; the `{open}` response is unchanged. Dialog rejects opening when native screen positioning is unavailable, instead of falling back to an ordinary window. Staging Workers reject open/close/toggle; state is allowed and never creates a panel. Cancellation prevents actions that have not yet executed. `$metrix.togglePanel()` remains available.

## Audio players

Declare `apis="audio"` (or `all`). `$metrix.audio.load({url?,path?,stream?})` requires exactly one URL or path and resolves an AudioPlayer. `stream` is boolean, default true. URLs use network permission checks and mandatory request identity, including authorized LAN HTTP; paths require read permission. The host downloads the full compressed source before streaming decode.

Every player method returns a Promise: `play()`, `pause()`, `stop()`, `seek(seconds)`, `configure({volume?,pan?,playbackRate?,loop?,loopStart?,loopEnd?})`, `status()`, `close()`. Volume is 0–1 (default 1), pan -1–1 (default 0), playbackRate 0.1–16 (default 1); loop defaults false, loopStart/loopEnd to 0. Times are finite seconds 0–1800; loopEnd is 0 for end-of-source or greater than loopStart. Configuration is validated before applying. `status()` returns `playing`, `position`, `duration` (null if unknown) and configuration. close is idempotent. Staging permits load/query but rejects play before activation. Playback status describes logical playback, without guaranteeing audible output.

WAV, MP3 and Ogg Vorbis are supported: 8000–192000 Hz, one or two channels, at most 30 minutes including unknown-length streams. Limits per Worker: eight players including in-flight loads, 8 MiB compressed per source, 16 MiB compressed total (each in-flight load reserves 8 MiB). `stream:false` effects allow 4 MiB decoded samples each and a 16 MiB decoded pool; streaming rings use at most 1,536,000 bytes by metadata, excluding codec allocations. Stream slots/budgets return only after decoder closure. Worker closure cancels loads and prevents late publication; these budgets do not provide hard decoder memory isolation.

### PCM Sound and independent Voices

With `apis="audio"` (or `all`), `audio.now()` synchronously returns this Worker's monotonic audio-clock seconds, independent of Date. `audio.createSound({samples:Float32Array,sampleRate,channels})` returns `Promise<AudioSound>`. sampleRate is an integer 8000–192000, channels 1 or 2, with nonempty channel-aligned finite samples in [-1,1]. Only genuine Float32Array views, including offset views, are accepted; empty/detached views, Float64Array, ordinary arrays, proxies and forged brands are rejected. The host snapshots the visible samples **after all option getters finish**; later mutation does not alter Sound.

Sound shares PCM; each `sound.play({when?,volume?,pan?,playbackRate?,loop?,attackMs?})` creates an independent Voice. Defaults/ranges: volume 1 in 0–1, pan 0 in -1–1, playbackRate 1 in 0.1–16, loop false, attackMs 0 in 0–2000. Staging rejects play until activation. when is an absolute time on audio.now()'s clock: omitted/past means immediate, at most five seconds ahead. `voice.release({when?,durationMs?})` fades linearly from current envelope gain to zero and ends the Voice; durationMs defaults 80 in 0–2000. Releasing a not-yet-started Voice cancels it at the release time. Repeated release replaces the reservation; volume multiplies the envelope.

Audio scheduling is control-level and is not sample-accurate. Ended Voices cannot be revived by release.

`voice.configure({volume?,pan?,playbackRate?,loop?})`, `status()`, `release()`, `stop()` and `close()` return Promises. status includes `state:scheduled|playing|releasing|ended|closed`, playing, position, duration and the four configuration fields. Natural/fade completion gives ended; stop/close idempotently gives closed and resolves after backend closure. Ended/closed status remains readable, while configure/release reject. Logical status does not guarantee audible output.

Per Worker: 64 Sounds including reservations, 4 MiB per Sound, 32 independent Voices including reservations/scheduled/releasing, 32 MiB shared PCM plus legacy effect decode reservations, and 16 MiB pending host PCM copies. The pending budget does not bound JS backing buffers. Legacy limits remain eight players, 16 MiB compressed total and a separate 16 MiB effect decode pool. Ordinary command queues hold at most 256 entries; overflow rejects and rolls back undelivered reservations.

`sound.close()` shares one Promise, immediately prevents new play, and lets existing Voices finish; after the last terminates it drops PCM and returns budgets. Retaining closed JS Sound/Voice objects does not retain PCM/backend players. Natural Voice termination releases its resources. Worker shutdown freezes reservations/commands, cancels pending commands and waits for Go cleanup independently of the JS event loop.

This complete `<script apis="audio">` module creates one shared tone lazily and plays a separate Voice per click; it performs no audio playback during initial render or Check.

```js
let sound;
export default {
  render() {
    return {
      panel: {
        type: "Button",
        props: {
          text: "Play tone",
          onClick: async () => {
            if (!sound) {
              const samples = new Float32Array(4800);
              for (let i = 0; i < samples.length; i++) {
                samples[i] = 0.2 * Math.sin((2 * Math.PI * 440 * i) / 48000);
              }
              sound = await $metrix.audio.createSound({ samples, sampleRate: 48000, channels: 1 });
            }
            const when = $metrix.audio.now() + 0.05;
            const voice = await sound.play({ when, attackMs: 10 });
            await voice.release({ when: when + 0.08, durationMs: 20 });
          },
        },
      },
    };
  },
  async event(name, payload) {
    if (name === "lifecycle" && payload.type === "stop" && sound) await sound.close();
  },
};
```

## Typed clipboard and languages

Declare `apis="clipboard"`. `navigator.clipboard.read()` resolves `ClipboardItem[]`; `write(items)` returns a Promise. `new ClipboardItem({[mime]: BlobOrPromise})` and `ClipboardItem.supports(type)` accept exactly `text/plain`, `text/html`, `image/png`, `text/markdown`, `text/uri-list`, `application/x-mygo-file-list`; custom `web ` MIME types are rejected. Item `types` retain construction order and are frozen. Writes await and validate every representation before one host commit: at most 64 items and 8 MiB total, PNG at most 16,777,216 pixels. Empty bytes are valid; native errors reject. File URLs must have empty/localhost host, absolute local paths, and per-path read authorization on both read and write. macOS preserves multiple items; Windows/Linux may merge text and choose the first matching item for other formats. This is a Worker extension, not the full browser standard; limits do not bound native transient allocations.

`navigator.languages` is a frozen preferred-language snapshot; `navigator.language === navigator.languages[0]`. Tags are normalized and deduplicated BCP 47 (scripts preserved), defaulting to en-US. The app captures official preferred languages, locale and region at readiness; each Worker keeps its creation-time snapshot. No `languagechange` is dispatched. Headless fallback uses LANGUAGE and locale environment settings. `app.locale` and `app.region` remain distinct snapshots; Intl uses the existing polyfill.

### Web Crypto and Badging Subsets

With `apis="crypto"`, `crypto.getRandomValues(view)` accepts only integer TypedArrays (including Uint8ClampedArray and supported BigInt arrays), returns the same view, and fills only its visible bytes. Floating-point arrays, DataView, ArrayBuffer, and forged objects throw a `TypeMismatchError` DOMException. Calls above 65536 bytes throw a `QuotaExceededError` DOMException. Digest and HMAC remain the supported cryptographic subset.

With `apis="badge"`, `navigator.setAppBadge(count?)` and `navigator.clearAppBadge()` return `Promise<void>`; validation failures reject the Promise. MyGO supports counts only: zero or no argument clears the badge, and concurrent dashboards use the last written count.
