---
title: "Blok Tools API — register and update tools"
description: "Register tools, update the toolbox at runtime, and read back which tools are currently available."
source: https://blokeditor.com/docs/tools-api/
lastmod: 2026-09-24
---

Framework JavaScript

Extending & system Tools

On this page tools.getBlockTools()

# Tools API: register and update tools

Access and manage editor tools.

Last updated Sep 24, 2026 [Edit this page on GitHub](https://github.com/JackUait/blok/blob/main/docs/src/components/api/api-data.ts)

### Reaching the editor instance

The methods below run on the editor you created with new Blok(). They are available once editor.isReady resolves.

TypeScript

```
// You already hold the instance returned by the constructor.
const editor = new Blok({ holder: 'editor' });
await editor.isReady;

// Call any API method on it.
editor.caret.setToLastBlock('end');
```

## Methods

### tools.getBlockTools()

BlockToolAdapter[]

Get all available block tool adapters. Each adapter exposes `name` plus tool metadata, including `assetKind`.

- On media tools that store an uploaded asset URL at `data.url`, `assetKind` is `'image' | 'video' | 'audio' | 'file'`.
- On every other tool it is `undefined`.

Use it to find the media-bearing tools at runtime, instead of hardcoding each tool's data shape. You can then check a saved document's `data.url`s against your CDN, for example to clean up orphaned uploads.

When to use

Lists the registered block tools at runtime. Handy for a custom block picker or for debugging tool config.

TypeScript

```
const blockTools = editor.tools.getBlockTools();
blockTools.forEach(tool => {
  console.log('Available tool:', tool.name);
});

// Discover which block types hold uploaded media, then collect their URLs
const mediaTypes = new Set(
  editor.tools.getBlockTools().filter(t => t.assetKind).map(t => t.name)
);
const referenced = (await editor.save()).blocks
  .filter(b => mediaTypes.has(b.type))
  .map(b => b.data.url);
```

### tools.getToolsConfig()

ToolsConfig

Returns the tools-related configuration of this instance: { tools, inlineToolbar?, tunes?, theme? }. Use it to create nested Blok editors with the same tool set.

TypeScript

```
const nested = new Blok({ holder, ...editor.tools.getToolsConfig() });
```

### tools.update(name, config)

void

Shallow-merges new configuration into an installed tool at runtime, with no editor recreation. A `toolbox` key is treated as the tool-level setting, the same as `toolbox` in the `tools` map.

- Pass `toolbox: false` to hide the tool from every insertion surface; existing blocks keep rendering.
- Pass a toolbox object to show it again.

This gates permissions without rebuilding the editor. Under the React adapter it is automatic: change the `toolbox` value in the `tools` prop and `useBlok`/`BlokEditor` applies it in place.

TypeScript

```
// Swap a config value (e.g. an uploader) at runtime
editor.tools.update('image', { uploader: { uploadByFile } });

// Permission flip: hide the tool from the + / slash / convert menus.
// Existing goodsList blocks still render; insertion is gated.
editor.tools.update('goodsList', { toolbox: false });

// Re-enable it later
editor.tools.update('goodsList', { toolbox: { title: 'Goods List' } });
```

### tools.setInlineToolbar(config)

void

Runtime setter for the global `inlineToolbar` config. It re-assigns inline tools for every block tool and recomposes the memoized sanitize configs.

- Paste-time sanitization follows the new set immediately.
- The inline toolbar reflects it on the next selection.
- Tool-scoped `inlineToolbar` settings, both arrays and opt-outs, stay authoritative.

Pass `true` for all inline tools, `false` for none, or a list of inline tool names. The list selects which tools appear, while their left-to-right order stays fixed.

If you render saved content through @bloklabs/core/view, note that a viewSchema is composed from the inlineToolbar value it was defined with. After a runtime setInlineToolbar involving custom inline tools, recompose it with defineBlokSchema before calling blocksToHtml.

When to use

Takes effect on the next selection. Paste-time sanitization is recomposed immediately, so pasted content follows the new inline-tool set right away. If you render saved content through @bloklabs/core/view, note that a viewSchema is composed from the inlineToolbar value it was defined with. After a runtime setInlineToolbar involving custom inline tools, recompose it with defineBlokSchema before calling blocksToHtml.

Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `config` | `boolean | string[]` | Required | — | `true` enables every registered inline tool. `false` disables the inline toolbar. An array restricts it to the listed inline tools, in that order. |

TypeScript

```
// Restrict inline formatting to bold and italic at runtime
editor.tools.setInlineToolbar(['bold', 'italic']);

// Disable the inline toolbar entirely
editor.tools.setInlineToolbar(false);

// Back to every registered inline tool
editor.tools.setInlineToolbar(true);
```

### tools.isInstalled(name)

boolean

Returns true when a tool with the given name is installed and available on this editor instance. That covers block, inline and tune tools. Use it to inspect the installed tool set, for example as a guard before `tools.update(name, config)`, which throws for unknown names.

When to use

Guard runtime tool calls. `tools.update()` throws for names that are not installed.

Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | `string` | Required | — | Registered tool name to look up. |

TypeScript

```
if (editor.tools.isInstalled('image')) {
  editor.tools.update('image', { uploader: { uploadByFile } });
}
```

### defineTool(toolClass, settings?)

ExternalToolSettings

A registration helper exported from `@bloklabs/core/tools`. It is not a member of the `tools` namespace.

The plain `tools` map types every entry with a bare `ToolSettings` whose `Config` falls back to `Record<string, unknown>`. So a misspelled config key (`defaultLevle` for `defaultLevel`) compiles silently. `defineTool` recovers the tool's real config type from its constructor and applies it to `settings.config`, turning those typos into compile errors.

The type-checking happens on the `settings` argument. The RETURN type stays the erased `ExternalToolSettings`, so the result drops straight into the `tools` map.

`ExtractToolConfig<TClass>` is the type that does the recovery. It is exported alongside `defineTool`, and falls back to `Record<string, unknown>` for tool classes whose constructor declares no concrete config.

TypeScript

```
import { Blok } from '@bloklabs/core';
import { Header, defineTool } from '@bloklabs/core/tools';

new Blok({
  tools: {
    header: defineTool(Header, { config: { levels: [1, 2, 3] } }),
    // `defaultLevle: 2` here would now be a compile error
  },
});
```

### mountChildBlocks(container, children)

void

The child-holder reconciler for container blocks, exported from `@bloklabs/core/tools`. Call it from your tool's `rendered()` hook. The built-in toggle, callout and column tools use it, and the React/Vue/Angular block adapters run it on every commit.

It is idempotent and cheap, so run it on every render. Per child it behaves like this.

- It leaves a holder already inside `container` alone.
- It RECLAIMS a holder stranded in a nested container that ENCLOSES `container`, inserting it at its model position rather than appending it last.
- It leaves holders sitting in any OTHER nested container alone, so two containers can never steal each other's blocks.
- It mounts everything else at its model position.

The reclaim is what makes a container survive the insert ordering. Core anchors a newly inserted first child as the container block's DOM sibling, so without the reclaim a child of a nested container renders one level out. That is permanent when your container's child slot had not been created yet at insert time, which is what happens when a framework portal commits a render after core inserts.

Mark `container` with `data-blok-nested-blocks` so the rest of the editor recognises it as a container.

Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `container` | `HTMLElement` | Required | — | The element child holders belong in. It is the element carrying `data-blok-nested-blocks`. |
| `children` | `{ holder: HTMLElement }[]` | Required | — | The block's children in model order, normally `api.blocks.getChildren(blockId)`. |

TypeScript

```
import { mountChildBlocks } from '@bloklabs/core/tools';

class CardTool {
  constructor({ api, block }) {
    this.api = api;
    this.blockId = block.id;
  }

  render() {
    this.slot = document.createElement('div');
    this.slot.setAttribute('data-blok-nested-blocks', '');

    return this.slot;
  }

  // Runs after the holder is in the document, and on every re-render
  rendered() {
    mountChildBlocks(this.slot, this.api.blocks.getChildren(this.blockId));
  }
}
```

### BlockToolConstructorOptions.origin

BlockOrigin

The create-vs-restore signal on the tool contract. Blok hands it to every block tool's constructor alongside `data`, `block` and `readOnly`.

A container tool that seeds default children, such as a two-column row or a card that starts with a heading, may only do that once, at creation. Every other time the tool is constructed, the document already says what its children are.

During a restore those children commonly land a tick AFTER `rendered()` runs, so an empty `api.blocks.getChildren()` there is only transient. Seeding on that read fabricates phantom children beside the real ones.

CREATION values, where you may seed:

- `'user'` (a direct editing gesture: Enter, the plus button, the slash menu, block settings, a markdown shortcut).
- `'api'` (a programmatic `blocks.insert` / `insertMany` / `insertInsideParent`).
- `'convert'` (a turn-into).

RESTORE values, where you never seed:

- `'load'` (a document render).
- `'replay'` (an undo/redo replay or a remote collaborative update).
- `'paste'` (pasted content that brings its own children).
- `'probe'` (the OFF-TREE instance `blocks.composeBlockData()` builds to read a tool's default data).

The probe instance is never inserted, yet it still runs `render()` and `rendered()`, so it must not touch the block tree at all.

Blok always supplies the value. Treat an absent value as `'api'`, and write the check as an allow-list of creation values, so a future origin fails closed. Pair it with `blocks.insert(..., origin)` if you drive insertion from your own UI.

On the React/Vue/Angular adapters you rarely read it by hand. The block spec's `onCreated` hook already encodes this allow-list. It fires after the adapter's first commit, the tick at which the block's DOM and its adopted child holders actually exist.

TypeScript

```
class TwoColumnCard {
  constructor({ api, block, origin }) {
    this.api = api;
    this.blockId = block.id;
    // Allow-list, so a future origin never silently opts into seeding.
    this.isCreation = ['user', 'api', 'convert', undefined].includes(origin);
  }

  render() {
    this.slot = document.createElement('div');
    this.slot.setAttribute('data-blok-nested-blocks', '');

    return this.slot;
  }

  rendered() {
    const children = this.api.blocks.getChildren(this.blockId);

    if (children.length > 0) {
      mountChildBlocks(this.slot, children);

      return;
    }

    // Empty on a load / undo-redo replay / paste / probe means "my children
    // have not arrived yet", NOT "I am brand new". Only a creation may seed.
    if (!this.isCreation) {
      return;
    }

    this.seedColumns();
  }
}
```

### BlockToolConstructorOptions.replaySource

'history' | 'remote' | undefined

Who rebuilt the block, set only when `origin` is `'replay'`.

- `'history'`: this client's own undo or redo.
- `'remote'`: a peer's change arrived over collaboration.

Every client rebuilds the block for a peer's change. So a side effect that only the acting client should run, such as a network fetch or a derived write, belongs to `'history'`. It is `undefined` for every other origin.

TypeScript

```
class Bookmark {
  constructor({ data, origin, replaySource }) {
    // Only the client that redid the paste fetches the preview again.
    this.refetch = origin === 'replay' && replaySource !== 'remote' && data.title === undefined;
  }
}
```

### BlockToolConstructable.keepsChildrenOnEnter

boolean

A static on your tool CLASS that decides where Enter goes on the container's empty LAST child. By default Blok reads that empty trailing line as the author's way out.

- With siblings present the line is outdented to the container's own parent.
- When it is the sole child, Blok inserts a fresh block after the whole container.

That is Notion's callout behaviour. A layout container whose children ARE its content (a column, a card, a `steps` block) wants the opposite. Such a tool needs the declaration: without it, the escape strands the new line beside the container.

Set it to `true` and the new line stays inside. That is the same rule the built-in `column`, `column_list` and `toggle` follow. It cannot be inferred from the DOM: a callout renders the very same `data-blok-nested-blocks` slot as a column and deliberately keeps the escape, so this is per-tool policy.

Core reads it for the symmetric "remove one indent level" gesture too (Enter/Backspace on a block nested under a PLAIN parent). A declaring tool is treated as a container there as well, and its children never stepwise-outdent out of it.

On the React/Vue/Angular adapters, declare it in the block spec's `statics` bag like any other class static.

TypeScript

```
class StepsTool {
  static keepsChildrenOnEnter = true;

  render() {
    this.slot = document.createElement('div');
    this.slot.setAttribute('data-blok-nested-blocks', '');

    return this.slot;
  }

  rendered() {
    mountChildBlocks(this.slot, this.api.blocks.getChildren(this.blockId));
  }
}

// Framework adapters forward it through `statics`:
export const StepsTool = createReactBlock({
  type: 'steps',
  statics: { ownsChildren: true, keepsChildrenOnEnter: true },
  component: StepsCard,
});
```

### BlockToolConstructable.childTools

{ allow?: string[]; deny?: string[] }

A static on your tool CLASS declaring which block tools may be DIRECT children of its block, and core enforces it everywhere for you.

- On INSERT a disallowed tool is demoted, never refused, because Enter must always produce a block.
  - The target is the first entry of `allow`, so `allow: ['segment-item']` makes "Enter at the end of a segment" produce another segment instead of a stray paragraph.
- On MOVE a drag or keyboard reorder that would carry a disallowed block across the container boundary is refused.
- In the TOOLBOX the disallowed tools are hidden while the caret sits in a child.

`deny` wins over `allow` for a tool named in both, and empty lists read as "no restriction".

This is the selective, insert-aware counterpart to `ownsChildren`, which is all-or-nothing and clamps moves only. It is also the generic form of the Table tool's `restrictedTools`, whose enforcement is hard-wired to table cells.

Without it a container tool has to defend itself downstream: filtering `child.name` in render, keeping its CSS working when a foreign child appears, and migrating strays out of stored documents. On the React/Vue/Angular adapters, declare it in the block spec's `statics` bag like any other class static.

TypeScript

```
class Segments {
  static get childTools() {
    return { allow: ['segment-item'] };
  }

  render() {
    this.slot = document.createElement('div');
    this.slot.setAttribute('data-blok-nested-blocks', '');

    return this.slot;
  }
}

// Only forbid a few tools, accept everything else
class Callout {
  static childTools = { deny: ['table', 'column_list'] };
}

// Framework adapters forward it through `statics`:
export const Segments = createReactBlock({
  type: 'segments',
  statics: { childTools: { allow: ['segment-item'] } },
  component: SegmentsCard,
});
```

### setData(newData)

boolean | void | Promise<boolean | void>

An optional method on your tool that applies new data to the LIVE instance. Declare it, and `blocks.update()`, undo/redo and remote collaborative edits all reuse the block you already rendered instead of recomposing it.

There is no new tool instance and no new holder, so ephemeral state survives: an open menu, a scroll position, a framework component's local state. The adopted child holders and the caret survive too.

Without it, core destroys the block and builds a replacement. That is why a host calling `blocks.update()` on every keystroke used to watch a component-backed block go blank.

- Return `false` when you cannot apply the data in place, and core falls back to the full recompose.
  - The list tool does that for a style change, which needs a different DOM shape.
- Returning `true` or nothing means the data was applied.
- Throwing has the same effect as `false`: it is logged, then the block is recomposed.

The React/Vue/Angular block factories implement it for you, so adapter blocks take the in-place path automatically.

Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `newData` | `BlockToolData` | Required | — | The block's full data after the update. That is the existing data merged with the caller's patch, not the patch alone. |

TypeScript

```
class CalloutTool {
  setData(newData) {
    if (newData.variant !== this.data.variant) {
      // A different variant renders a different DOM shape — let core rebuild.
      return false;
    }

    this.data = newData;
    this.box.textContent = newData.text ?? '';

    return true;
  }
}
```
