Skip to content

Building extensions

Block plugins

Build a custom block - what a block is, the kit you write it with, and how it gets distributed.

A block plugin adds new blocks to the editor's palette. Authors drag them onto a page, bind them to data, and style them just like the built-in blocks.

This page is the developer guide. Blocks are written in TypeScript on top of a small Plugin Development Kit (PDK) that ships with the platform. You don't need to know the editor's internals to build one - only the PDK and the block lifecycle.

For programmatic API details (the exact PDK signatures, the manifest schema, distribution endpoints), see the Developer Portal - this page covers concepts, the build/test loop, and the constraints worth designing around.


Anatomy of a block

A block plugin consists of:

  1. A manifest.json - declares the plugin and the blocks it provides (name, icon, group, description).
  2. One or more block classes - TypeScript classes built on the PDK.
  3. Templates - Liquid strings that define the rendered markup.
  4. Optional CSS - scoped styles applied inside the block.
  5. A bundle - the compiled output, served from a URL the host can fetch.

The editor loads the manifest, registers each block, and adds them to the palette.


What the PDK gives you

The PDK exposes a small surface - just enough to declare a placeable, bindable element:

  • A class decorator to register the element as a block (with metadata: icon, palette group, description).
  • A property decorator to declare each editable property - its type, default, validation, and binding behaviour. Every property is automatically bindable to literals, variables, query results, or Liquid expressions in the editor.
  • An event decorator for blocks that emit typed events (rarely needed; useful for interactive previews).
  • A change-watcher hook for reacting to property updates.
  • A lifecycle interface if your block needs setup/teardown around mounting.

That's it. There's no editor API, no store access, no global state. A block is a self-contained element that knows how to render itself.


Property types

Every property declares a type. The editor's Properties inspector knows how to render each type:

TypeInspector control
stringText input
numberNumeric input
booleanToggle
date, datetimeDate / datetime picker
colorColor picker
selectDropdown
assetAsset picker
dataData binding picker (query result, variables, literals)
richtextInline rich-text editor
objectNested object with typed children
arrayHomogeneous list of typed children

Every prop is bindable. The author can set a literal, reference a variable, reference a query result, or write a Liquid expression - the chip next to the property switches between these modes. The editor resolves the binding at render time and passes the resolved value to your block.

You declare the intent ("this should be a number") and the editor handles the rest.


Templates

Most blocks render with a small Liquid template:

  • The template can reference the block's resolved properties and the report's query result and variables.
  • Property values are HTML-escaped before rendering, so raw markup in a value is treated as literal text rather than parsed as HTML.
  • For dynamic markup beyond simple Liquid, the PDK lets you provide a typed render function that returns a template fragment.

Styling

Each block can ship its own scoped CSS. The CSS is applied inside the block's rendered output, so it can't accidentally leak into the rest of the report.

Inside your CSS, consume theme tokens, not raw colours. Reference var(--color-primary) instead of #0070f3. This is what makes your block look correct under whichever theme the workspace has applied - light, dark, or branded.


The build/test loop

The editor has a developer mode for iterating on a plugin locally without packaging and uploading it each cycle. Build in watch mode, point the editor's developer mode at your local build, and your blocks appear in the palette with hot-reload on change.

For unit tests, the PDK plays well with standard browser-DOM testing tools. For end-to-end tests, drive the editor with any browser automation framework. The PDK doesn't lock you into a specific tool.


Packaging and distribution

A built plugin is:

  • The compiled bundle.
  • The manifest.
  • Any referenced assets (icons, fonts).

Two paths to a customer:

  1. Marketplace publication. Submit your plugin to the Marketplace. The platform signs the bundle and serves it to customers from a CDN. Customers install through Manager → Marketplace.
  2. Private distribution. For internal-only plugins, customers can upload the bundle to their workspace's plugin registry directly.

Either way, the bundle is verified before the editor loads it. See Extensibility model for the trust model.


Versioning

Blocks declare a semver version. The editor checks compatibility when loading:

  • Major mismatch → rejected.
  • Minor mismatch (loaded > requested) → accepted; assume backward compatibility.
  • Patch mismatch → always accepted.

Be honest with major bumps - if you change a property's type or remove a property, that's a major. Adding a new optional property is a minor.


What a block plugin can't do

A few intentional limits:

  • Reach arbitrary network endpoints. Data access is mediated by the host's query layer. A block that needs server data declares a data-typed property; the author binds it to a query in the editor.
  • Read or write the platform database. Blocks are pure renderers; they have no notion of persistence.
  • Modify other blocks. Blocks don't reference siblings. Inter-block coordination happens through report variables.
  • Override the report's theme. Theme tokens are global; per-block styles override them only within the block.

These are the same isolation guarantees that make third-party blocks safe to install.


Next

Previous
Observability