# Dashboard Mutation API

Programmatic API for modifying dashboards. Each command is executed via:

```typescript
api.execute({ type: 'COMMAND_NAME', payload: { ... } })
```

All responses share this shape:

```json
{
  "success": true,
  "data": {},
  "changes": [{ "path": "...", "previousValue": "...", "newValue": "..." }],
  "warnings": ["optional array of warning strings"]
}
```

On failure, `success` is `false` and `error` contains a message. `changes` is always `[]` on failure.

## Which commands exist depends on the open document

The API is mounted on whichever document is rendering, and each document type registers its own
commands. A dashboard exposes the dashboard commands; a notebook exposes the notebook commands. Asking
for one that is not registered fails with `Unknown command type: X. Available commands: ...`, so
`getAvailableCommands()` (or that error) is how a caller finds out where it is.

This is why no command has to be described as "dashboards only": the dashboard commands are simply not
reachable from a notebook, and a notebook spec is a different schema that would silently lose every
narrative cell if a dashboard serializer answered for it.

`CREATE_NOTEBOOK_SPEC` is the one exception, registered on both, because there is no blank notebook to
open before creating one.

---

## Layout

### `GET_LAYOUT`

Read the current layout tree and elements map. Call this first to discover paths and current state.

**Request:**

```json
{ "type": "GET_LAYOUT", "payload": {} }
```

**Response:**

```json
{
  "success": true,
  "data": {
    "layout": {
      "kind": "RowsLayout",
      "spec": {
        "rows": [
          {
            "kind": "RowsLayoutRow",
            "spec": { "title": "Monitoring", "collapse": false, "hideHeader": false, "fillScreen": false },
            "layout": { "kind": "GridLayout" },
            "path": "/rows/0"
          }
        ]
      }
    },
    "elements": {
      "panel-1": { "kind": "Panel", "spec": { "title": "Request rate", "...": "..." } }
    }
  },
  "changes": []
}
```

### `UPDATE_LAYOUT`

Switch layout type and/or update layout properties at a path. Omit `layoutType` to keep the current type and only apply options.

**Switch rows to tabs:**

```json
{
  "type": "UPDATE_LAYOUT",
  "payload": { "path": "/", "layoutType": "TabsLayout" }
}
```

**Switch to AutoGridLayout with options:**

```json
{
  "type": "UPDATE_LAYOUT",
  "payload": {
    "path": "/",
    "layoutType": "AutoGridLayout",
    "options": { "maxColumnCount": 4, "columnWidthMode": "wide", "fillScreen": true }
  }
}
```

**Update AutoGrid properties without switching type:**

```json
{
  "type": "UPDATE_LAYOUT",
  "payload": {
    "path": "/",
    "options": { "columnWidthMode": "custom", "columnWidth": 500, "rowHeightMode": "standard" }
  }
}
```

**Response:**

```json
{
  "success": true,
  "data": { "path": "/", "layoutType": "AutoGridLayout" },
  "changes": [{ "path": "/", "previousValue": "GridLayout", "newValue": "AutoGridLayout" }]
}
```

Allowed conversions are same-category only: `RowsLayout` <-> `TabsLayout` (group) or `GridLayout` <-> `AutoGridLayout` (grid). Providing `options` for a non-AutoGrid layout type returns an error.

---

## Rows

### `ADD_ROW`

Add a row to the layout. If the target is not a RowsLayout, the existing content is nested inside the new row.

**Add a row at the root:**

```json
{
  "type": "ADD_ROW",
  "payload": {
    "row": { "spec": { "title": "Monitoring" } },
    "parentPath": "/"
  }
}
```

**Add a repeated row inside a tab:**

```json
{
  "type": "ADD_ROW",
  "payload": {
    "row": { "spec": { "title": "Region stats", "repeat": { "mode": "variable", "value": "region" } } },
    "parentPath": "/tabs/0",
    "position": 0
  }
}
```

