---
title: "EmDash Block Kit Reference: Verified Field Names and Gotchas"
description: "The field names, required fields, enum values, and shape mismatches you won't find in EmDash docs — every entry verified against the actual TypeScript types and runtime source."
article_type: sub-pillar
canonical: https://dashstro.com/learn/emdash-block-kit-reference
---
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](/learn/emdash-plugin-pitfalls). 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.

| 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](https://github.com/SerpDelta/emdash-plugin).

## 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:

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

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

```ts
{ _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:

```bash filename=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](/learn/emdash-plugin-pitfalls).

## 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](/learn/emdash-for-developers) or [thinking about whether it's production-ready](/learn/is-emdash-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](/learn/emdash-plugin-development), 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._
