---
title: "Block properties and Property definitions"
description: "Define Block properties with Property definitions, give them to a Block type with the properties capability and read and write them."
canonical_url: "https://skaile-ai.github.io/forge-editor/guides/block-properties"
---
# Block properties and Property definitions

> Define Block properties with Property definitions, give them to a Block type with the properties capability and read and write them.

A **Block property** is a value that a Block carries, such as a status, a due date or a link to another Block. A **Property definition** declares one: its key, its label and its value type. One definition type serves the whole editor: the `properties` capability, the Block inspector, the details popover, the table, the AI context and the server's chat schema all describe a Block property with it. The [Property definition contract](https://skaile-ai.github.io/forge-editor/reference/block-types#propertydefinition) lists the members.

## Define the properties of a Block type

A Block type lists the `properties` capability with `{ definitions }`. The example defines four Block properties for the Block type `decision` and sets one of them from code:

```ts [docs/snippets/block-properties.ts]
import type { BlockType, PropertiesConfig, PropertyDefinition } from '@skaile/forge-editor/composites/schema'
import { blockPropertiesAt, setBlockProperty } from '@skaile/forge-editor/composites'
import type { Editor } from '@tiptap/core'

// One Property definition per Block property: the key under which it is stored in `meta`, its label and its value type.
const definitions: PropertyDefinition[] = [
  { key: 'status', label: 'Status', type: 'select', options: [{ value: 'entwurf', label: 'Entwurf' }, { value: 'fertig', label: 'Fertig' }] },
  { key: 'topics', label: 'Themen', type: 'multiselect', options: [{ value: 'cost', label: 'Kosten' }, { value: 'risk', label: 'Risiko' }] },
  { key: 'owner', label: 'Zuständig', type: 'text' },
  { key: 'related', label: 'Verwandt', type: 'reference', targets: ['decision'] },
]

// The `properties` capability carries them; a Block type without it has no Block properties.
export const decision: BlockType = {
  id: 'decision',
  label: 'Entscheidung',
  icon: 'i-lucide-gavel',
  capabilities: [{ id: 'properties', config: { definitions } satisfies PropertiesConfig }],
}

/** Sets `status` on the block around the caret, in one transaction. False when no block there has Block properties. */
export function finishAtCaret(editor: Editor, blockTypes: readonly BlockType[]): boolean {
  const at = blockPropertiesAt(editor, editor.state.selection.$from, blockTypes)
  return at ? setBlockProperty(editor, at.properties, 'status', 'fertig') : false
}
```

A Block type without the `properties` capability has no Block properties: the Block inspector shows no section and the details popover shows no fields.