Row `spec` may include optional **`variables`**: an array of v2 `VariableKind` objects for section-scoped template variables on that row. Behavior matches dashboard deserialization.

**Response:**

```json
{
  "success": true,
  "data": {
    "path": "/rows/1",
    "row": { "kind": "RowsLayoutRow", "spec": { "title": "Monitoring" } }
  },
  "changes": [{ "path": "/rows/1", "previousValue": null, "newValue": { "title": "Monitoring" } }]
}
```

### `UPDATE_ROW`

Update a row's properties. Only provided fields are changed. Optional **`spec.variables`** sets or clears section variables for the row (`[]` clears when the toggle is on; omit to leave unchanged).

**Request:**

```json
{
  "type": "UPDATE_ROW",
  "payload": {
    "path": "/rows/0",
    "spec": { "title": "Renamed Row", "collapse": true }
  }
}
```

**Set repeat on an existing row:**

```json
{
  "type": "UPDATE_ROW",
  "payload": {
    "path": "/rows/1",
    "spec": { "repeat": { "mode": "variable", "value": "cluster" } }
  }
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "path": "/rows/0",
    "row": {
      "kind": "RowsLayoutRow",
      "spec": { "title": "Renamed Row", "collapse": true, "hideHeader": false, "fillScreen": false }
    }
  },
  "changes": [
    {
      "path": "/rows/0",
      "previousValue": { "title": "Old Title", "collapse": false, "hideHeader": false, "fillScreen": false },
      "newValue": { "title": "Renamed Row", "collapse": true, "hideHeader": false, "fillScreen": false }
    }
  ]
}
```

### `REMOVE_ROW`

Remove a row. Use `moveContentTo` to relocate panels instead of deleting them.

**Remove and relocate panels:**

```json
{
  "type": "REMOVE_ROW",
  "payload": { "path": "/rows/0", "moveContentTo": "/rows/1" }
}
```

**Remove and delete panels:**

```json
{
  "type": "REMOVE_ROW",
  "payload": { "path": "/rows/2" }
}
```

**Response:**

```json
{
  "success": true,
  "data": { "path": "/rows/0" },
  "changes": [{ "path": "/rows/0", "previousValue": { "title": "Monitoring" }, "newValue": null }]
}
```

### `MOVE_ROW`

Reorder a row or move it to a different parent.

**Reorder within the same parent:**

```json
{
  "type": "MOVE_ROW",
  "payload": { "path": "/rows/0", "toPosition": 2 }
}
```

**Move a row from one tab to another:**

```json
{
  "type": "MOVE_ROW",
  "payload": { "path": "/tabs/0/rows/0", "toParent": "/tabs/1", "toPosition": 0 }
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "path": "/rows/2",
    "row": {
      "kind": "RowsLayoutRow",
      "spec": { "title": "Monitoring", "collapse": false, "hideHeader": false, "fillScreen": false }
    }
  },
  "changes": [{ "path": "/rows/0", "previousValue": "/rows/0", "newValue": "/rows/2" }]
}
```

---

## Tabs

### `ADD_TAB`

Add a tab to the layout. If the target is not a TabsLayout, the existing content is nested inside the new tab.

**Request:**

```json
{
  "type": "ADD_TAB",
  "payload": {
    "tab": { "spec": { "title": "Overview" } },
    "parentPath": "/"
  }
}
```

**Add a repeated tab:**

```json
{
  "type": "ADD_TAB",
  "payload": {
    "tab": { "spec": { "title": "Environment", "repeat": { "mode": "variable", "value": "env" } } },
    "parentPath": "/"
  }
}
```

Tab `spec` may include optional **`variables`** for section-scoped template variables on that tab (same rules as row `variables`).

**Response:**

