---
title: "Block capabilities, abilities and actions"
description: "Declare a Block capability in a host Feature, gate a Block action with requires and ability, see what abilities gate and let a Block type opt in."
canonical_url: "https://skaile-ai.github.io/forge-editor/guides/block-capabilities"
---
# Block capabilities, abilities and actions

> Declare a Block capability in a host Feature, gate a Block action with requires and ability, see what abilities gate and let a Block type opt in.

A **Block capability** says what a Block supports: it is a Container block, it has a Source, it carries a Bibliographic record. It is code, and a Feature declares it. A [Block type](https://skaile-ai.github.io/forge-editor/guides/block-types) is data: it lists the capabilities its Typed blocks have and configures them. A **Block action** names the capabilities a Block must have (`requires`) and the ability its place in the document must have (`ability`).

Four words, four jobs:

<table>
<thead>
  <tr>
    <th>
      Word
    </th>
    
    <th>
      Question it answers
    </th>
    
    <th>
      Where it lives
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Block capability
    </td>
    
    <td>
      What is this Block?
    </td>
    
    <td>
      <code>
        EditorFeature.blockCapabilities
      </code>
      
       (code)
    </td>
  </tr>
  
  <tr>
    <td>
      Block type
    </td>
    
    <td>
      Which capabilities does a Typed block of this type have, and how are they set?
    </td>
    
    <td>
      a <code>
        BlockType
      </code>
      
       (data, JSON)
    </td>
  </tr>
  
  <tr>
    <td>
      Ability
    </td>
    
    <td>
      What is offered at this place in the document?
    </td>
    
    <td>
      <code>
        ABILITY
      </code>
      
       ids; the <code>
        abilities
      </code>
      
       rule of each node and the host's <code>
        abilities
      </code>
      
       prop
    </td>
  </tr>
  
  <tr>
    <td>
      Block action
    </td>
    
    <td>
      What can the user run on this Block?
    </td>
    
    <td>
      <code>
        EditorFeature.blockActions
      </code>
      
      , derive methods, the host's <code>
        blockActions
      </code>
    </td>
  </tr>
</tbody>
</table>

A capability describes and gates nothing by itself; an action gates on it through `requires`. It also never installs anything: the schema, the extensions and the node views stay with the Feature that owns the mechanism. A capability descriptor only tells the editor how to find out whether a Block has it, where its data lives and how to read it.

The library declares these capabilities: `container` and `title` are built in, the composites feature adds `properties`, `source`, `bibliography`, `basket`, `titleHidden`, `ai-brief`, `references`, `import`, `export` and `derived`. The [capability contract](https://skaile-ai.github.io/forge-editor/reference/block-capabilities) lists them with their config.

## Declare a capability

A host adds a capability as a Feature with `blockCapabilities`. The library's own `pruefung` Feature is the worked example: a Typed block has `pruefung` when its Block type lists it, and the Block type names the kinds of Prüfung it takes (`redaktionell`, `fachlich`). The same Feature adds the Block action „Prüfung eintragen“, which `requires` the capability. The snippet shows the shape of the shipped `pruefungCapability`; import it from `@skaile/forge-editor/pruefung` instead of declaring it again.

```ts [docs/snippets/pruefung-capability.ts]
import { z } from 'zod'
import type { BlockCapability } from '@skaile/forge-editor/core'
import { listsCapability } from '@skaile/forge-editor/core'

// The config a Block type gives. It is validated when the editor is created.
const pruefungConfig = z.strictObject({
  kinds: z.array(z.string().min(1)).min(1),
  chain: z.boolean().default(false),
  lock: z.string().min(1).optional(),
})
type PruefungConfig = z.infer<typeof pruefungConfig>

// The capability only describes: a Typed block has `pruefung` when its Block type lists it. This is the shape of the
// shipped `pruefungCapability` (`@skaile/forge-editor/pruefung`); the `pruefung` Feature owns the mechanism (records, hash, actions).
export const capability: BlockCapability = {
  id: 'pruefung',
  configSchema: pruefungConfig,
  appliesTo: (node, ctx) => listsCapability(ctx.blockTypeOf(node), 'pruefung'),
  read: (node, ctx) => ctx.configOf<PruefungConfig>(node, 'pruefung') ?? null,
}
```

The members of the descriptor:

- `id` is unique across the preset. Block types and `requires` entries name it. A Feature may not use the id of a built-in capability, and two Features may not declare the same id: `defineEditor` throws.
- `appliesTo(node, ctx)` decides whether `node` has the capability. `listsCapability(ctx.blockTypeOf(node), id)` answers "the Block type lists it". The result is memoised per node type and Block type id, so the predicate may read only the node type (its spec and flags), `ctx.blockTypeId` and the Block type's config (`ctx.configOf`). A capability that must read an attribute of the node itself sets `perNode: true` and keeps the check a constant-time attribute read (`derived` reads `meta.derivation` this way).
- `configSchema` is a zod schema for the config a Block type gives. Without it the capability takes no config and a Block type that gives one is refused.
- `read(node, ctx)` returns what the Block holds for the capability, or `null`: the Source media child, the Basket id, the Bibliographic record. It runs only for Blocks that have the capability and is not memoised.
- `storage` describes where the data lives (`metaKeys`, `childTypes`), so conversion, Markdown and copy can treat it the same way. It is descriptive only.

`appliesTo` answers "does a Block of this type support it". What a single Block holds is Block contents, and `read` returns it. Keep the two apart.

## Gate a Block action with `requires`

`BlockAction.requires` lists capability ids. The action is hidden on a Block that lacks one of them, and `blockActionsOf` throws when an id is declared by no Feature. It replaces node-name and Block-type-name checks in `isVisible`. `ability` is the second, independent gate: an action shows when the Block has every required capability **and** its place in the document has the ability ([Abilities](#abilities)). `blockTypes` limits an action to named Block types and is only for an action that is truly specific to a type; if the action is about what a Block can do, use `requires`.

## Abilities

An ability is a plain string id (`ABILITY` in `@skaile/forge-editor/core/schema`, or your own). Which abilities hold at a position follows from the nodes around it: the set starts with `abilities.root` (default `ROOT_ABILITIES`), then every ancestor from the outside in adds its `grants` and removes its `revokes`, and the innermost node finally adds its `own`. A code block revokes marks and headings, media and the title row revoke text editing, and a Typed block's `own` abilities (`composite.meta`, `composite.derive`, …) hold on the block itself, not on a paragraph inside it. A rule may be a function of the node, so it can read the node's attributes. Host rules passed through the `abilities` prop replace a node's own rule; the prop is read once at mount.

One ability gates many things. The editor checks it for:

<table>
<thead>
  <tr>
    <th>
      Item
    </th>
    
    <th>
      Field
    </th>
    
    <th>
      Effect where the ability is missing
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Toolbar and <code>
        /
      </code>
      
       entries
    </td>
    
    <td>
      <code>
        CommandDef.ability
      </code>
    </td>
    
    <td>
      disabled (toolbar) or not listed (<code>
        /
      </code>
      
       menu)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Mod-
      </code>
      
      /<code>
        Alt-
      </code>
      
       shortcuts and input rules of a node or mark type
    </td>
    
    <td>
      <code>
        TYPE_ABILITIES
      </code>
    </td>
    
    <td>
      the key is swallowed; the typed text stays literal
    </td>
  </tr>
  
  <tr>
    <td>
      Block actions and derive methods
    </td>
    
    <td>
      <code>
        BlockAction.ability
      </code>
      
      , <code>
        DeriveMethod.ability
      </code>
    </td>
    
    <td>
      hidden
    </td>
  </tr>
  
  <tr>
    <td>
      „Umwandeln in …“ targets
    </td>
    
    <td>
      <code>
        TYPE_ABILITIES
      </code>
      
       of the target type
    </td>
    
    <td>
      not offered
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        @
      </code>
      
       mentions, block references, AI rewrite and autocomplete
    </td>
    
    <td>
      <code>
        insert.mention
      </code>
      
      , <code>
        insert.reference
      </code>
      
      , <code>
        ai.*
      </code>
    </td>
    
    <td>
      the popup, menu or ghost text does not open
    </td>
  </tr>
</tbody>
</table>

An ability decides what the editor **offers**, not what may be **written**. Pasted and dropped content, collaborators' changes and commands called from code are not checked, plain typing and Backspace have no ability, and the editor knows no users: a host that wants per-role abilities passes a different `abilities` prop per mount. To refuse a change, use a `transactionGuards` contribution ([Feature contract](https://skaile-ai.github.io/forge-editor/reference/feature-contract)); to make a whole document read-only for a user, use `authorize` ([Editor ports](https://skaile-ai.github.io/forge-editor/guides/editor-ports)).

## Let a Block type opt in

A Block type lists the capability with its config. The Block type goes through the `blockTypes` option of the composites feature ([Block types](https://skaile-ai.github.io/forge-editor/guides/block-types)).

```ts [docs/snippets/pruefung-preset.ts]
import { compositeFeature } from '@skaile/forge-editor/composites/vue'
import { type BlockType, defaultBlockTypes } from '@skaile/forge-editor/composites/schema'
import { coreContentFeature, coreUiFeature, createTranslator, defineEditor } from '@skaile/forge-editor/core'
import { pruefungFeature } from '@skaile/forge-editor/pruefung'

// A Block type opts into the capability and configures it.
const contract: BlockType = {
  id: 'contract',
  label: 'Vertrag',
  icon: 'i-lucide-file-signature',
  capabilities: [
    { id: 'container', config: { accepts: 'all' } },
    { id: 'pruefung', config: { kinds: ['redaktionell', 'fachlich'] } },
  ],
}

const blockTypes = [...defaultBlockTypes(createTranslator('de')), contract]

// A Block type that lists a capability no Feature declares throws when the editor is created; so does a block action that
// `requires` an undeclared id, when the actions are collected.
export const preset = defineEditor({
  features: [
    coreContentFeature(),
    coreUiFeature(),
    compositeFeature({ blockTypes }),
    pruefungFeature(),
  ],
})
```

A Block that does not have the capability gets no „Prüfung eintragen“ button. A Typed block of the type `contract` gets it, with the config `{ kinds: ['redaktionell', 'fachlich'] }` in `capabilityConfigOf`.

## Read a capability at runtime

All of these take the editor, because the answer depends on the editor's own Block types:

<table>
<thead>
  <tr>
    <th>
      Function
    </th>
    
    <th>
      Returns
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        capabilitiesOf(editor, node)
      </code>
    </td>
    
    <td>
      the ids of the capabilities <code>
        node
      </code>
      
       has (<code>
        ReadonlySet<string>
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        capabilitiesAt(editor, pos)
      </code>
    </td>
    
    <td>
      the same for the Block at <code>
        pos
      </code>
      
      ; empty where no node starts
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        capabilityConfigOf(editor, node, id)
      </code>
    </td>
    
    <td>
      the config the Block type gives the capability, or <code>
        undefined
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        capabilityDataOf(editor, node, id)
      </code>
    </td>
    
    <td>
      what <code>
        read
      </code>
      
       returns, or <code>
        null
      </code>
      
       when the Block lacks the capability or holds nothing
    </td>
  </tr>
</tbody>
</table>

They are exported from `@skaile/forge-editor/core` and run without a framework. The preset keeps the merged registry as `preset.capabilities`; pass it to `parseBlockTypes` as `registry` when a Block type file must name your capabilities ([Block types](https://skaile-ai.github.io/forge-editor/guides/block-types#block-types-as-json)).

## Keep a capability small

A capability stays a descriptor plus a storage contract. If it needs a node, a node view or a settings tab, that belongs to a Feature (`extensions`, `overlays`, `settingsTabs`), and the capability only says that the Block has it. Blocks that are no Typed blocks can have a capability too, but a host block that wants `title` or `properties` must declare the chrome and the `meta` attribute itself: the capability tells the UI what to expect, not how to build the node.


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
