EmDash Block Kit Reference: Verified Field Names and Gotchas
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 typevalidation-DAttVLF0.js— the runtime validator, including the required-field checks and the enum setsindex.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.
| Block | Intuitive field | Actual required field | Why it bites |
|---|---|---|---|
| Stats | stats: [...] | 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 |
| Form | submit: { 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? } only | There 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.
| Field | Accepted values (full set) | What I'd assume that fails |
|---|---|---|
| CodeBlock.language | ts, tsx, jsonc, bash, css | javascript, typescript (the abbreviations are required), python, go, json |
| ButtonElement.style | primary, danger, secondary | ghost, link, outline, default |
| BannerBlock.variant | default, alert, error | info, success, warning |
| StatItem.trend | up, down, neutral | positive, 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:
# 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.