```json
{
  "success": true,
  "data": {
    "path": "/tabs/1",
    "tab": { "kind": "TabsLayoutTab", "spec": { "title": "Overview" } }
  },
  "changes": [{ "path": "/tabs/1", "previousValue": null, "newValue": { "title": "Overview" } }]
}
```

### `UPDATE_TAB`

Update a tab's properties. Only provided fields are changed. Optional **`spec.variables`** sets or clears section variables on the tab (`[]` clears when the toggle is on; omit to leave unchanged).

**Request:**

```json
{
  "type": "UPDATE_TAB",
  "payload": {
    "path": "/tabs/0",
    "spec": { "title": "Renamed Tab" }
  }
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "path": "/tabs/0",
    "tab": { "kind": "TabsLayoutTab", "spec": { "title": "Renamed Tab" } }
  },
  "changes": [
    {
      "path": "/tabs/0",
      "previousValue": { "title": "Old Title" },
      "newValue": { "title": "Renamed Tab" }
    }
  ]
}
```

### `REMOVE_TAB`

Remove a tab. Use `moveContentTo` to relocate panels instead of deleting them.

**Request:**

```json
{
  "type": "REMOVE_TAB",
  "payload": { "path": "/tabs/0", "moveContentTo": "/tabs/1" }
}
```

**Response:**

```json
{
  "success": true,
  "data": { "path": "/tabs/0" },
  "changes": [{ "path": "/tabs/0", "previousValue": { "title": "Overview" }, "newValue": null }]
}
```

### `MOVE_TAB`

Reorder a tab or move it to a different parent.

**Request:**

```json
{
  "type": "MOVE_TAB",
  "payload": { "path": "/tabs/0", "toPosition": 2 }
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "path": "/tabs/2",
    "tab": { "kind": "TabsLayoutTab", "spec": { "title": "Overview" } }
  },
  "changes": [{ "path": "/tabs/0", "previousValue": "/tabs/0", "newValue": "/tabs/2" }]
}
```

---

## Panels

### `ADD_PANEL`

Create a new panel and add it to the dashboard. The `id` is auto-assigned. The `layoutItem.kind` is optional -- it is auto-detected from the target layout. If provided and mismatched, a warning is emitted.

**Request:**

```json
{
  "type": "ADD_PANEL",
  "payload": {
    "panel": {
      "kind": "Panel",
      "spec": {
        "title": "Request rate",
        "vizConfig": {
          "kind": "VizConfig",
          "group": "timeseries",
          "spec": { "options": {}, "fieldConfig": { "defaults": {}, "overrides": [] } }
        },
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "refId": "A",
                  "query": {
                    "kind": "DataQuery",
                    "group": "prometheus",
                    "spec": { "expr": "rate(http_requests_total[5m])" }
                  }
                }
              }
            ]
          }
        }
      }
    },
    "parentPath": "/rows/0",
    "layoutItem": { "spec": { "x": 0, "y": 0, "width": 12, "height": 8 } }
  }
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "element": { "kind": "Panel", "spec": { "title": "Request rate", "...": "..." } },
    "layoutItem": {
      "kind": "GridLayoutItem",
      "spec": { "x": 0, "y": 0, "width": 12, "height": 8, "element": { "kind": "ElementReference", "name": "panel-1" } }
    }
  },
  "changes": [{ "path": "/elements/panel-1", "previousValue": null, "newValue": "..." }]
}
```

All panel write commands (ADD, UPDATE, MOVE) return `{ element, layoutItem }`. The element name is in `layoutItem.spec.element.name`. LIST_PANELS returns the same shape as an array. The `layoutItem` always includes the resolved `kind`, even if the request omitted it.

### `UPDATE_PANEL`

Partial update of an existing panel. Only provided fields are applied. Options and fieldConfig are deep-merged. Plugin type changes use proper fieldConfig cleanup.

**Change title only:**

```json
{
  "type": "UPDATE_PANEL",
  "payload": {
    "element": { "name": "panel-abc" },
    "panel": { "spec": { "title": "New title" } }
  }
}
```