A `status` property says where a Block is in its work. It is no record that someone checked it: a checked state belongs in a Prüfung, which is pinned to the content of the Block and lives outside the document ([Prüfung](https://skaile-ai.github.io/forge-editor/guides/pruefung)).

<table>
<thead>
  <tr>
    <th>
      <code>
        PropertyDefinition
      </code>
      
       member
    </th>
    
    <th>
      Content
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        key
      </code>
      
      , <code>
        label
      </code>
    </td>
    
    <td>
      the key under which the Block property is stored in <code>
        meta
      </code>
      
      , and its label
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        type
      </code>
    </td>
    
    <td>
      one of the value types below
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options
      </code>
    </td>
    
    <td>
      <code>
        { value, label, group? }
      </code>
      
       choices of a <code>
        select
      </code>
      
      , <code>
        multiselect
      </code>
      
       or <code>
        status
      </code>
      
       property (required for <code>
        multiselect
      </code>
      
      ); <code>
        group
      </code>
      
       (<code>
        'todo' | 'doing' | 'done'
      </code>
      
      ) only on <code>
        status
      </code>
      
       options
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        targets
      </code>
    </td>
    
    <td>
      <code>
        reference
      </code>
      
       only: the Block type ids the Block reference may point to; absent = any Typed block
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        hidden
      </code>
    </td>
    
    <td>
      the property is stored and queried like any other, but the Block inspector, the details popover and the Block table do not show it: for a JSON value an own view edits (the query of a collection)
    </td>
  </tr>
</tbody>
</table>

The value types are the entries of `PROPERTY_TYPES` (`@skaile/forge-editor/core`):

<table>
<thead>
  <tr>
    <th>
      Type
    </th>
    
    <th>
      Stored value
    </th>
    
    <th>
      Notes
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        text
      </code>
      
      , <code>
        textarea
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      a <code>
        textarea
      </code>
      
       property gets no column in the Block table
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        date
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      formatted in the table
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      a numeric string is converted; an empty value removes the key
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        select
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      one of <code>
        options
      </code>
      
      . A <code>
        select
      </code>
      
       property with the key <code>
        status
      </code>
      
       also shows as the status chip on the title line
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        multiselect
      </code>
    </td>
    
    <td>
      <code>
        string[]
      </code>
    </td>
    
    <td>
      several of <code>
        options
      </code>
      
      , which are required
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        reference
      </code>
    </td>
    
    <td>
      block ID
    </td>
    
    <td>
      a Block reference to a Typed block of the Document; <code>
        targets
      </code>
      
       limits the Block types. It implies the <code>
        references
      </code>
      
       capability
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        bibliography
      </code>
    </td>
    
    <td>
      CSL-JSON item in <code>
        meta.bib
      </code>
    </td>
    
    <td>
      needs the <code>
        bibliography
      </code>
      
       capability; without it the definition drops out of the fields
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        derivation
      </code>
    </td>
    
    <td>
      the Derivation
    </td>
    
    <td>
      read-only
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        checkbox
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      a switch; unchecked removes the key
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        url
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      a link as text, edited in an <code>
        <input type="url">
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      one of <code>
        options
      </code>
      
       like <code>
        select
      </code>
      
      , whose options carry a <code>
        group
      </code>
      
       (<code>
        todo
      </code>
      
      , <code>
        doing
      </code>
      
      , <code>
        done
      </code>
      
      ). A property with the key <code>
        status
      </code>
      
       shows as the status chip on the title line; its icon and color follow the option's group
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        reference-list
      </code>
    </td>
    
    <td>
      <code>
        BlockAddress[]
      </code>
    </td>
    
    <td>
      several Blocks (<code>
        { documentId, blockId }
      </code>
      
      ; <code>
        documentId
      </code>
      
       <code>
        ''
      </code>
      
       = this Document). The form lists them as removable chips and adds a Typed block of the Document from a select; entries that are no valid address are dropped (<code>
        blockAddressesOf
      </code>
      
      ), and the key is removed when none is left
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        source-anchor
      </code>
    </td>
    
    <td>
      <code>
        SourceAnchor
      </code>
    </td>
    
    <td>
      a passage of a PDF: <code>
        { page, rects, quote: { exact, prefix?, suffix? } }
      </code>
      
       (<code>
        SourceAnchor
      </code>
      
      , <code>
        sourceAnchorOf
      </code>
      
      , <code>
        @skaile/forge-editor/core
      </code>
      
      ). Read-only in the form (page and quoted text); the add-on <code>
        @skaile/forge-editor-pdf-viewer
      </code>
      
       creates it from a text selection. <code>
        rects
      </code>
      
       are fractions (0 to 1) of the page; the value is kept only with a page of 1 or more and a non-empty <code>
        quote.exact
      </code>
      
      , otherwise the key is removed
    </td>
  </tr>
</tbody>
</table>

**multiselect properties.** A `multiselect` property stores its choices as a `string[]` under its key: `meta.topics = ['a', 'b']`. The details popover and the Block inspector show a multi-choice menu (a checkbox list in the React binding). An empty choice removes the key from `meta`, and entries that are not strings are dropped (`metaWithProperty`). Table cells, sort keys and the AI brief read the option labels joined by `, `, and the chat model is told to write a JSON list.

Do not declare a Property definition with a reserved `meta` key (`derivation`, `bib`, `basket`, `titleHidden`). Every Block type keeps those four itself ([Reserved meta keys](https://skaile-ai.github.io/forge-editor/guides/composites#reserved-meta-keys)). A key must not start with `$` either: `$title`, `$tags`, `$type`, `$id`, `$doc` and `$basket` are the pseudo-fields of a Block query, and the Block type config check rejects such a key.

## Property kinds

`PROPERTY_KINDS` (`@skaile/forge-editor/core`) holds one `PropertyKind` per entry of `PROPERTY_TYPES`: which filter operators apply (`ops`), the ordering key of a value (`sortKey`), the bucket it falls into when rows are grouped (`groupKey`), the aggregate functions that make sense (`aggregates`), the value as text (`text(value, definition, env)`, with `env: QueryEnv` carrying the locale and time zone), whether a cell truncates it (`longText`) and the column alignment (`align`). A query and a table read this registry instead of checking the type themselves. `statusGroupOf(definition, value)` gives the `todo`, `doing` or `done` group of a `status` value.

## Where the values live

Block properties live in the `meta` attribute of the node. Only the node of a Typed block has that attribute today, so only Typed blocks carry Block properties. The `properties` capability applies to any node that has a `meta` attribute and whose Block type lists it (`carriesProperties(node)`).

Storage is unchanged by the mechanism. In Markdown the values travel in the fence's meta JSON (`::: decision {"status":"open"}`). `changeBlockType`, conversions and the fence keep only the keys that the target Block type declares, plus the four reserved ones. A global `meta` attribute on every Block is not part of the editor: it would need a Markdown block-attribute syntax first, because the fences write `meta` only inside `::: kind {meta}`.

## The Block inspector and the other views

The Block inspector (`CompositeInspector`) shows a section „Blockeigenschaften“ (English: "Block properties") for any Block that has the `properties` capability, with one input per Property definition of its Block type. The „Dokument“ section stays the Document's own facts. The details popover, the [Block table](https://skaile-ai.github.io/forge-editor/guides/composites#block-table-and-block-inspector) and the AI brief read the same definitions. Every edit is one editor transaction, so it reaches collaborators and undo like any other edit. A Derived Block (any Block whose capabilities include `derived`) also shows its Derivation in the inspector as „Herkunft“ (English: "Origin"), with „Quelle fehlt“ ("Source missing") when the original is gone, whatever its Block type; a Block type with a `derivation` Property definition shows it once, through that definition. The model reads it as `state.derivation` (`InspectorDerivation`: `node`, `derivation`, `sourceMissing`; `null` when the Block is not Derived or the definition shows it).

An own view reads the Block properties at the caret from the framework-free model `createCompositeInspector(editor)` (`@skaile/forge-editor/composites`): `state.properties` is a `BlockProperties` (`id`, `pos`, `meta`, `definitions`), and `setField(definition, value)` writes one text value to it (the structured types `reference`, `bibliography`, `derivation`, `multiselect`, `checkbox`, `reference-list` and `source-anchor` have their own editors and are ignored). The functions below work without it:

<table>
<thead>
  <tr>
    <th>
      Function (<code>
        @skaile/forge-editor/composites
      </code>
      
      )
    </th>
    
    <th>
      Does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        carriesProperties(node)
      </code>
    </td>
    
    <td>
      whether the node type has the <code>
        meta
      </code>
      
       attribute
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        blockPropertiesAt(editor, $pos, blockTypes)
      </code>
    </td>
    
    <td>
      the innermost Block around <code>
        $pos
      </code>
      
       that carries properties, with its node, or <code>
        null
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        blockPropertiesOf(node, pos, blockTypes, capabilities)
      </code>
    </td>
    
    <td>
      the <code>
        BlockProperties
      </code>
      
       of one node
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        propertyHostOf(doc, target)
      </code>
    </td>
    
    <td>
      finds the Block again by its block ID (or its position without an ID)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        setBlockProperty(editor, target, key, value)
      </code>
    </td>
    
    <td>
      stores one value in one transaction; <code>
        false
      </code>
      
       when the Block is gone or carries no properties
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        metaWithProperty(meta, definition, value)
      </code>
    </td>
    
    <td>
      the new <code>
        meta
      </code>
      
       object; an empty value removes the key
    </td>
  </tr>
</tbody>
</table>

`setBlockProperty` finds the Block again by its block ID, so a position read before a remote change stays usable. A key that the `fields` do not define is written as text. A table row (`CompositeRow`) carries the Property definitions of its Block type as `fields`.

**Under collaboration.** `meta` is one attribute of the Block, which Yjs syncs as a whole value; two users who each write their whole `meta` at the same moment would lose one key. In a collaboration session whose backend offers `collaboration.patchProperty` (the HTTP backend), `setBlockProperty` therefore sends the one key to `POST /api/editor/documents/:id/property`, the collaboration server writes it into the open document (`metaWithProperty`, one writer) and the sync delivers it to every client: nothing is written locally, and the call returns `true` when the Block exists. When the server refuses (the document is not open there, the user is read-only, the Block is unknown), the value is written locally and the editor shows the notice „Eigenschaft konnte nicht synchron gespeichert werden“. Writes by single-actor flows (derive, outline cleanup) stay whole-`meta` writes.


## Sitemap

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