Skip to content
On this pageHow this reference was built
  1. How this reference was built
  2. The five most surprising block field names
  3. Enum values you probably have wrong
  4. The settingsSchema no-op
  5. Admin Block Kit and frontend Portable Text are different renderers
  6. How to verify a new block type yourself
  7. Closing

EmDash Block Kit Reference: Verified Field Names and Gotchas

Ben 6 min read

EmDash Block Kit looks enough like Slack's Block Kit that you'll assume the field names are the same. They aren't. The EmDash docs don't ship a complete reference yet. The only authoritative source is the TypeScript types and runtime validator inside node_modules/@emdash-cms/blocks — which is where every entry in this post was verified.

This is the companion to the EmDash plugin pitfalls postmortem . Read that for the story of the three bugs that made me write both posts. Bookmark this one for the field names.

How this reference was built

Every claim below was traced to one or more of:

  • validation-BG2u9jAE.d.ts — the TypeScript interface definitions for every block and element type
  • validation-DAttVLF0.js — the runtime validator, including the required-field checks and the enum sets
  • index.js — the React renderer that actually draws the blocks in the admin UI

When the docs say one thing and the source says another, the source wins. When the builder exports one shape and the validator requires another, I'll name the mismatch. If you spot a drift against a future version of @emdash-cms/blocks, the workflow at the end of this post tells you how to re-verify.

The five most surprising block field names

These are the ones that will silently break your plugin before you notice.

BlockIntuitive fieldActual required fieldWhy it bites
Statsstats: [...]items: [...]The React renderer calls block.items.map(...) — if items is missing, it crashes inside React with "Cannot read properties of undefined (reading 'map')" and your admin shows a generic CatchBoundary page
Table{ columns, rows }{ columns, rows, page_action_id }page_action_id is a required string. Omit it and validation fails even if you never wire up sort or pagination
Formsubmit: { actionId }submit: { action_id }The builder takes actionId (camelCase) and rewrites it to action_id (snake_case) before serializing. If you hand-roll the form block, it must be snake_case
Button element{ text: "Save" }{ label: "Save" }EmDash uses label, not text. Passing text is silently dropped and the button renders blank
BlockResponse redirect{ blocks, redirect }{ blocks, toast? } onlyThere is no redirect property. Plugin routes can't return HTTP redirects either — new Response(null, { status: 302 }) from a plugin route becomes {"data":{}} in the admin fetch

Enum values you probably have wrong

EmDash validates these against fixed sets. Pass a value not in the set and the validator rejects it, usually with a clear error — but it's fiddly if you're guessing.

FieldAccepted values (full set)What I'd assume that fails
CodeBlock.languagets, tsx, jsonc, bash, cssjavascript, typescript (the abbreviations are required), python, go, json
ButtonElement.styleprimary, danger, secondaryghost, link, outline, default
BannerBlock.variantdefault, alert, errorinfo, success, warning
StatItem.trendup, down, neutralpositive, negative, flat
TableColumn.format(validated against COLUMN_FORMATS set)check validation-DAttVLF0.js — the set changes across versions

The code language list is the one that bit me hardest. If you've been writing technical content and you're used to every renderer accepting javascript or python, EmDash is narrower — and you'll need to either stick to the 5 supported languages or add to the validator.

The settingsSchema no-op

definePlugin() accepts an admin.settingsSchema property declared in the core TypeScript types. You'd expect EmDash to render a settings form from this schema and store the values somewhere your plugin can read. It does neither.

Searched the @emdash-cms/admin package for references to settingsSchema: zero. No UI consumer exists. The type declaration is there, the runtime is missing.

If you want plugin settings, build the form yourself with Block Kit (a form block with secret_input / text_input / select elements) and read the values with ctx.kv.get("settings:<key>"). That's what every working EmDash plugin I've seen does, including SerpDelta's .

Admin Block Kit and frontend Portable Text are different renderers

The columns block is where this bites you. The admin Block Kit validator wants this shape:

{ type: "columns", columns: Block[][] }  // 2-D array of blocks

The frontend Portable Text renderer on a dashstro-style public site wants this shape:

{ _type: "columns", columns: [{ _type: "column", content: Block[] }] }  // wrapper objects

Both shapes are "valid columns" — in different renderers. Admin Block Kit is the plugin UI (@emdash-cms/blocks); the frontend PT renderer is what Astro's <PortableText> component uses on public pages. Passing one shape to the other will either crash or silently drop the layout.

Rule of thumb: if you're building a plugin's admin UI, read the types in @emdash-cms/blocks/dist/validation-BG2u9jAE.d.ts. If you're writing content that renders on a public Astro page, read the shapes the project's own <PortableText> component and custom components expect. Same conceptual block, two different contracts.

How to verify a new block type yourself

The source is your friend. Here's the exact workflow I use when I need to add a block I haven't touched before:

verify.sh
# 1. The TypeScript interface — field names and required/optional
grep -A10 "interface BannerBlock" \
  node_modules/@emdash-cms/blocks/dist/validation-BG2u9jAE.d.ts

# 2. The runtime validator — required-field messages + enum sets
grep -A20 "case \"banner\"" \
  node_modules/@emdash-cms/blocks/dist/validation-DAttVLF0.js

# 3. The React renderer — what the block actually draws
grep -A15 "BannerBlockComponent\|case \"banner\"" \
  node_modules/@emdash-cms/blocks/dist/index.js

Three greps, ~30 seconds, ground truth. Faster than reading a doc that might be stale, and immune to the "I was sure it said X" trap that made me ship the stats.stats typo in the first place .

Closing

This reference exists because EmDash plugin DX is at v0.1 and every early adopter discovers the same divergences the hard way. If you're evaluating EmDash as a developer or thinking about whether it's production-ready , the plugin layer is usable but unforgiving — knowing the field names up front saves hours.

If you spot a drift against a newer @emdash-cms/blocks version, re-run the three greps above. The source file names will change (validation-BG2u9jAE.d.ts is a content-hashed name) but the patterns don't — search for interface [A-Z]+Block and case "<type>": and you'll find them.

And if you're about to write your first EmDash plugin, start with the build guide , then come back here when your first hand-rolled block silently fails to render.

Verified against @emdash-cms/blocks@0.1.0, @emdash-cms/admin@0.1.0, emdash@0.1.0 on 2026-04-09. EmDash plugin DX evolves quickly at v0.1 — re-verify against current source if you're reading this in a different week.