**Deep-merge visualization options:**

```json
{
  "type": "UPDATE_PANEL",
  "payload": {
    "element": { "name": "panel-abc" },
    "panel": {
      "spec": {
        "vizConfig": {
          "spec": { "options": { "legend": { "displayMode": "table" } } }
        }
      }
    }
  }
}
```

**Change plugin type (e.g. timeseries to stat):**

```json
{
  "type": "UPDATE_PANEL",
  "payload": {
    "element": { "name": "panel-abc" },
    "panel": {
      "spec": {
        "vizConfig": { "group": "stat", "spec": { "options": { "graphMode": "none" } } }
      }
    }
  }
}
```

**Response (all UPDATE_PANEL variants):**

```json
{
  "success": true,
  "data": {
    "element": {
      "kind": "Panel",
      "spec": {
        "title": "New title",
        "vizConfig": { "kind": "VizConfig", "group": "timeseries", "spec": { "...": "..." } },
        "data": { "kind": "QueryGroup", "spec": { "queries": ["..."] } }
      }
    },
    "layoutItem": {
      "kind": "GridLayoutItem",
      "spec": {
        "x": 0,
        "y": 0,
        "width": 12,
        "height": 8,
        "element": { "kind": "ElementReference", "name": "panel-abc" }
      }
    }
  },
  "changes": [
    {
      "path": "/elements/panel-abc",
      "previousValue": { "kind": "Panel", "spec": { "...": "previous state" } },
      "newValue": "..."
    }
  ]
}
```

Same `{ element, layoutItem }` shape as ADD_PANEL and MOVE_PANEL. The `transparent` field in the spec maps to the internal `displayMode` state (`true` -> `"transparent"`, `false` -> `"default"`).

### `REMOVE_PANEL`

Remove one or more panels by element name.

**Request:**

```json
{
  "type": "REMOVE_PANEL",
  "payload": {
    "elements": [{ "name": "panel-abc" }, { "name": "panel-def" }]
  }
}
```

**Response:**

```json
{
  "success": true,
  "data": { "removed": ["panel-abc", "panel-def"] },
  "changes": [
    { "path": "/elements/panel-abc", "previousValue": { "kind": "Panel", "spec": { "...": "..." } }, "newValue": null },
    { "path": "/elements/panel-def", "previousValue": { "kind": "Panel", "spec": { "...": "..." } }, "newValue": null }
  ]
}
```

If some elements fail while others succeed, `success` is `true` and partial failures are reported in `warnings`.

### `LIST_PANELS`

List elements on the dashboard (panels, library panels, etc.) as an array of `{ element, layoutItem }` entries. Same shape as write command responses, with the element name embedded in `layoutItem.spec.element.name`.

**Request (all panels):**

```json
{ "type": "LIST_PANELS", "payload": {} }
```

**Request (filtered by element names):**

```json
{ "type": "LIST_PANELS", "payload": { "elements": ["panel-1", "panel-5"] } }
```

**Request (with runtime status and data schema):**

`includeStatus` and `includeSchema` are independent; request either or both.

```json
{ "type": "LIST_PANELS", "payload": { "includeStatus": true, "includeSchema": true } }
```

**Response:**

