Records
A record is an instance of a model. It holds your actual data — the content fields, metadata, version history, and publishing state. All record types (pages, blog posts, products, bookings) are accessed through the same unified API.
Overview
Every record has:
- Data — The content fields defined by the model’s schema (text, images, references, etc.)
- Metadata — Non-versioned properties (tags, external IDs, custom attributes)
- Natural key — An optional slug or handle for human-friendly lookups (e.g.,
about-us,homepage) - Version history — For models with versioning enabled, each edit creates a new version
- Publishing state — For models with publishing enabled, records move through a draft/publish workflow
Versioning and Publishing Workflow
For models with versioning and publishing enabled:
- Create — A new record starts as version 1 in draft state
- Edit — Each save creates a new version (version 2, 3, etc.)
- Preview — View any version before publishing
- Publish — Make a specific version live; the public API returns the published version
- Roll back — Restore a previous version at any time
For models without publishing, changes take effect immediately. For models without versioning, updates replace the current data in place.
In the Admin
Creating a record
- Navigate to the content section for the model (e.g., Pages, Blog Posts)
- Click Create New
- Fill in the fields
- Click Save Draft or Publish (for models with publishing)
Editing a record
- Select the record from the list
- Make changes in the editor
- Save as a new draft or publish directly
Version history
For versioned models, click the History tab to view all versions, compare changes, and restore previous versions.
Via the CLI
List records for a model
foir records list blog-postGet a record by ID
foir records get rec_abc123Get a record by natural key
foir records get my-first-post --model-key blog-postCreate a record
foir records create --data '{
"modelKey": "blog-post",
"naturalKey": "my-first-post",
"data": {
"title": "My First Post",
"slug": "my-first-post",
"content": "Hello world."
},
"changeDescription": "Initial creation"
}'Create and publish in one step:
foir records create --data '{...}' --publishUpdate a record
foir records update --data '{
"id": "rec_abc123",
"data": {
"title": "Updated Title"
},
"changeDescription": "Fixed the title"
}'Delete a record
foir records delete rec_abc123Publish a version
foir records publish ver_xyz789You can also publish by natural key:
foir records publish my-first-post --model-key blog-postUnpublish a record
foir records unpublish rec_abc123Create a new version
foir records create-version --data '{
"parentId": "rec_abc123",
"data": { "title": "New Draft Version" },
"changeDescription": "Reworked the intro"
}'Duplicate a record
foir records duplicate --data '{
"sourceId": "rec_abc123",
"naturalKey": "my-first-post-copy"
}'List versions
foir records versions rec_abc123List variants
foir records variants rec_abc123Create a variant
foir records create-variant --data '{
"recordId": "rec_abc123",
"variantKey": "mobile",
"name": "Mobile Version",
"data": { "title": "Mobile Title" }
}'Via the API
Queries
Get a record by ID
query {
record(id: "rec_abc123") {
id
modelKey
naturalKey
data
metadata
versionNumber
publishedVersionNumber
publishedAt
createdAt
updatedAt
}
}Get a record by natural key
query {
recordByKey(modelKey: "page", naturalKey: "about") {
id
data
metadata
publishedVersionNumber
}
}List records with filtering, sorting, and pagination
query {
records(
modelKey: "product"
filters: [
{ field: "data.category", operator: EQ, value: "electronics" }
{ field: "data.price", operator: GTE, value: 100 }
]
sort: { field: "createdAt", direction: DESC }
limit: 20
offset: 0
) {
items {
id
naturalKey
data
publishedAt
}
total
}
}Content resolution
Use the resolved field to get fully resolved content with variant selection, locale handling, and reference resolution:
query {
recordByKey(modelKey: "page", naturalKey: "homepage") {
resolved(locale: "en-US", contexts: { device: "mobile", market: "us" }) {
content
variant {
id
variantKey
}
version {
id
versionNumber
}
resolvedWith {
locale
contexts
}
}
}
}Pass preview: true to get the latest draft instead of the published version.
List versions
query {
recordVersions(parentId: "rec_abc123", limit: 10) {
items {
id
versionNumber
changeDescription
createdAt
}
}
}Mutations
Create a record
mutation {
createRecord(input: {
modelKey: "blog-post"
naturalKey: "my-first-post"
data: {
title: "My First Post"
slug: "my-first-post"
content: "Hello world."
}
changeDescription: "Initial creation"
}) {
record {
id
naturalKey
versionNumber
}
}
}Update a record
mutation {
updateRecord(input: {
id: "rec_abc123"
data: {
title: "Updated Title"
}
changeDescription: "Fixed the title"
}) {
record {
id
versionNumber
updatedAt
}
matched
}
}Publish a version
mutation {
publishVersion(versionId: "ver_xyz789")
}Unpublish a record
mutation {
unpublishRecord(id: "rec_abc123")
}Delete a record
mutation {
deleteRecord(id: "rec_abc123") {
id
modelKey
}
}Batch Operations
Process multiple create, update, and delete operations in a single request:
mutation {
batchRecordOperations(input: {
operations: [
{
type: "CREATE",
create: {
modelKey: "product",
naturalKey: "blue-widget",
data: { name: "Blue Widget", price: 29.99 }
}
},
{
type: "UPDATE",
update: {
id: "rec_existing123",
data: { price: 24.99 }
}
},
{
type: "DELETE",
delete: {
id: "rec_old456"
}
}
]
}) {
success
results {
type
index
record { id, naturalKey }
matched
}
}
}Atomic Field Operations
Instead of replacing the entire data object, use operations for surgical updates:
mutation {
updateRecord(input: {
id: "rec_abc123"
operations: [
{ op: SET, path: "lastViewed", value: "2026-02-23T12:00:00Z" }
{ op: INCREMENT, path: "viewCount", value: 1 }
]
}) {
record { data }
matched
}
}| Operation | Description | Example |
|---|---|---|
SET | Set a field value | { op: SET, path: "status", value: "active" } |
INCREMENT | Add to a numeric field | { op: INCREMENT, path: "viewCount", value: 1 } |
MIN | Set to minimum of current and new value | { op: MIN, path: "lowestPrice", value: 9.99 } |
MAX | Set to maximum of current and new value | { op: MAX, path: "highScore", value: 500 } |
APPEND | Append to an array field | { op: APPEND, path: "tags", value: "featured" } |
REMOVE | Remove from an array field | { op: REMOVE, path: "tags", value: "draft" } |
Conditional Updates
Apply updates only when specific field values match. If conditions are not met, matched returns false and the record is not modified:
mutation {
updateRecord(input: {
id: "rec_abc123"
data: { status: "confirmed" }
conditions: [
{ op: EQ, path: "status", value: "pending" }
{ op: GT, path: "quantity", value: 0 }
]
}) {
record { data }
matched
}
}Condition operators: EQ, NE, GT, GTE, LT, LTE, EXISTS, NOT_EXISTS
Filtering
Use FilterInput on list queries to narrow results:
| Operator | Description | Example Value |
|---|---|---|
EQ | Equals | "active" |
NE | Not equals | "cancelled" |
LT | Less than | 100 |
GT | Greater than | 50 |
LTE | Less than or equal | 100 |
GTE | Greater than or equal | 50 |
LIKE | Pattern match | "%shirt%" |
IN | In array | ["a", "b"] |
NOT_IN | Not in array | ["x", "y"] |
IS_NULL | Is null | null |
IS_NOT_NULL | Is not null | null |
Advanced write patterns
For high-concurrency or bulk write scenarios, the GraphQL API exposes three patterns that go beyond a plain update:
- Atomic field operations —
INCREMENT,APPEND,PREPEND,REMOVE, etc. on a single field, evaluated server-side without read-modify-write. See GraphQL → Atomic Field Operations. - Conditional updates — update a record only if the current value matches an expected one. The optimistic-concurrency primitive for state machines (
PENDING→CONFIRMEDonly if it’s stillPENDING). See GraphQL → Conditional Updates. - Batch operations — group up to N create/update/delete calls into a single
batchRecordOperationsmutation that runs as one round trip. See GraphQL → Batch Operations.
Best Practices
- Use natural keys for records that need human-friendly URLs (pages, blog posts). They make API queries and CLI lookups much simpler.
- Write meaningful
changeDescriptionvalues on each update so your version history is useful. - Use atomic field operations (
INCREMENT,APPEND, etc.) for counters and array fields to avoid race conditions. - Use conditional updates for workflows where state transitions matter (e.g., only confirm a booking if it is still pending).
- For bulk imports or migrations, use batch operations to reduce round trips.