Templates
Templates are the content schema for your organization. Each template defines what fields a content item has, how they’re organized, what values are valid, and what contributors see in the editor. Templates are JSON documents that you define in your FlareBuilder workspace.
Structure
Every template is a JSON document with a top-level title field and a sections array. Sections hold fields. Fields hold data.
{ "schemaVersion": "10.0", "name": "Blog Post", "description": "Standard article format", "title": { "label": "Post Title", "props": { "placeholder": "Enter a title...", "color": "#e8ca93" }, "validations": { "required": { "value": true, "message": "A title is required" }, "maxLength": { "value": 256, "message": "Title cannot exceed 256 characters" } } }, "sections": [ { "id": "content", "label": "Content", "config": { "color": "#3F51B5" }, "fields": [ { "id": "summary", "label": "Summary", "type": "multiline", "description": "A one or two sentence description shown in feeds" }, { "id": "body", "label": "Body", "type": "richText" } ] } ]}Sections
Sections group related fields visually and semantically. Each section has an id, a label, and a fields array.
{ "id": "event_details", "label": "Event Details", "config": { "color": "#4CAF50", "collapsible": true }, "conditionals": { "visibility": { "type": "rule", "field": "is_event", "operator": "in", "value": [true] } }, "fields": [ ... ]}| Property | Required | Description |
|---|---|---|
id | Yes | Lowercase alphanumeric and underscores, unique across the template |
label | Yes | Display name in the editor (1–100 characters) |
config.color | No | Hex color for visual distinction (e.g. #3F51B5) |
config.collapsible | No | If true, the section can be collapsed in the editor |
conditionals.visibility | No | Condition AST node: hide the entire section based on field values (see Conditionals) |
Fields
Fields are the individual data inputs within a section. Every field has at minimum id, label, and type.
{ "id": "registration_url", "label": "Registration Link", "type": "link", "description": "Where attendees sign up", "props": { "defaultValue": "https://" }, "constraints": { "required": { "value": true, "message": "A registration link is required" } }}Field Types
| Type | Description | Key constraint rules |
|---|---|---|
text | Single-line text | minLength, maxLength, pattern, and optional helper |
multiline | Multi-line text | minLength, maxLength |
link | URL input | pattern, and optional helper |
richText | Rich text editor (see Rich Text Output) | minLength, maxLength |
contentJson | Structured rich text (JSON) | required |
image | Image URL | allowedTypes, maxItems |
media | File attachment | allowedTypes, maxItems, minItems |
boolean | True/false toggle | required |
integer | Whole number | min, max |
decimal | Decimal number | min, max, precision |
date | Date picker | min, max |
datetime | Date and time picker | min, max |
select | Dropdown selection | required |
location | Lat/lon/altitude coordinates | altMin, altMax |
reference | Reference to another content item | — |
group | A set of nested fields (see Groups) | minItems, maxItems |
oneOf | Polymorphic field: contributor picks a type and fills its fields (see One-of Fields) | required |
Filtering and Search Indexes
To make content filterable via the Feed API — by date range, location, or full-text search — declare an indexes map at the template level. The indexes map tells FlareBuilder which fields to extract into indexed columns when content is saved.
{ "schemaVersion": "10.0", "name": "Event", "indexes": { "_event_start": "schedule.eventStart", "_event_end": "schedule.eventEnd", "_geo": "location.venueCoords", "_search_text": ["overview.description", "speakers.speakerName"] }, "title": { ... }, "sections": [ ... ]}Each key in indexes maps to one or more field paths in sectionId.fieldId format:
| Index key | Feed API filter | Required field type |
|---|---|---|
_event_start | event_start / event_end | datetime |
_event_end | event_start / event_end | datetime |
_geo | geobox | location |
_search_text | q (full-text search) | text or multiline (array of paths) |
Constraint Rules
All constraint rules follow the same shape: an object with value and message. Place them in the field’s constraints object.
"constraints": { "required": { "value": true, "message": "This field is required" }, "maxLength": { "value": 500, "message": "Maximum 500 characters" }, "minLength": { "value": 10, "message": "At least 10 characters" }, "pattern": { "value": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", "message": "Must be a valid email address" }, "min": { "value": 0, "message": "Must be 0 or greater" }, "max": { "value": 100, "message": "Must be 100 or less" }, "precision": { "value": 2, "message": "Maximum 2 decimal places" }, "allowedTypes": { "value": ["jpg", "png", "webp"], "message": "Images only" }, "minItems": { "value": 1, "message": "At least one item required" }, "maxItems": { "value": 5, "message": "Maximum 5 items" }, "altMin": { "value": 0, "message": "Altitude cannot be negative" }, "altMax": { "value": 8848, "message": "Altitude seems too high" }}Built-in validation presets are available for common pattern values:
| Preset | Matches |
|---|---|
| Email address | user@example.com |
| URL / website | https://example.com |
| US phone number | (555) 123-4567 |
| US ZIP code | 12345 or 12345-6789 |
| IPv4 address | 192.168.1.1 |
| Letters only | ABCabc |
| Alphanumeric | ABC123 |
Field Options
| Property | Description |
|---|---|
description | Help text shown below the field in the editor |
placeholder | Placeholder text shown inside empty inputs |
props.defaultValue | Default value pre-filled when creating new content. A reset button appears when the value differs from this. |
Rich Text Output
The richText field type supports a props.outputFormat option that controls how the field value is delivered in Feed and Content API responses.
props.outputFormat | Description |
|---|---|
html (default) | The ProseMirror document is rendered to an HTML string |
json | The raw ProseMirror JSON document is returned as-is |
Set outputFormat in the field’s props:
{ "id": "body", "label": "Body", "type": "richText", "props": { "outputFormat": "html" }}HTML output (default):
{ "body": "<p>Hello <strong>world</strong></p>"}JSON output — returns the ProseMirror document structure, useful for client-side rendering with TipTap, ProseMirror, or custom renderers:
{ "body": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Hello " }, { "type": "text", "marks": [{ "type": "bold" }], "text": "world" } ] } ] }}Groups
Groups are fields with type: "group". They contain a nested fields array and can be made repeatable.
Non-repeatable Group
A non-repeatable group is a logical grouping of fields presented together in the editor.
{ "id": "contact", "label": "Contact", "type": "group", "props": { "inline": true }, "fields": [ { "id": "contact_name", "label": "Name", "type": "text" }, { "id": "contact_email", "label": "Email", "type": "text", "constraints": { "pattern": { "value": "^[^@]+@[^@]+\\.[^@]+$", "message": "Must be a valid email" } } } ]}Setting props.inline: true renders the group’s fields side-by-side rather than stacked.
Repeatable Group
Setting repeatable: true makes the group repeatable. Contributors can add, remove, and reorder entries. Add constraints.minItems / constraints.maxItems to set bounds.
{ "id": "speakers", "label": "Speakers", "type": "group", "repeatable": true, "constraints": { "minItems": { "value": 1, "message": "At least one speaker is required" }, "maxItems": { "value": 10, "message": "Maximum 10 speakers" } }, "fields": [ { "id": "speaker_name", "label": "Name", "type": "text", "constraints": { "required": { "value": true, "message": "Speaker name is required" } } }, { "id": "speaker_bio", "label": "Bio", "type": "multiline" }, { "id": "speaker_photo", "label": "Photo", "type": "image" } ]}In the Feed API response, repeatable groups appear as an array:
{ "id": "speakers", "label": "Speakers", "data": { "speakers": [ { "speaker_name": "Alice Chen", "speaker_bio": "...", "speaker_photo": "https://..." }, { "speaker_name": "Bob Torres", "speaker_bio": "...", "speaker_photo": null } ] }}Group constraints:
- Groups cannot contain other groups (one level of nesting only)
- All nested field IDs must be unique within the template
One-of Fields
A oneOf field is a polymorphic field that lets contributors choose one of several predefined structures. Each option is a type template — a regular template marked with "is_type": true. The contributor picks a type, then fills in that type’s fields inline.
Defining a Type Template
Mark a template as a type by setting is_type at the top level:
{ "schemaVersion": "10.0", "name": "Video Embed", "is_type": true, "title": { "label": "Caption" }, "sections": [ { "id": "embed", "label": "Embed", "fields": [ { "id": "url", "label": "Video URL", "type": "link" }, { "id": "autoplay", "label": "Autoplay", "type": "boolean" } ] } ]}Type templates are not content templates — they define a reusable structure, not a standalone content type.
Using a One-of Field
Reference type templates by their ID in props.types:
{ "id": "media_block", "label": "Media", "type": "oneOf", "props": { "types": ["<video-embed-template-id>", "<image-gallery-template-id>"] }}props.types is required and must contain at least one type template ID. All referenced templates must have is_type: true.
Feed API Output
A one-of field stores the chosen type ID and all its field values as a flat object under _type:
{ "media_block": { "_type": "<video-embed-template-id>", "url": "https://youtube.com/watch?v=...", "autoplay": false }}One-of constraints:
props.typesis required (at least one type template ID)- Type templates cannot contain
oneOffields themselves (no recursion)
Conditionals
Sections, fields, groups, and oneOf fields can be conditionally shown or hidden based on the value of another field in the same section.
Set conditionals.visibility on the element. The value is a condition AST node — either a single rule or a ruleset combining multiple rules with && / ||.
Single Rule
"conditionals": { "visibility": { "type": "rule", "field": "field_id_to_watch", "operator": "in", "value": ["yes", "maybe"] }}Compound Ruleset
Combine multiple rules with && (all must match) or || (any must match):
"conditionals": { "visibility": { "type": "ruleset", "combinator": "&&", "children": [ { "type": "rule", "field": "is_public", "operator": "equals", "value": true }, { "type": "rule", "field": "status", "operator": "notEquals", "value": "draft" } ] }}Rulesets can nest up to 3 levels deep.
Operators
| Operator | Value required | Behavior |
|---|---|---|
exists | No | Show when the field has any non-empty value (not null, "", or []) |
notExists | No | Show when the field is empty or null |
in | Array | Show when the field value matches any item in the array |
equals | Any | Show when the field value equals the given value (type-coerced) |
notEquals | Any | Show when the field value does not equal the given value |
contains | String | Show when the field value contains the word (case-insensitive, word boundaries) |
notContains | String | Show when the field value does not contain the word |
startsWith | String | Show when the field value starts with the given string (case-insensitive) |
lessThan | Number | Show when the field value is less than the given number |
greaterThan | Number | Show when the field value is greater than the given number |
Examples
Show a “Details” section only when a checkbox is enabled:
{ "id": "details", "label": "Details", "conditionals": { "visibility": { "type": "rule", "field": "has_details", "operator": "exists" } }, "fields": [ ... ]}Show event fields only for specific content types:
{ "id": "event_info", "label": "Event Info", "conditionals": { "visibility": { "type": "rule", "field": "content_type", "operator": "in", "value": ["in-person", "hybrid"] } }, "fields": [ ... ]}Show an “Additional Guests” group only when guest count exceeds 1:
{ "id": "additional_guests", "label": "Additional Guests", "type": "group", "conditionals": { "visibility": { "type": "rule", "field": "guest_count", "operator": "greaterThan", "value": 1 } }, "fields": [ ... ]}Show scheduling fields only when the event is public AND not a draft:
{ "id": "scheduling", "label": "Scheduling", "conditionals": { "visibility": { "type": "ruleset", "combinator": "&&", "children": [ { "type": "rule", "field": "is_public", "operator": "equals", "value": true }, { "type": "rule", "field": "status", "operator": "notEquals", "value": "draft" } ] } }, "fields": [ ... ]}Colors
Sections and the title field support a color hex value. Colors appear as visual identifiers in the template editor and as left-border accents on section panels in the content editor.
For sections, set config.color. For the title, set props.color:
// Section color"config": { "color": "#4CAF50" }
// Title color"props": { "color": "#e8ca93" }Color must be a 6-digit hex string: #RRGGBB. Colors are cosmetic only — they have no effect on the Feed API response.
Helpers
link and text fields support a helper configuration. A button appears next to the field; when clicked, it fills the field with a value generated from other fields in the form.
{ "id": "map_url", "label": "Map Link", "type": "link", "helper": { "label": "Open in Google Maps", "template": "https://www.google.com/maps/search/?api=1&query={{ $locationName | @encode }}" }}Template Expression Syntax
helper.template is a plain string that may contain one or more {{ expression }} blocks. Each block is replaced with a resolved value; everything else is kept verbatim.
Reference a field value with $field_id:
{{ $locationName }}Apply transforms with | pipes:
{{ $title | @slug }}Transforms chain left-to-right. Available transforms:
| Transform | Effect |
|---|---|
@slug | Lowercase, spaces/symbols → hyphens ("Hello World" → "hello-world") |
@lower | Convert to lowercase |
@upper | Convert to uppercase |
@encode | URL-encode the value (for use inside query parameters) |
@uuid | Generate a new random UUID (ignores the source value) |
@now | Output the current ISO timestamp (ignores the source value) |
@format('pattern') | Format a date value using the given date-fns pattern. Defaults to yyyy-MM-dd when no pattern is provided. |
Pass a parameter to a function with parentheses and single quotes:
{{ @now('yyyy') }}{{ $eventStartDate | @format('MMM d, yyyy') }}Any @function can accept an optional ('arg'). Functions that don’t use the argument simply ignore it. Currently @now and @format use the parameter as a date-fns format pattern:
| Expression | Output |
|---|---|
{{ @now }} | 2026-02-16T12:00:00.000Z (full ISO timestamp) |
{{ @now('yyyy') }} | 2026 |
{{ @now('MMM d, yyyy') }} | Feb 16, 2026 |
{{ $eventStartDate | @format }} | 2025-09-15 (default yyyy-MM-dd) |
{{ $eventStartDate | @format('h:mm a') }} | 9:00 AM |
Provide a fallback default with a single-quoted literal at the end:
{{ $title | @slug | 'untitled' }}If the field is empty, the default is used instead. Fields that have a default never disable the helper button — it stays active even when those fields are empty. Fields without a default keep the button disabled until they have a value.
Examples
Generate a Google Maps search URL from the venue name:
{ "id": "map_url", "label": "Map Link", "type": "link", "helper": { "label": "Open in Google Maps", "template": "https://www.google.com/maps/search/?api=1&query={{ $locationName | @encode }}" }}Build a Wikipedia search link from the session title, falling back to a search for “FlareBuilder”:
{ "id": "reference_url", "label": "Reference Link", "type": "link", "helper": { "label": "Search Wikipedia", "template": "https://en.wikipedia.org/wiki/Special:Search?search={{ $title | @encode | 'FlareBuilder' }}" }}Generate a URL slug from the title (falls back to "untitled" when title is empty):
{ "id": "slug", "label": "URL Slug", "type": "text", "helper": { "label": "Generate from title", "template": "{{ $title | @slug | 'untitled' }}" }}Generate a unique external ID combining the slugified title and a UUID:
{ "id": "external_id", "label": "External ID", "type": "text", "helper": { "label": "Generate ID", "template": "{{ $title | @slug | 'item' }}-{{ @uuid }}" }}Generate a display-friendly date from a datetime field:
{ "id": "display_date", "label": "Display Date", "type": "text", "helper": { "label": "Format start date", "template": "{{ $eventStartDate | @format('MMMM d, yyyy') }}" }}Stamp the current year into a copyright line:
{ "id": "copyright", "label": "Copyright", "type": "text", "helper": { "label": "Set copyright year", "template": "© {{ @now('yyyy') }} All rights reserved." }}Complete Example
A template for a conference session:
{ "schemaVersion": "10.0", "name": "Conference Session", "indexes": { "_event_start": "schedule.eventStart", "_event_end": "schedule.eventEnd", "_search_text": ["overview.description"] }, "title": { "label": "Session Title", "props": { "placeholder": "e.g. Building with Cloudflare Workers", "color": "#e8ca93" }, "validations": { "required": { "value": true, "message": "Title is required" }, "maxLength": { "value": 200, "message": "Keep it under 200 characters" } } }, "sections": [ { "id": "overview", "label": "Overview", "config": { "color": "#3F51B5" }, "fields": [ { "id": "description", "label": "Abstract", "type": "multiline", "description": "A brief abstract shown in schedules and the public feed", "constraints": { "required": { "value": true, "message": "An abstract is required" }, "maxLength": { "value": 500, "message": "Keep it under 500 characters" } } }, { "id": "track", "label": "Track", "type": "select", "description": "Which conference track this session belongs to", "props": { "options": ["Infrastructure", "Developer Experience", "AI & ML", "Security"] } }, { "id": "level", "label": "Audience Level", "type": "select", "props": { "options": ["Beginner", "Intermediate", "Advanced"] } } ] }, { "id": "schedule", "label": "Schedule", "config": { "color": "#009688" }, "fields": [ { "id": "eventStart", "label": "Start Time", "type": "datetime" }, { "id": "eventEnd", "label": "End Time", "type": "datetime" }, { "id": "room", "label": "Room", "type": "text" } ] }, { "id": "speakers", "label": "Speakers", "config": { "color": "#E91E63" }, "fields": [ { "id": "speaker_list", "label": "Speaker List", "type": "group", "repeatable": true, "constraints": { "minItems": { "value": 1, "message": "At least one speaker required" }, "maxItems": { "value": 4, "message": "Maximum 4 speakers per session" } }, "fields": [ { "id": "name", "label": "Name", "type": "text", "constraints": { "required": { "value": true, "message": "Name is required" } } }, { "id": "company", "label": "Company", "type": "text" }, { "id": "bio", "label": "Bio", "type": "multiline" }, { "id": "photo", "label": "Photo", "type": "image" }, { "id": "profile_url", "label": "Profile Link", "type": "link" } ] } ] }, { "id": "recording", "label": "Recording", "config": { "color": "#607D8B", "collapsible": true }, "conditionals": { "visibility": { "type": "rule", "field": "track", "operator": "exists" } }, "fields": [ { "id": "recording_url", "label": "Recording URL", "type": "link" }, { "id": "slides_url", "label": "Slides URL", "type": "link" } ] } ]}How Template Data Appears in the Feed
Content fields appear in the feed response under sections, organized by section ID:
{ "id": "abc123", "title": "Building with Cloudflare Workers", "template_name": "Conference Session", "tags": ["platform", "workers"], "date_published": "2025-06-01T00:00:00Z", "date_created": "2025-05-20T10:00:00Z", "date_expires": null, "author": { "id": "user-uuid" }, "permalink": "https://your-org.flarebuilder.com/p/abc123", "sections": [ { "id": "overview", "label": "Overview", "data": { "description": "A deep dive into building edge-native APIs...", "track": "Infrastructure", "level": "Intermediate" } }, { "id": "schedule", "label": "Schedule", "data": { "eventStart": "2025-09-15T09:00:00Z", "eventEnd": "2025-09-15T10:00:00Z", "room": "Hall B" } }, { "id": "speakers", "label": "Speakers", "data": { "speaker_list": [ { "name": "Alice Chen", "company": "Cloudflare", "bio": "Alice works on the Workers runtime...", "photo": "https://...", "profile_url": "https://linkedin.com/in/..." } ] } } ]}The recording section is absent because its conditionals.visibility condition was not met when this content was saved.