```json
{
  "success": true,
  "data": {
    "elements": [
      {
        "element": {
          "kind": "Panel",
          "spec": {
            "title": "Request rate",
            "vizConfig": { "kind": "VizConfig", "group": "timeseries", "spec": { "...": "..." } },
            "data": { "kind": "QueryGroup", "spec": { "queries": ["..."] } }
          }
        },
        "layoutItem": {
          "kind": "GridLayoutItem",
          "spec": {
            "x": 0,
            "y": 0,
            "width": 12,
            "height": 8,
            "element": { "kind": "ElementReference", "name": "panel-1" }
          }
        },
        "status": {
          "loadingState": "Error",
          "hasError": true,
          "hasNoData": false,
          "errors": [
            { "source": "query", "message": "parse error: unexpected } in query", "refId": "A", "type": "unknown" }
          ],
          "notices": [{ "severity": "warning", "text": "Query returned partial data" }]
        },
        "dataSchema": [
          {
            "name": "response_time",
            "fields": [
              { "name": "Time", "type": "time" },
              { "name": "Value", "type": "number" }
            ]
          }
        ]
      },
      {
        "element": { "kind": "Panel", "spec": { "title": "Error count", "...": "..." } },
        "layoutItem": {
          "kind": "AutoGridLayoutItem",
          "spec": { "element": { "kind": "ElementReference", "name": "panel-2" } }
        }
      }
    ]
  },
  "changes": []
}
```

`status` is present only when `includeStatus` is `true`, and `dataSchema` only when `includeSchema` is `true` (and the panel has a data provider). Both are a runtime side-channel: never part of the saved v2 dashboard spec (`element`), only the read result.

`status` reports the panel's live query health:

- `loadingState` — the raw scene loading state (`NotStarted`, `Loading`, `Streaming`, `Done`, `Error`). Whether a panel is loading is derivable from this, so no separate `isLoading` is returned.
- `hasError` / `hasNoData` — reported explicitly because `loadingState` does not imply them: a `Done` panel can still carry errors (a query error or an error-severity notice) or return no data.
- `errors` — every panel error in one structured array. `source` (`query` / `plugin` / `notice`) says where the error came from; `message` plus `refId`/`type` (query errors only) are a curated subset of `@grafana/data`'s `DataQueryError`. Consolidates all channels: query/datasource errors, error-severity data-frame notices, and plugin failures (unknown/missing viz type, library-panel load failure, or a module that fails to compile).
- `notices` — non-error (`info` / `warning`) data-frame notices, deduped across frames. Error-severity notices are folded into `errors` instead.

`dataSchema` contains the fields each result frame produced (`name`, `type`, `labels`) — metadata, not values. Use it to get the real field (column) names before referencing a field by name in a transformation (`organize`, `calculateField`, `filterFieldsByName`, `sortBy`) or a `byName` field override, so you target names that actually exist.

````

Each entry uses the same `{ element, layoutItem }` shape as write commands. The element name is in `layoutItem.spec.element.name`.

### `MOVE_PANEL`

Move a panel to a different group or reposition it within a grid. The `layoutItem.kind` is optional -- it is auto-detected from the target layout. If provided and mismatched, a warning is emitted.

**Move to another row:**

