Skip to Content
FeaturesRecords

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:

  1. Create — A new record starts as version 1 in draft state
  2. Edit — Each save creates a new version (version 2, 3, etc.)
  3. Preview — View any version before publishing
  4. Publish — Make a specific version live; the public API returns the published version
  5. 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

  1. Navigate to the content section for the model (e.g., Pages, Blog Posts)
  2. Click Create New
  3. Fill in the fields
  4. Click Save Draft or Publish (for models with publishing)

Editing a record

  1. Select the record from the list
  2. Make changes in the editor
  3. 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-post

Get a record by ID

foir records get rec_abc123

Get a record by natural key

foir records get my-first-post --model-key blog-post

Create 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 '{...}' --publish

Update a record

foir records update --data '{ "id": "rec_abc123", "data": { "title": "Updated Title" }, "changeDescription": "Fixed the title" }'

Delete a record

foir records delete rec_abc123

Publish a version

foir records publish ver_xyz789

You can also publish by natural key:

foir records publish my-first-post --model-key blog-post

Unpublish a record

foir records unpublish rec_abc123

Create 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_abc123

List variants

foir records variants rec_abc123

Create 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 } }
OperationDescriptionExample
SETSet a field value{ op: SET, path: "status", value: "active" }
INCREMENTAdd to a numeric field{ op: INCREMENT, path: "viewCount", value: 1 }
MINSet to minimum of current and new value{ op: MIN, path: "lowestPrice", value: 9.99 }
MAXSet to maximum of current and new value{ op: MAX, path: "highScore", value: 500 }
APPENDAppend to an array field{ op: APPEND, path: "tags", value: "featured" }
REMOVERemove 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:

OperatorDescriptionExample Value
EQEquals"active"
NENot equals"cancelled"
LTLess than100
GTGreater than50
LTELess than or equal100
GTEGreater than or equal50
LIKEPattern match"%shirt%"
INIn array["a", "b"]
NOT_INNot in array["x", "y"]
IS_NULLIs nullnull
IS_NOT_NULLIs not nullnull

Advanced write patterns

For high-concurrency or bulk write scenarios, the GraphQL API exposes three patterns that go beyond a plain update:

  • Atomic field operationsINCREMENT, 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 (PENDINGCONFIRMED only if it’s still PENDING). See GraphQL → Conditional Updates.
  • Batch operations — group up to N create/update/delete calls into a single batchRecordOperations mutation 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 changeDescription values 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.
Last updated on