> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cfo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Canvas blocks for agents

> Reference for agents: find Canvas components with inspect_canvas_library, create and patch Canvas blocks with edit_canvas_blocks, shape their datasets, follow the theme, check renders with validate_code_blocks, read saved blocks, and name blocks in edit_pages calls.

A Canvas block is a chart or custom visual on a Page: a sandboxed React component that reads Model values through named datasets. Four tools work with them:

* `inspect_canvas_library` finds the components and templates a block can use.
* `edit_canvas_blocks` creates a block or changes a saved one.
* `validate_code_blocks` renders saved blocks in a browser and reports what it saw.
* `inspect_canvas_blocks` reads saved blocks.

Read the `build-visualizations` skill through `inspect_skills` before you add a Canvas block; it owns design guidance. Page order and block removal belong to `edit_pages`, and editable model tables to `edit_table_blocks`.

## Find components and templates

Call `inspect_canvas_library` with a `list` block whose `query` is a few search words:

```json theme={null}
{ "list": { "query": "slider chart kpi" } }
```

* An entry matches when any one word appears in its title, description, tags, aliases, or decision outcomes.
* `kind` narrows the list to `component` or `template`. Without `query`, the list holds every entry of that kind.

Then `read` the entries you plan to use. One `read` takes up to 16 targets and resolves templates and components together, so a template and the components it needs come back in one call:

```json theme={null}
{ "read": { "target": ["canvas-library:template:hiring-plan-decision", "canvas-library:component:slider", "canvas-library:component:kpi-card"] } }
```

A `read` returns each entry's instructions, its props skeleton, and its runtime requirements. A name that matches nothing, or more than one entry, comes back as an unresolved entry. Library entries are building guidance; they are not saved blocks and prove nothing about how a block renders.

## Create a Canvas block

`edit_canvas_blocks` `create` requires `page`, `title`, and `source`:

```json theme={null}
{
  "scenario": "Main",
  "create": {
    "page": "Board summary",
    "title": "Pipeline by stage",
    "source": "function Block({ data }) { return <Card><Text variant=\"title\">{data.pipeline.rows.length} months of pipeline</Text></Card>; }",
    "datasets": [
      {
        "name": "pipeline",
        "layout": {
          "rows": [{ "variable": "Pipeline value", "breakdown": ["Stage"] }],
          "columns": [{ "time": "MONTH" }]
        }
      }
    ]
  }
}
```

* `page` is the Page that receives the block, by name or as `inspect_pages` prints it. `after_block` places the block under another one; without it the block goes last.
* `title` is the block's name. Later updates and page edits name the block by it.
* The receipt names the new block two ways. `target` is the block's name-form selector, which the page and Canvas tools accept wherever they take a block as `target`. `ref` is the block's pasteable reference, such as `Pipeline by stage#a74`, which they accept wherever a result's block reference is accepted, such as `reorder_blocks` `block_order`. The reference keeps naming the block in later `edit_pages` calls even after a rename.
* The receipt also carries `dataset_evidence`, one series and one row per dataset as the block receives them, and `source_notes`. A dataset still calculating when the result was due is listed with `status: "unavailable"` and `error`: **this dataset's calculation had not finished when this result was due, so it was stopped**. The block is saved either way, and the other datasets keep their evidence.

### What the source must be

`source` is one complete component, `Block({ data, timeSettings, theme, state, action, emit })`, run in a sandbox:

* React is injected. Reach hooks through it, such as `React.useState(...)`; a bare `useState(...)` is refused, and `window.React` is unavailable.
* Imports, network access, and direct AgCharts are forbidden. Custom font families are refused.
* `print()` prints only that block's frame.
* The injected components, their props, and the rest of the runtime contract are in the `source` field's description in the tool schema, and each component's props in `inspect_canvas_library` `read`.

## Datasets

A dataset is a live, read-only table read that the block receives as `data.<name>`. A block holds up to 8.