```json
{
  "type": "MOVE_PANEL",
  "payload": { "element": { "name": "panel-abc" }, "toParent": "/rows/1" }
}
````

**Reposition within the current grid using layoutItem:**

```json
{
  "type": "MOVE_PANEL",
  "payload": {
    "element": { "name": "panel-abc" },
    "layoutItem": { "spec": { "x": 0, "y": 0, "width": 12, "height": 8 } }
  }
}
```

**Move to an AutoGridLayout parent (position is auto-arranged):**

```json
{
  "type": "MOVE_PANEL",
  "payload": {
    "element": { "name": "panel-abc" },
    "toParent": "/tabs/0"
  }
}
```

**Response (all MOVE_PANEL variants):**

```json
{
  "success": true,
  "data": {
    "element": { "kind": "Panel", "spec": { "title": "Request rate", "...": "..." } },
    "layoutItem": {
      "kind": "GridLayoutItem",
      "spec": {
        "x": 0,
        "y": 0,
        "width": 12,
        "height": 8,
        "element": { "kind": "ElementReference", "name": "panel-abc" }
      }
    }
  },
  "changes": [
    {
      "path": "/elements/panel-abc",
      "previousValue": { "kind": "GridLayoutItem", "spec": { "x": 0, "y": 0, "width": 12, "height": 8 } },
      "newValue": { "parent": "/rows/1" }
    }
  ]
}
```

Same `{ element, layoutItem }` shape as ADD_PANEL and UPDATE_PANEL. When moving to an AutoGridLayout target, `layoutItem` returns `{ "kind": "AutoGridLayoutItem", "spec": { "element": { ... } } }`.

> **Deprecated:** The `position` field (`{ x, y, width, height }`) is deprecated. Use `layoutItem: { spec: { ... } }` instead. If both are provided, `layoutItem` takes precedence.

---

## Variables

Variable commands accept optional **`parentPath`** (layout path from `GET_LAYOUT`). Default **`"/"`** targets **dashboard-level** variables. Paths ending at a **row** or **tab** (for example `"/rows/0"` or `"/tabs/1/rows/0"`) target that section’s variable set. **`UPDATE_VARIABLE`** and **`REMOVE_VARIABLE`** require an explicit **`parentPath`** when the name does not exist on the dashboard but exists on a section.

### `ADD_VARIABLE`

**Request:**

```json
{
  "type": "ADD_VARIABLE",
  "payload": {
    "variable": {
      "kind": "CustomVariable",
      "spec": { "name": "env", "query": "dev,staging,prod", "multi": true }
    }
  }
}
```

**Add a section variable on the first row:**

```json
{
  "type": "ADD_VARIABLE",
  "payload": {
    "parentPath": "/rows/0",
    "variable": {
      "kind": "CustomVariable",
      "spec": { "name": "region", "query": "eu,us", "multi": false }
    }
  }
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "variable": { "kind": "CustomVariable", "spec": { "name": "env", "query": "dev,staging,prod", "multi": true } }
  },
  "changes": [
    {
      "path": "/variables/env",
      "previousValue": null,
      "newValue": { "kind": "CustomVariable", "spec": { "...": "..." } }
    }
  ]
}
```

For section scope, `changes[0].path` is prefixed (for example `"/rows/0/variables/region"`).

### `UPDATE_VARIABLE`

**Request:**

```json
{
  "type": "UPDATE_VARIABLE",
  "payload": {
    "name": "env",
    "variable": {
      "kind": "CustomVariable",
      "spec": { "name": "env", "query": "dev,staging,prod,canary", "multi": true }
    }
  }
}
```

**Update a variable on the first row:**

```json
{
  "type": "UPDATE_VARIABLE",
  "payload": {
    "parentPath": "/rows/0",
    "name": "region",
    "variable": {
      "kind": "CustomVariable",
      "spec": { "name": "region", "query": "eu,us,apac", "multi": false }
    }
  }
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "variable": {
      "kind": "CustomVariable",
      "spec": { "name": "env", "query": "dev,staging,prod,canary", "multi": true }
    }
  },
  "changes": [
    {
      "path": "/variables/env",
      "previousValue": "...",
      "newValue": { "kind": "CustomVariable", "spec": { "...": "..." } }
    }
  ]
}
```

### `REMOVE_VARIABLE`

**Request:**

```json
{
  "type": "REMOVE_VARIABLE",
  "payload": { "name": "env" }
}
```

**Remove a section variable:**

```json
{
  "type": "REMOVE_VARIABLE",
  "payload": { "parentPath": "/rows/0", "name": "region" }
}
```

**Response:**

```json
{
  "success": true,
  "data": { "name": "env" },
  "changes": [{ "path": "/variables/env", "previousValue": "...", "newValue": null }]
}
```

### `LIST_VARIABLES`

**Request:**

```json
{ "type": "LIST_VARIABLES", "payload": {} }
```

**List variables for a row section:**

```json
{ "type": "LIST_VARIABLES", "payload": { "parentPath": "/rows/0" } }
```

**Response:**

```json
{
  "success": true,
  "data": {
    "variables": [
      {
        "kind": "CustomVariable",
        "spec": { "name": "env", "query": "dev,staging,prod", "multi": true, "hide": "dontHide" }
      },
      {
        "kind": "QueryVariable",
        "spec": {
          "name": "instance",
          "query": { "kind": "DataQuery", "group": "prometheus", "spec": { "expr": "label_values(up, instance)" } },
          "refresh": "onDashboardLoad"
        }
      }
    ]
  },
  "changes": []
}
```

---

## Settings

### `UPDATE_DASHBOARD_SETTINGS`

Update dashboard-level settings. Requires edit permissions. All fields are optional; only the fields provided are changed. `tags` and `links` replace the full list. Time-related fields are nested under `timeSettings` (matching the v2 dashboard spec).

**Request:**

```json
{
  "type": "UPDATE_DASHBOARD_SETTINGS",
  "payload": {
    "title": "Production Overview",
    "description": "Key production service metrics",
    "tags": ["production", "sre"],
    "editable": true,
    "cursorSync": "Crosshair",
    "links": [{ "title": "Runbook", "url": "https://runbooks.example.com", "type": "link", "targetBlank": true }],
    "timeSettings": { "from": "now-7d", "to": "now", "autoRefresh": "1m", "timezone": "utc" },
    "liveNow": false,
    "preload": true
  }
}
```

`cursorSync` accepts `"Off"`, `"Crosshair"`, or `"Tooltip"`. `cursorSync` and `liveNow` are behavior-based; when the dashboard has no `CursorSync` / `LiveNowTimer` behavior a warning is returned. The response `data` contains the updated settings (same shape as `GET_DASHBOARD_INFO`).

---

## Metadata

### `GET_DASHBOARD_INFO`

Get dashboard identity/folder metadata plus every dashboard-level setting that `UPDATE_DASHBOARD_SETTINGS` can write. Read-only, no permissions required.

**Request:**

```json
{ "type": "GET_DASHBOARD_INFO", "payload": {} }
```

**Response:**

```json
{
  "success": true,
  "data": {
    "title": "My Dashboard",
    "description": "Dashboard description",
    "tags": ["production", "monitoring"],
    "editable": true,
    "cursorSync": "Crosshair",
    "links": [{ "title": "Runbook", "url": "https://runbooks.example.com", "type": "link" }],
    "timeSettings": { "from": "now-6h", "to": "now", "autoRefresh": "30s", "timezone": "utc" },
    "liveNow": false,
    "preload": true,
    "uid": "abc123",
    "folderTitle": "Infrastructure",
    "folderUid": "folder-1",
    "created": "2025-01-15T10:00:00Z",
    "updated": "2025-03-13T14:30:00Z"
  },
  "changes": []
}
```

---

## Utility

### `ENTER_EDIT_MODE`

Enter dashboard edit mode. Write commands call this automatically; this is rarely needed directly.

**Request:**

```json
{ "type": "ENTER_EDIT_MODE", "payload": {} }
```

**Response:**

```json
{
  "success": true,
  "data": { "wasAlreadyEditing": false, "isEditing": true },
  "changes": [{ "path": "/isEditing", "previousValue": false, "newValue": true }]
}
```

If already in edit mode, `wasAlreadyEditing` is `true` and `changes` is `[]`.

---

## Notebooks

Available on a notebook page only (except `CREATE_NOTEBOOK_SPEC`), behind the `dashboard.notebooks`
feature flag. A notebook is a flat, ordered list of cells — markdown, code, panel, library panel —
described by a `NotebookSpec`. Its panel elements use the same shape as a dashboard v2 spec; what it
adds is `Cell` (narrative content) and `NotebookLayout`.

These are whole-spec commands: there are no granular notebook commands, so an edit is read, change the
JSON, write back.

### `GET_NOTEBOOK_SPEC`

Return the whole notebook.

```json
{ "type": "GET_NOTEBOOK_SPEC", "payload": { "validate": false } }
```

`validate` checks the serialized spec against the notebook schema and fails the read if it is invalid.
Worth requesting: a read that comes back with dangling cell references means the scene lost elements on
the way out.

**Response:** `{ "success": true, "data": { "spec": { ... } } }`

### `APPLY_NOTEBOOK_SPEC`

Replace the notebook from a whole spec. **In memory** — nothing is saved.

```json
{ "type": "APPLY_NOTEBOOK_SPEC", "payload": { "spec": { ... }, "validate": false } }
```

**Response:** `{ "success": true, "data": { "applied": true, "spec": { ... } }, "warnings": [...] }`

`data.spec` is the notebook re-serialized after the write, so there is no need for a follow-up read. It
is absent if that re-serialization failed — the write still landed, and `warnings` says the surviving
cells could not be checked.

`warnings` names any cell that was requested and is not in the result. A layout entry pointing at an
element that is not in `elements` is skipped rather than rejected, so without this a write could lose a
cell and still report success. Pass `validate: true` to have that rejected up front instead.

The payload is strict: an unknown key is rejected rather than ignored, so a mistyped `validate` cannot
silently apply the spec unchecked.

### `CREATE_NOTEBOOK_SPEC`

Create a **new** notebook and open it. Registered on dashboards too, because there is no blank notebook
to apply a spec into the way `/dashboard/new` is one for a dashboard.

```json
{ "type": "CREATE_NOTEBOOK_SPEC", "payload": { "spec": { ... }, "validate": true, "open": true } }
```

**Response:**
`{ "success": true, "data": { "created": true, "opened": true, "uid": "n-abc123", "url": "/notebooks/n-abc123" } }`

Unlike every other command here this one **persists immediately**, and the server assigns the uid — so
`validate` defaults to `true`. Set `open: false` to create without navigating. Use
`APPLY_NOTEBOOK_SPEC` to change a notebook that already exists.

`opened` is whether the navigation was accepted, which is not the same as `open`: a dirty dashboard's
unsaved-changes prompt blocks it. The notebook exists either way, but `GET_NOTEBOOK_SPEC` and
`APPLY_NOTEBOOK_SPEC` only reach it once its page is mounted.

`opened: false` is conclusive. `opened: true` is not the same as "the notebook client is mounted": it
reads the history entry, and the route behind it is a lazy chunk plus a fetch, so a command sent
immediately afterwards can still land on the previous document. `getAvailableCommands()` is the
authoritative answer to which document is mounted.

The command is registered on a dashboard only when the `dashboard.notebooks` flag is on, so
`getAvailableCommands()` names it exactly where it can run.

---

## Paths

Every layout node has a path string returned by `GET_LAYOUT`:

| Path             | Meaning                         |
| ---------------- | ------------------------------- |
| `/`              | Root layout                     |
| `/rows/0`        | First row                       |
| `/rows/1`        | Second row                      |
| `/tabs/0`        | First tab                       |
| `/tabs/1/rows/0` | First row inside the second tab |

Paths are positional and shift after add/remove operations. Re-read the layout between complex restructuring steps to get updated paths.

## Layout types

| Type             | Description                                                                      |
| ---------------- | -------------------------------------------------------------------------------- |
| `RowsLayout`     | Panels organized into collapsible rows.                                          |
| `TabsLayout`     | Panels organized into tabs.                                                      |
| `GridLayout`     | Flat grid with explicit x/y/width/height positioning.                            |
| `AutoGridLayout` | Auto-arranged grid with configurable column width, row height, and column count. |

## Nesting rules

- Maximum four layers of group nesting.
- Tabs cannot be nested directly inside tabs. Deeper nesting (e.g. tabs > rows > tabs) is allowed.
- Rows can be nested inside both rows and tabs.

For example, tabs containing rows is valid, and rows containing rows is valid. Tabs directly containing tabs is rejected.
