Field Types
Fields are the building blocks of your content models. When you create a model, you add fields to define what data each record can hold.
Overview
Foir recognizes a fixed set of built-in field types. Each field type has its own editor experience, validation rules, and configuration options. The string you store in a field’s type is the lowercase name of one of these types — anything else is rejected by the platform validator.
All fields support these common options:
| Option | Description |
|---|---|
| Label | Display name shown in the editor |
| Key | Unique identifier used in APIs (auto-generated from label) |
| Required | Must be filled before saving/publishing |
| Help Text | Guidance shown below the field in the editor |
| Default Value | Pre-filled value for new records |
| Queryable | Materialize the field for fast filtering/sorting (see Queryable fields) |
| Access | Per-field read/write principal policy (see Field-level access) |
The built-in field types
There are fifteen built-in field types:
| Type | Stores | Editor / widget |
|---|---|---|
text | A string (single- or multi-line) | Text input (or textarea via config.widget) |
number | A numeric value | Number input |
boolean | True/false | Checkbox |
date | A date or date-and-time | Date picker |
select | A reference to a record of another model | Record picker |
enum | A literal string token from inline options | Enum select |
json | Arbitrary JSON | JSON editor |
list | An ordered array of items | Array editor |
model | A model key (reference to a model schema) | Model picker |
reference | A reference to another record | Reference picker |
file | An uploaded file | File picker |
image | An uploaded image with metadata | Image picker |
video | An uploaded video with metadata | Video picker |
flexible | An array of structured blocks | Block editor |
richtext | Translatable rich-text prose | Rich-text editor |
In addition, any model with inline mode enabled becomes usable as a field type by its own key. See Inline Schemas.
Text Fields
Text
A string field for titles, names, descriptions, and prose. By default it renders as a single-line input. To get a multi-line editor, set config.widget to textarea (see Widgets).
{
"key": "title",
"type": "text",
"label": "Title",
"required": true
}Multi-line:
{
"key": "excerpt",
"type": "text",
"label": "Excerpt",
"config": { "widget": "textarea" }
}Translatable: Yes
Richtext
Translatable rich-text prose — headings, bold/italic, links, lists. Stored as structured JSON. On the public API a richtext field resolves to a RichtextValue object with html, markdown, and json representations, so consumers can render without a specialized renderer.
{
"key": "body",
"type": "richtext",
"label": "Body"
}richtext is prose only — it has no inline media embeds or structured blocks. For a stack of mixed prose, media, and components, use flexible.
query {
page(naturalKey: "home") {
body { html markdown }
}
}Translatable: Yes
Widgets
Each field type has a default editor widget and, for some types, a small set of allowed alternatives. Override the widget with config.widget. The most common case is rendering a text field as a multi-line textarea:
{ "key": "summary", "type": "text", "config": { "widget": "textarea" } }text also allows a color widget. Other field types use their default widget.
Structured Content Fields
Flexible
An ordered array of structured blocks — the field type for page bodies, section stacks, and any mixed content layout. Each block is an instance of a block type: either a built-in block primitive or one of your project’s inline-mode models.
{
"key": "body",
"type": "flexible",
"label": "Body"
}On the public API a flexible field resolves to a list of a Block union. Type-narrow each block in your query:
query {
page(naturalKey: "home") {
body {
__typename
... on RichtextBlock { html markdown }
... on ImageBlock { url alt width height }
... on MediaBlock { url mediaType }
... on ReferenceBlock {
modelKey
naturalKey
record {
... on Author { name avatar { url } }
... on Product { title price }
}
}
... on Hero { headline ctaUrl }
... on FeatureGrid { features { title description icon { url } } }
}
}
}function RenderBody({ blocks }: { blocks: BodyBlock[] }) {
return (
<>
{blocks.map((b) => {
switch (b.__typename) {
case 'RichtextBlock': return <div key={b._id} dangerouslySetInnerHTML={{ __html: b.html }} />;
case 'MediaBlock': return <Media key={b._id} {...b} />;
case 'ReferenceBlock':return <Reference key={b._id} record={b.record} />;
case 'Hero': return <Hero key={b._id} {...b} />;
// ...
}
})}
</>
);
}Built-in block types
These block primitives are always available, no inline-mode model required:
RichtextBlock, ImageBlock, VideoBlock, FileBlock, TextBlock, ReferenceBlock, MediaBlock, FlexibleBlock.
Block allowlist
By default, all inline-mode models in your project (plus the built-in blocks) are available in a flexible field. Restrict to a subset with config.allowedTypes (alias: config.allowedBlockTypes):
{
"key": "body",
"type": "flexible",
"label": "Body",
"config": {
"allowedTypes": ["hero", "feature-grid", "testimonial-card"]
}
}Translatable: Variants apply at three levels (page / field / block); see Variants.
Media Fields
Image
Upload and manage images with built-in metadata support. Resolves to an ImageValue object with url, width, height, alt, blurhash, dominantColor, mimeType, fileSize, focalPoint, crop, and variants.
{
"key": "featuredImage",
"type": "image",
"label": "Featured Image"
}Features:
- Alt text — Text for accessibility
- Focal point — Set the important area for smart cropping
- Crops — Per-usage crop geometry
- Blurhash — Automatically generated placeholder hash
- Variants — Pre-tokenised size presets (
thumbnail,small,medium,large,xlarge)
Translatable: No (alt text is part of the image value)
Video
Upload videos with automatic processing. Resolves to a VideoValue object with url, thumbnailUrl, previewUrl, hlsManifestUrl, width, height, duration, blurhash, dominantColor, mimeType, and fileSize.
{
"key": "heroVideo",
"type": "video",
"label": "Hero Video"
}Features:
- Poster image — Thumbnail shown before playback
- HLS streaming — Automatic manifest generation
- Duration — Automatically detected on upload
Translatable: No
File
Any file type — PDFs, documents, spreadsheets, downloads. Resolves to a FileValue object with url, filename, mimeType, and size.
{
"key": "attachment",
"type": "file",
"label": "Attachment"
}Translatable: No
Number, Date, and Boolean Fields
Number
Numeric values for prices, quantities, ratings, and counters.
{
"key": "price",
"type": "number",
"label": "Price",
"required": true
}Translatable: No
Date
Date picker for publish dates, event dates, deadlines, and scheduling. Use the datetime-picker widget for date-and-time.
{
"key": "publishDate",
"type": "date",
"label": "Publish Date"
}Translatable: No
Boolean
Yes/no toggle for featured flags, visibility settings, and switches.
{
"key": "isFeatured",
"type": "boolean",
"label": "Featured"
}Translatable: No
Selection Fields
Select (record-backed)
Choose a value from the records of another model. select resolves through the dataloader to the referenced record — it is not an inline list of options. Configure it with optionModelKey (the model whose records are the choices) and, optionally, multiple for multi-select.
{
"key": "language",
"type": "select",
"label": "Language",
"config": {
"optionModelKey": "language",
"multiple": false
}
}Set multiple: true to allow multiple selections (stored as an array).
Translatable: No
Enum (inline options)
Choose from a fixed, inline list of { label, value } options. Unlike select, enum values are literal string tokens stored directly on the record — there is no related model. The storefront receives a typed string-literal union.
{
"key": "category",
"type": "enum",
"label": "Category",
"config": {
"options": [
{ "label": "News", "value": "news" },
{ "label": "Tutorial", "value": "tutorial" },
{ "label": "Case Study", "value": "case-study" }
],
"multiple": false,
"default": "news"
}
}Set multiple: true for a multi-select that stores an array of tokens.
Translatable: No
Reference and Model Fields
Reference
Link to another record. Use for authors, related posts, parent pages, and any content relationship.
{
"key": "author",
"type": "reference",
"label": "Author",
"config": {
"referenceTypes": ["team-member"]
}
}Config options: referenceTypes (array of model keys the field can link to)
Translatable: No
Model
Store a reference to a model schema (the model’s key), rather than a record. Useful for configuration that needs to point at a model type.
{
"key": "targetModel",
"type": "model",
"label": "Target Model"
}Translatable: No
Structured Data Fields
JSON
Arbitrary JSON data for complex or unstructured values.
{
"key": "metadata",
"type": "json",
"label": "Metadata"
}Translatable: No
List
Array of items for ordered collections.
{
"key": "bulletPoints",
"type": "list",
"label": "Bullet Points"
}Translatable: No
Queryable fields
By default, only the always-available direct columns (id, naturalKey, createdAt, etc.) and a small set of indexed fields are fast to filter and sort on. To filter or sort on a declared field, set queryable: true. The platform materializes the field into its index and auto-backfills existing records when you flip the bit.
{
"key": "status",
"type": "enum",
"label": "Status",
"queryable": true,
"config": { "options": [ ... ] }
}Flip queryable on text, number, date, and boolean fields that show up in frequent filters or sorts. See Records → Filtering.
Field-level access
Each field can carry an optional access policy with read and write principal lists. Absent, a field is writable by any authenticated principal and readable per the model’s public-field rules. A non-empty write list restricts writes to the named principals; the platform enforces this on every record write path.
{
"key": "internalNotes",
"type": "text",
"label": "Internal Notes",
"access": {
"write": ["admin"]
}
}Translatable Fields Summary
Fields that support per-locale translations:
| Field Type | Translatable |
|---|---|
| Text | Yes |
| Richtext | Yes |
| Number | No |
| Boolean | No |
| Date | No |
| Select | No |
| Enum | No |
| JSON | No |
| List | No |
| Model | No |
| Reference | No |
| File | No |
| Image | No |
| Video | No |
| Flexible | Variant overrides apply (page/field/block) |
See Localization for details on how translations work.
In the Admin
Adding fields to a model
- Go to Models
- Select or create a model
- Click Add Field
- Choose the field type
- Configure options (label, required, help text, widget, type-specific settings)
- Save the model
Custom field types
You can create your own composite field types by defining a model with inline mode enabled. For example, create a “Button” model with text, URL, and style fields, then use it as a field type on any model.
See Inline Schemas for details.
Best Practices
- Mark fields as required only when truly necessary. Optional fields give content teams more flexibility.
- Use help text to guide editors on format expectations, character limits, or content guidelines.
- Choose the most specific field type for the data. Use
numberfor prices (nottext),datefor dates, andbooleanfor toggles. - Use
enumfor a fixed small set of tokens; useselectwhen the choices are themselves records you manage in another model. - Set
queryable: trueon any field you need to filter or sort by on the API. - Enable translations on fields that will need localized content. You can enable this later, but planning ahead saves migration effort.
- Use references instead of duplicating content. Link to a shared “Author” record rather than entering the author name on every post.