public/app/features/dashboard-scene/mutation-api/README.md
Programmatic API for modifying dashboards. Each command is executed via:
api.execute({ type: 'COMMAND_NAME', payload: { ... } })
All responses share this shape:
{
"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.
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.
GET_LAYOUTRead the current layout tree and elements map. Call this first to discover paths and current state.
Request:
{ "type": "GET_LAYOUT", "payload": {} }
Response:
{
"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_LAYOUTSwitch 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:
{
"type": "UPDATE_LAYOUT",
"payload": { "path": "/", "layoutType": "TabsLayout" }
}
Switch to AutoGridLayout with options:
{
"type": "UPDATE_LAYOUT",
"payload": {
"path": "/",
"layoutType": "AutoGridLayout",
"options": { "maxColumnCount": 4, "columnWidthMode": "wide", "fillScreen": true }
}
}
Update AutoGrid properties without switching type:
{
"type": "UPDATE_LAYOUT",
"payload": {
"path": "/",
"options": { "columnWidthMode": "custom", "columnWidth": 500, "rowHeightMode": "standard" }
}
}
Response:
{
"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.
ADD_ROWAdd 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:
{
"type": "ADD_ROW",
"payload": {
"row": { "spec": { "title": "Monitoring" } },
"parentPath": "/"
}
}
Add a repeated row inside a tab:
{
"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:
{
"success": true,
"data": {
"path": "/rows/1",
"row": { "kind": "RowsLayoutRow", "spec": { "title": "Monitoring" } }
},
"changes": [{ "path": "/rows/1", "previousValue": null, "newValue": { "title": "Monitoring" } }]
}
UPDATE_ROWUpdate 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:
{
"type": "UPDATE_ROW",
"payload": {
"path": "/rows/0",
"spec": { "title": "Renamed Row", "collapse": true }
}
}
Set repeat on an existing row:
{
"type": "UPDATE_ROW",
"payload": {
"path": "/rows/1",
"spec": { "repeat": { "mode": "variable", "value": "cluster" } }
}
}
Response:
{
"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_ROWRemove a row. Use moveContentTo to relocate panels instead of deleting them.
Remove and relocate panels:
{
"type": "REMOVE_ROW",
"payload": { "path": "/rows/0", "moveContentTo": "/rows/1" }
}
Remove and delete panels:
{
"type": "REMOVE_ROW",
"payload": { "path": "/rows/2" }
}
Response:
{
"success": true,
"data": { "path": "/rows/0" },
"changes": [{ "path": "/rows/0", "previousValue": { "title": "Monitoring" }, "newValue": null }]
}
MOVE_ROWReorder a row or move it to a different parent.
Reorder within the same parent:
{
"type": "MOVE_ROW",
"payload": { "path": "/rows/0", "toPosition": 2 }
}
Move a row from one tab to another:
{
"type": "MOVE_ROW",
"payload": { "path": "/tabs/0/rows/0", "toParent": "/tabs/1", "toPosition": 0 }
}
Response:
{
"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" }]
}
ADD_TABAdd a tab to the layout. If the target is not a TabsLayout, the existing content is nested inside the new tab.
Request:
{
"type": "ADD_TAB",
"payload": {
"tab": { "spec": { "title": "Overview" } },
"parentPath": "/"
}
}
Add a repeated tab:
{
"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:
{
"success": true,
"data": {
"path": "/tabs/1",
"tab": { "kind": "TabsLayoutTab", "spec": { "title": "Overview" } }
},
"changes": [{ "path": "/tabs/1", "previousValue": null, "newValue": { "title": "Overview" } }]
}
UPDATE_TABUpdate 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:
{
"type": "UPDATE_TAB",
"payload": {
"path": "/tabs/0",
"spec": { "title": "Renamed Tab" }
}
}
Response:
{
"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_TABRemove a tab. Use moveContentTo to relocate panels instead of deleting them.
Request:
{
"type": "REMOVE_TAB",
"payload": { "path": "/tabs/0", "moveContentTo": "/tabs/1" }
}
Response:
{
"success": true,
"data": { "path": "/tabs/0" },
"changes": [{ "path": "/tabs/0", "previousValue": { "title": "Overview" }, "newValue": null }]
}
MOVE_TABReorder a tab or move it to a different parent.
Request:
{
"type": "MOVE_TAB",
"payload": { "path": "/tabs/0", "toPosition": 2 }
}
Response:
{
"success": true,
"data": {
"path": "/tabs/2",
"tab": { "kind": "TabsLayoutTab", "spec": { "title": "Overview" } }
},
"changes": [{ "path": "/tabs/0", "previousValue": "/tabs/0", "newValue": "/tabs/2" }]
}
ADD_PANELCreate 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:
{
"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:
{
"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_PANELPartial 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:
{
"type": "UPDATE_PANEL",
"payload": {
"element": { "name": "panel-abc" },
"panel": { "spec": { "title": "New title" } }
}
}
Deep-merge visualization options:
{
"type": "UPDATE_PANEL",
"payload": {
"element": { "name": "panel-abc" },
"panel": {
"spec": {
"vizConfig": {
"spec": { "options": { "legend": { "displayMode": "table" } } }
}
}
}
}
}
Change plugin type (e.g. timeseries to stat):
{
"type": "UPDATE_PANEL",
"payload": {
"element": { "name": "panel-abc" },
"panel": {
"spec": {
"vizConfig": { "group": "stat", "spec": { "options": { "graphMode": "none" } } }
}
}
}
}
Response (all UPDATE_PANEL variants):
{
"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_PANELRemove one or more panels by element name.
Request:
{
"type": "REMOVE_PANEL",
"payload": {
"elements": [{ "name": "panel-abc" }, { "name": "panel-def" }]
}
}
Response:
{
"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_PANELSList 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):
{ "type": "LIST_PANELS", "payload": {} }
Request (filtered by element names):
{ "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.
{ "type": "LIST_PANELS", "payload": { "includeStatus": true, "includeSchema": true } }
Response:
{
"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:
{
"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):
{
"type": "MOVE_PANEL",
"payload": {
"element": { "name": "panel-abc" },
"toParent": "/tabs/0"
}
}
Response (all MOVE_PANEL variants):
{
"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
positionfield ({ x, y, width, height }) is deprecated. UselayoutItem: { spec: { ... } }instead. If both are provided,layoutItemtakes precedence.
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_VARIABLERequest:
{
"type": "ADD_VARIABLE",
"payload": {
"variable": {
"kind": "CustomVariable",
"spec": { "name": "env", "query": "dev,staging,prod", "multi": true }
}
}
}
Add a section variable on the first row:
{
"type": "ADD_VARIABLE",
"payload": {
"parentPath": "/rows/0",
"variable": {
"kind": "CustomVariable",
"spec": { "name": "region", "query": "eu,us", "multi": false }
}
}
}
Response:
{
"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_VARIABLERequest:
{
"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:
{
"type": "UPDATE_VARIABLE",
"payload": {
"parentPath": "/rows/0",
"name": "region",
"variable": {
"kind": "CustomVariable",
"spec": { "name": "region", "query": "eu,us,apac", "multi": false }
}
}
}
Response:
{
"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_VARIABLERequest:
{
"type": "REMOVE_VARIABLE",
"payload": { "name": "env" }
}
Remove a section variable:
{
"type": "REMOVE_VARIABLE",
"payload": { "parentPath": "/rows/0", "name": "region" }
}
Response:
{
"success": true,
"data": { "name": "env" },
"changes": [{ "path": "/variables/env", "previousValue": "...", "newValue": null }]
}
LIST_VARIABLESRequest:
{ "type": "LIST_VARIABLES", "payload": {} }
List variables for a row section:
{ "type": "LIST_VARIABLES", "payload": { "parentPath": "/rows/0" } }
Response:
{
"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": []
}
UPDATE_DASHBOARD_SETTINGSUpdate 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:
{
"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).
GET_DASHBOARD_INFOGet dashboard identity/folder metadata plus every dashboard-level setting that UPDATE_DASHBOARD_SETTINGS can write. Read-only, no permissions required.
Request:
{ "type": "GET_DASHBOARD_INFO", "payload": {} }
Response:
{
"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": []
}
ENTER_EDIT_MODEEnter dashboard edit mode. Write commands call this automatically; this is rarely needed directly.
Request:
{ "type": "ENTER_EDIT_MODE", "payload": {} }
Response:
{
"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 [].
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_SPECReturn the whole notebook.
{ "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_SPECReplace the notebook from a whole spec. In memory — nothing is saved.
{ "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_SPECCreate 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.
{ "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.
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.
| 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. |
For example, tabs containing rows is valid, and rows containing rows is valid. Tabs directly containing tabs is rejected.