| Field | Meaning |
| - | - |
| `name` | The identifier the code uses, `data.<name>`. It must match `^[a-zA-Z][a-zA-Z0-9_]{0,63}$`: a letter, then letters, digits, or underscores, at most 64 characters in all. |
| `layout.rows`, `layout.columns` | The visible shape, as a table declares it. Variables, with any breakdown, go on rows; the shared dates or categories go on columns. [Table view grammar](/agents/table-view-grammar.md#canvas-dataset-layouts) has the layout fields. |
| `scenario` | Optional Scenario for a comparison dataset. Without it, the dataset reads the current Scenario. A block reads at most three distinct Scenarios, the selected one included. |
| `window` | Optional `start`, `end` (as `YYYY-MM-DD`), and `granularity`. Without it, the workspace default applies. |

The block receives the read inverted: each row of the layout becomes a series, and each column becomes one entry of `data.<name>.rows`, one plotted point. A layout with `columns: []` and `window.granularity: "NO_GRANULARITY"` declares timeless values: one row holding each Variable's value.

On `update`, each `datasets` entry adds a dataset or replaces the saved one with the same name, and the others stay. Leaving `datasets` out keeps them all; `[]` clears them all; `remove_datasets` removes the ones it names.

### Values a dataset row carries

Each row carries each Variable's own value under its series key:

| Saved value | What the row holds |
| - | - |
| Number | A finite number |
| Text | The text |
| Date | An ISO day, such as `2026-01-30` |
| Boolean | `"true"` or `"false"` |
| Date and time | A complete UTC instant in ISO form, such as `2026-01-30T00:00:00.000Z`, for a field that carries a time |
| Empty cell or calculation error | `null` |

`row.date` is a display label, and `periodStart`, when present, is the ISO start of the period. A dataset that failed has empty `rows` and `series` and an `error` string. A comparison dataset also carries `scenario.label`, `scenario.isCurrent`, and `scenario.lastClose`, and `timeSettings.lastClose` is the current Scenario's close date or `null`.

## Colors and the theme

* Colors and type from `theme.tokens`, `theme.design`, or `--cb-*` CSS variables follow the viewer's light or dark mode.
* A literal or named color, including one in a palette, keeps its value in both modes. The write saves it, and the receipt's `source_notes` names the first one: "`<color>` on line `<n>` is the first fixed color in this source: it keeps this value in light and dark mode. Colors from theme.tokens, theme.design, or --cb-\* CSS vars follow the viewer's mode."
* A workspace brand can set the heading typeface, the accent color, and the corner shape. A block follows the brand through `theme.tokens.fontHeading` or `--cb-font-heading`, `theme.tokens.accent` or `--cb-accent`, and `theme.tokens.radius` or `--cb-radius`, in every workspace it is copied to.
* `source_notes` also flags a component declared inside `Block`. React remounts such a component every time `Block` renders, so a focused input loses focus and its local state resets. Declare components at the top level of the source and pass values through props.

## Change a saved block

`edit_canvas_blocks` `update` names the block with `target`, by name, ID, or as `inspect_pages` prints it. Fields you leave out keep their saved values, and the block keeps its identity and position.

* `source` replaces the whole component.
* `source_patches` changes part of it: a list of up to 50 exact `old` to `new` text replacements, applied in order. Each `old` must occur exactly once in the source as the earlier patches left it. The block saves only when every patch applies.
* `title` renames the block; an empty string clears the title.
* `page`, when given, confirms the block is still on that Page.

```json theme={null}
{
  "update": {
    "target": "Pipeline by stage#a74",
    "source_patches": [
      { "old": "height={240}", "new": "height={320}" },
      { "old": "title=\"Pipeline\"", "new": "title=\"Pipeline by stage\"" }
    ]
  }
}
```

## Check that a block renders

A saved block compiles, but only a browser shows whether it renders.

* A Canvas write that no browser rendered returns a `render_unverified` warning: **The block is saved and compiles; whether it renders is proven only when validate\_code\_blocks opens it in a browser.**
* `validate: true` beside the write returns the browser's result for the saved revision under `validation` instead. A render check that fails or is unavailable leaves the edit saved. The dataset readback and the render check share one answer deadline, about 10 seconds before the call's 119-second limit, so the receipt arrives instead of a timeout. If validation has not finished when the result is due, `validation.note` reads **The Canvas edit saved. Browser validation had not finished when this result was due, so it was stopped and the render was not observed.** If the result is due before validation begins, it reads **The Canvas edit saved. This result was due before browser validation began, so validation did not run and the render was not observed.** In both cases `validation.enabled` is `false`, as it is whenever no browser render was observed, including when the check is switched off or unreachable; read `validation.note` to tell the causes apart. The receipt carries the `render_unverified` warning, and the saved block stays saved.
* `validate_code_blocks` checks saved blocks on their own:

```json theme={null}
{ "target": ["Pipeline by stage#a74", "Cash runway gauge#1c0"], "visual_review": { "target": "Pipeline by stage#a74", "theme": "dark" } }
```

### What `validate_code_blocks` reports

* It renders up to 8 blocks per call in Chromium, with live calculated datasets, once per theme (light and dark). Each render uses the block's saved state, Scenario, number formats, and Last close, in a frame 1200 px wide. Each result names the revision and source hash it rendered.
* The result's `observed` sentence states what the check saw, such as: "Each block was rendered once per theme, with its saved state, in a 1200 px wide frame. Errors, warnings, plotted chart values, guide anchors, and any visual review come from those renders. Clicks, typing, links, and guide launches did not run, so what they do is unobserved, and the layout at other widths is unseen."
* A block that rendered cleanly carries `plotted`: one entry per chart the render drew, with `chart` naming the chart by its title, when it has one, and its source position, such as `Chart "Pipeline" at source L12`, and `series` holding one entry per numeric value field of every visible series, with `name`, `count`, and the `first`, `last`, `low`, and `high` plotted points as `{x, y}`. The values are the ones the chart was given. The chart draws stacked and normalized series, waterfall running totals, and histogram bin counts from them, so the drawn shape of such a series differs from the list.
* Errors are failures the render reported: runtime errors, widget errors, a stalled render, console errors, and failed element loads. Warnings may be intended: console warnings, blank or error-code cells, empty or zero-only data, missing series keys, hidden series, and content past the frame's edge.
* A clean render does not check that the values are financially correct.
* `guide_targets` lists the copyable guide anchors visible in at least one theme, and `guideAnchors` keeps each theme's visibility and ambiguity. `guideAnchors` lists at most the first 64 distinct anchor IDs per theme, in document order. When the render had more, `guide_anchors_cut` says how many, and `guide_targets` draws only on the listed ones.
* `visual_review` returns one block in one theme as an image at the frame's width.

### When a render does not happen

| Situation | Result |
| - | - |
| The blocks in one call read a lot of calculated data | They render in several consecutive requests. Each request carries at most 20 MiB of calculated data. |
| One block alone reads more than 20 MiB | That block is not rendered. Its result is `valid: false`, with one message per theme that states its size and that nothing about how it renders was observed. The other blocks still render, and `observed` counts how many did. |
| Browser validation is switched off or unreachable | `enabled: false`, with a note that the saved block was not rendered or validated. |
| The render run failed | `ok: false`, `error_kind: browser_validation_unavailable`, `transient: true`, and a message that the browser render check failed before it reported on these blocks, with the error behind it. Send the same call again. |

### Errors a person's browser reported

When a person's browser hits a runtime failure while rendering a saved block, it reports the error for that exact Scenario, block, and source revision. `inspect_pages` `read` then returns it as the block's `render_error`, and a later successful render clears it. An MCP call has no browser of its own, so a block nobody has rendered has no such report. A missing `render_error` does not mean the block renders.

## Read saved Canvas blocks

`inspect_canvas_blocks` takes `target`, a block name, ID, or reference, or a list of related blocks, and optionally `page` to constrain them. It returns each block's exact saved source, dataset layouts and Scenario bindings, display state, position, revision, and references. All blocks are read at one snapshot, the latest unless `as_of_point` names an earlier one. It does not calculate datasets, and saved code is no proof of a successful render.

A selection larger than one result is cut short:

* Each run of 1,000 or more encoded characters in a source, such as base64 or hex, reads as `[[N encoded characters not shown]]`.
* A source still too large comes back in part. `source_part` gives the byte range it covers, and the next call passes `next.source_offset` as `source_offset`.
* `not_returned` lists the selected blocks left out, with their sizes, and `next` holds the arguments that read the rest.

## Links a block opens

On a user click, `emit({type: 'navigate', url})` opens a page or block link that a tool result returned, or another app address in the current workspace, through normal browser history. It keeps Scenario query parameters and `#block-anchor-<block-id>`. External addresses and public share views are not supported. The `source` field's description covers the block's other actions, such as saving presentation `state` and starting an Ari action.

## Name blocks in page edits

`edit_pages` arranges and removes blocks, including Canvas blocks.

* A block action names its Page with `page`.
* `add_text` may omit `page`. It then uses the Page of an `after_block` reference it was given, else the Page in view, else the Scenario's only Page. When none of these settles it, the call is refused: with several Pages, with **no target page** and up to 20 of them as candidates, and its hint gives the Scenario's page count and how many are listed; with no Pages, with **this scenario holds no pages**.
* `update_text` and `delete_block` may omit `page`; they then use the Page in view, and a returned block target finds its own Page.
* `reorder_blocks` names the Page it arranges with `page`, or with `target`. `block_order` is the complete top-to-bottom order, with each visible block once, by name or by the block references other results print. `place` moves one block instead: `block`, `after_block`, and `to_page` to move it to another Page.
* A `delete_block` aimed at a block that another saved change already deleted answers `not_found` and changes nothing. Its message names that change and, when the block went with its Page, the Page: `Block <id> is already deleted in scenario "Main": saved change <n> deleted it together with its page "<name>". This call changed nothing.` The result carries `block_id`, `layer_id`, and `deleted_by_change_log_id`.
* `delete_block` refuses to delete objects that lose their last table occurrence while formulas, other blocks, settings, or hand-added items depend on them. `force: true` deletes them anyway, and those references break. Objects still shown on other tables are kept.

For paging through `inspect_pages`, see [Pagination and truncation](/agents/mcp-tools.md#pagination-and-truncation).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.