Skip to content

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": [ ... ]
}
PropertyRequiredDescription
idYesLowercase alphanumeric and underscores, unique across the template
labelYesDisplay name in the editor (1–100 characters)
config.colorNoHex color for visual distinction (e.g. #3F51B5)
config.collapsibleNoIf true, the section can be collapsed in the editor
conditionals.visibilityNoCondition 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

TypeDescriptionKey constraint rules
textSingle-line textminLength, maxLength, pattern, and optional helper
multilineMulti-line textminLength, maxLength
linkURL inputpattern, and optional helper
richTextRich text editor (see Rich Text Output)minLength, maxLength
contentJsonStructured rich text (JSON)required
imageImage URLallowedTypes, maxItems
mediaFile attachmentallowedTypes, maxItems, minItems
booleanTrue/false togglerequired
integerWhole numbermin, max
decimalDecimal numbermin, max, precision
dateDate pickermin, max
datetimeDate and time pickermin, max
selectDropdown selectionrequired
locationLat/lon/altitude coordinatesaltMin, altMax
referenceReference to another content item
groupA set of nested fields (see Groups)minItems, maxItems
oneOfPolymorphic 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 keyFeed API filterRequired field type
_event_startevent_start / event_enddatetime
_event_endevent_start / event_enddatetime
_geogeoboxlocation
_search_textq (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:

PresetMatches
Email addressuser@example.com
URL / websitehttps://example.com
US phone number(555) 123-4567
US ZIP code12345 or 12345-6789
IPv4 address192.168.1.1
Letters onlyABCabc
AlphanumericABC123

Field Options

PropertyDescription
descriptionHelp text shown below the field in the editor
placeholderPlaceholder text shown inside empty inputs
props.defaultValueDefault 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.outputFormatDescription
html (default)The ProseMirror document is rendered to an HTML string
jsonThe 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.types is required (at least one type template ID)
  • Type templates cannot contain oneOf fields 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

OperatorValue requiredBehavior
existsNoShow when the field has any non-empty value (not null, "", or [])
notExistsNoShow when the field is empty or null
inArrayShow when the field value matches any item in the array
equalsAnyShow when the field value equals the given value (type-coerced)
notEqualsAnyShow when the field value does not equal the given value
containsStringShow when the field value contains the word (case-insensitive, word boundaries)
notContainsStringShow when the field value does not contain the word
startsWithStringShow when the field value starts with the given string (case-insensitive)
lessThanNumberShow when the field value is less than the given number
greaterThanNumberShow 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": [ ... ]
}

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:

TransformEffect
@slugLowercase, spaces/symbols → hyphens ("Hello World""hello-world")
@lowerConvert to lowercase
@upperConvert to uppercase
@encodeURL-encode the value (for use inside query parameters)
@uuidGenerate a new random UUID (ignores the source value)
@nowOutput 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:

ExpressionOutput
{{ @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' }}"
}
}

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.