Overview
Block definitions are self-contained modules that define all aspects of a form block: config schema, response schema, data transformation, UI components, UDE mapping, and analytics aggregation. This page covers the backend/implementation side - for UI patterns, see the ditto-design skill.
What it is
A BlockTypeDefinition interface that encapsulates all block behavior in one file. Type-safe config and response schemas with Zod validation.
Why we use it
Zero switch statements to add new blocks. Co-located behavior makes blocks easy to understand and maintain. AI metadata enables template generation.
When to use
Creating new form field types. Each block type gets its own folder under src/ui/blocks/dynamic-block/blocks/
Capability Presets
| Preset | Use For | Capabilities |
|---|---|---|
| FORM_FIELD_CAPABILITIES | Data collection blocks | hasTitle, hasSubtitle, hasHint, collectsData |
| HEADING_CAPABILITIES | Section headers | hasTitle, hasSubtitle, isDecorative |
| CONTENT_CAPABILITIES | Rich text display | hasTitle, hasSubtitle (body), isDecorative |
| PAGE_BREAK_CAPABILITIES | Page boundaries | isPageBreak, isDecorative |
| CONTAINER_CAPABILITIES | Group containers | hasTitle, hasSubtitle, isContainer |
Quick Start
1. Define Schemas
Define schemas for config and response data.
import { z } from 'zod';
import { faIcon } from '@fortawesome/duotone-regular-svg-icons';
import type { BlockTypeDefinition } from '../../types';
import { FORM_FIELD_CAPABILITIES } from '../../capabilities';
import { MyConfigEditor } from './config-editor';
import { MyRenderer } from '../../renderers/my-renderer';
import { aggregateMyBlock } from '@/features/resources/analytics';
// =============================================================================
// Config Schema
// =============================================================================
export const myBlockConfigSchema = z.object({
variant: z.enum(['option1', 'option2']).default('option1'),
placeholder: z.string().optional(),
maxLength: z.number().min(1).optional(),
});
export type MyBlockConfig = z.infer<typeof myBlockConfigSchema>;
// =============================================================================
// Response Schema
// =============================================================================
export const myBlockResponseSchema = z.object({
value: z.string(),
});
export type MyBlockResponse = z.infer<typeof myBlockResponseSchema>;2. Create Definition
Create the block definition with all required properties.
// =============================================================================
// Block Definition
// =============================================================================
export const myBlockDefinition: BlockTypeDefinition<MyBlockConfig, MyBlockResponse> = {
type: 'my_block',
category: 'collect',
label: 'My Block',
description: 'Short description for picker',
icon: faIcon,
capabilities: FORM_FIELD_CAPABILITIES,
configSchema: myBlockConfigSchema,
defaultConfig: {
variant: 'option1',
placeholder: '',
},
responseSchema: myBlockResponseSchema,
transformResponse: (value, block, context) => {
if (value === undefined || value === null || value === '') {
return null;
}
return { value: String(value) };
},
flattenResponse: (response) => ({
value: response.value,
}),
ConfigEditor: MyConfigEditor,
Renderer: MyRenderer,
ude: {
fieldType: 'TEXT',
columnHelper: 'text',
getValue: (response) => {
const r = response as MyBlockResponse | null;
return r?.value ?? null;
},
},
analytics: {
type: 'responses',
aggregate: (responses, config, total) =>
aggregateMyBlock(responses as MyBlockResponse[], total),
},
insights: {
contributesToScore: false,
},
ai: {
description: 'Detailed description for AI template generation.',
useCases: ['Scenario 1', 'Scenario 2'],
configExamples: [
{
scenario: 'Example scenario',
config: { variant: 'option1', placeholder: 'Enter value...' },
},
],
},
};3. Register Block
Add to BLOCK_TYPES registry.
// src/ui/blocks/dynamic-block/blocks/index.ts
import { myBlockDefinition } from './my-block/config';
export const BLOCK_TYPES = {
// ... existing blocks
my_block: myBlockDefinition,
} as const;
export function getBlockType(type: string) {
return BLOCK_TYPES[type as keyof typeof BLOCK_TYPES] ?? null;
}Patterns
Display-Only Block
Display blocks with no data collection.
// Display-only block - no responseSchema
export const headingBlockDefinition: BlockTypeDefinition<HeadingConfig> = {
type: 'heading',
category: 'share',
label: 'Heading',
description: 'Section header',
icon: faHeading,
capabilities: HEADING_CAPABILITIES,
configSchema: headingConfigSchema,
defaultConfig: { level: 'h2' },
// No responseSchema - display only
ConfigEditor: HeadingConfigEditor,
Renderer: HeadingRenderer,
// REQUIRED: explicit hidden analytics
analytics: {
type: 'hidden',
aggregate: () => ({ responseCount: 0, responseRate: 0, data: {} }),
},
};UDE Configuration
Map block responses to UDE columns.
// UDE field type mapping
ude: {
fieldType: 'TEXT', // UDE column type
columnHelper: 'text', // Column renderer helper
getValue: (response) => {
const r = response as MyBlockResponse | null;
return r?.value ?? null;
},
},
// Available fieldTypes:
// - TEXT: Free-form text
// - SINGLE_SELECT: Dropdown/radio
// - MULTI_SELECT: Checkboxes
// - NUMBER: Numeric value
// - DATE: Date/time
// - BOOLEAN: Yes/NoAnalytics Configuration
Define how responses are aggregated for analytics.
// Analytics configuration
analytics: {
type: 'metrics', // 'metrics' | 'distribution' | 'responses' | 'hidden'
aggregate: (responses, config, total) => {
const values = responses.map(r => r.value);
const avg = values.reduce((a, b) => a + b, 0) / values.length;
return {
responseCount: responses.length,
responseRate: responses.length / total,
data: {
average: avg,
min: Math.min(...values),
max: Math.max(...values),
},
};
},
},
// Analytics types:
// - metrics: Numeric aggregation (average, median, distribution)
// - distribution: Option counts (bar charts)
// - responses: Raw response list (text blocks)
// - hidden: No dashboard display (decorative blocks)Watch Out
Display blocks MUST have explicit hidden analytics
Don't
// WRONG - Missing analytics config
export const headingBlockDefinition: BlockTypeDefinition<HeadingConfig> = {
type: 'heading',
category: 'share',
// ... no analytics property
};Do
// RIGHT - Explicit hidden analytics
export const headingBlockDefinition: BlockTypeDefinition<HeadingConfig> = {
type: 'heading',
category: 'share',
analytics: {
type: 'hidden',
aggregate: () => ({ responseCount: 0, responseRate: 0, data: {} }),
},
};Collect blocks MUST have responseSchema and transformResponse
Don't
// WRONG - Collect block without responseSchema
export const textInputDefinition: BlockTypeDefinition<TextInputConfig> = {
type: 'text_input',
category: 'collect',
// Missing responseSchema!
// Missing transformResponse!
};Do
// RIGHT - Collect block with required schemas
export const textInputDefinition: BlockTypeDefinition<TextInputConfig, TextInputResponse> = {
type: 'text_input',
category: 'collect',
responseSchema: textInputResponseSchema,
transformResponse: (value, block, ctx) => {
if (!value) return null;
return { value: String(value) };
},
flattenResponse: (response) => ({ value: response.value }),
};- Type names use snake_case (text_input, not textInput or TextInput)
- Use capability presets from capabilities.ts, not manual objects
- Response schemas must be JSON-serializable (stored in ResourceSubmission)
- Include AI metadata for template generation (ai.description, useCases, configExamples)
Definition Interface
| Property | Required | Description |
|---|---|---|
| type | Yes | Unique identifier (snake_case) |
| category | Yes | share | collect | assign | verify | utility |
| configSchema | Yes | Zod schema for builder config |
| responseSchema | Collect only | Zod schema for participant input |
| transformResponse | Collect only | Form value to response |
| flattenResponse | Collect only | Response to form value |
| Renderer | Yes | Participant UI component |
| ConfigEditor | Optional | Builder settings UI |
| analytics | Yes | Dashboard aggregation config |
| ude | Optional | UDE table display config |
| ai | Optional | Template generation metadata |