---
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-07
---

Framework JavaScript

Extending & system Tools

On this page tools.getBlockTools()

# Tools API: register and update tools

Access and manage editor tools.

[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` — set to `'image' | 'video' | 'audio' | 'file'` on media tools that store an uploaded asset URL at `data.url`, and `undefined` otherwise. Use it to discover the media-bearing tool set at runtime (instead of hardcoding each tool's data shape) and reconcile a saved document's `data.url`s against your CDN — e.g. to garbage-collect orphaned uploads.

When to use

Enumerate the registered block tools at runtime — handy for a custom block picker or 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? } — for creating nested Blok editors with the same tool set.

TypeScript

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

### tools.update(name, config)

void

Shallow-merge new configuration into an installed tool at runtime — no editor recreation. A `toolbox` key is treated as the tool-level setting (same as `toolbox` in the `tools` map): pass `toolbox: false` to hide the tool from every insertion surface (existing blocks keep rendering) or a toolbox object to (re)show it — permission gating without rebuilding the editor. Under the React adapter this 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. Re-assigns inline tools for every block tool and recomposes the memoized sanitize configs — so paste-time sanitization follows the new set immediately, and the inline toolbar reflects it on the next selection. Tool-scoped `inlineToolbar` settings (arrays and opt-outs) stay authoritative. Pass `true` for all inline tools, `false` for none, or an ordered list of inline tool names. 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 — block, inline or tune. Public introspection over the installed tool set, e.g. 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`, 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>` — the type that does the recovery — is exported alongside it, 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 — it is what the built-in toggle, callout and column tools use, and what the React/Vue/Angular block adapters run on every commit. It is idempotent and cheap, so run it on every render. Per child it: leaves a holder already inside `container` alone; RECLAIMS a holder stranded in a nested container that ENCLOSES `container`, inserting it at its model position rather than appending it last; leaves holders sitting in any OTHER nested container alone, so two containers can never steal each other's blocks; and 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 it a child of a nested container renders one level out — permanently, when your container's child slot had not been created yet at insert time (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 — 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, handed to every block tool's constructor alongside `data`, `block` and `readOnly`. A container tool that seeds default children — a two-column row, 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, and 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 — 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 — 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 — it is never inserted, yet it still runs `render()` and `rendered()`, so it must not touch the block tree at all). Blok always supplies it; 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, and 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();
  }
}
```

### 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, and as a sole child a fresh block is inserted after the whole container — Notion's callout behaviour. A layout container whose children ARE its content (a column, a card, a `steps` block) wants the opposite, and without the declaration the escape strands the new line beside the container. Set it to `true` and the new line stays inside, 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), so 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 — and 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 robust against a foreign child, 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 — no new tool instance, no new holder, so ephemeral state (an open menu, a scroll position, a framework component's local state), the adopted child holders and the caret survive. Without it core destroys the block and builds a replacement, which is why a host that called `blocks.update()` per keystroke used to watch a component-backed block go blank. Return `false` when you cannot apply the data in place — the list tool does that for a style change, which needs a different DOM shape — and core falls back to the full recompose; returning `true` or nothing means it was applied. Throwing has the same effect as `false` (logged, then recomposed). The React/Vue/Angular block factories implement it for you, so adapter blocks are on the in-place path automatically.

Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `newData` | `BlockToolData` | Required | — | The block's full data after the update — 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;
  }
}
```
