Skip to Content
API ReferenceGraphQL API

GraphQL API

The Foir GraphQL API is the primary interface for querying content, managing records, executing operations, searching, and authenticating customers. Your project’s GraphQL schema is generated from your models, so every model you define becomes directly queryable.

Schema Generation

When you create models in the Foir dashboard, the GraphQL schema is automatically generated for your project. Each model produces:

  • A singular query (e.g., page, product) that returns a single typed record
  • A plural query (e.g., pages, products) that returns a cursor-paginated connection
  • Create, update, delete, and (for versioned/publishable models) publish and unpublish mutations
  • Typed input types matching your model’s fields, including where, orderBy, and update-operator inputs

Each generated record type exposes your declared fields directly at the top level alongside a small set of underscore-prefixed system fields (_id, _naturalKey, _modelKey, _createdAt, _updatedAt, _hasDraft). There is no data wrapper.

You can explore your generated schema using the GraphiQL explorer at https://api.foir.dev/graphiql or by fetching the schema from GET https://api.foir.dev/schema.

Every query and mutation is generated per model from your schema. Each model gets its own typed fields (e.g. page, pages, createPage), and system fields are _-prefixed (_id, _metadata). These typed per-model fields are the recommended interface — use them. A generic record(id:) / records(modelKey:) pair also exists as a raw-JSON escape hatch (gated on records:read), but there is no generic recordByKey or createRecord: fetch-by-natural-key and writes go through the typed per-model fields.

Request Headers

Every request accepts these headers. Authentication is required; the others are optional opt-ins.

HeaderPurpose
Content-Type: application/jsonRequired — all requests are JSON.
x-api-key: <key>API-key authentication. Use a pk_… key for public reads, a sk_… key for writes or draft access. See Authentication.
Authorization: Bearer <token>Admin session token. Used by the admin app and the CLI; not for storefront traffic. See Authentication.
X-Foir-Schema-Channel: draftRead the draft schema and content instead of published. Requires a secret key with the drafts:read scope; silently ignored on public keys. See Schema Publishing.

To personalise reads (variants, segments), pass the contexts query argument — there is no context request header.

Responses that hit a rate limit include X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and (on 429) Retry-After — see Errors & Rate Limits.

Querying Records

Every public model gets a typed singular query (named after the model, e.g. page) and a typed plural query (the model’s plural, e.g. pages). Both return the model’s typed object: your declared fields are merged at the top level, alongside the underscore-prefixed system fields. There is no data or metadata wrapper.

Single Record

Fetch a single record by natural key (slug or handle) or by ID:

query GetPage { page(naturalKey: "homepage") { _id _naturalKey _createdAt _updatedAt title heroImage { url alt } } }
query GetPageById { page(id: "rec_abc123") { _id title } }

You must supply either naturalKey or id. The query returns null when no record matches.

List Records

The plural query returns a Relay-style cursor connection. Use where to filter, orderBy to sort, and first/after (or last/before) to paginate:

query ListProducts { products( where: { category: { eq: "electronics" }, price: { gte: 100 } } orderBy: [{ createdAt: DESC }] first: 10 ) { edges { node { _id _naturalKey title price } cursor } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount } }

To fetch the next page, pass the previous page’s pageInfo.endCursor as after.

Query Arguments

Single-record query (<model>):

ArgumentTypeDescription
naturalKeyStringNatural key / slug
idStringRecord ID
localeStringLocale for content resolution (e.g., "en-US", "fr-FR")
previewBooleanReturn draft content instead of published (requires the drafts:read scope)
contextsResolveContextInputContext dimensions for variant resolution

List query (<models>):

ArgumentTypeDescription
where<Model>WhereInputTyped filter tree (see Filtering)
orderBy[<Model>OrderByInput]Sort order (see Sorting)
firstIntForward page size
afterStringOpaque cursor — fetch rows after this position
lastIntBackward page size
beforeStringOpaque cursor — fetch rows before this position
localeStringLocale for content resolution
previewBooleanReturn draft content instead of published
contextsResolveContextInputContext dimensions for variant resolution

Content Resolution

Content resolution — variant selection, locale handling, and reference resolution — happens automatically based on the query arguments. There is no separate resolved sub-field: the resolved values are returned directly on the typed fields.

  • locale selects the localized value for translatable fields.
  • contexts drives variant matching (see Context).
  • preview: true returns the latest draft instead of the published version. This requires a secret key holding the drafts:read scope; on a publishable key the flag is rejected.
  • Reference fields resolve to the typed target record, so you project the referenced record’s fields inline (e.g. author { _id name }). Polymorphic references resolve to a union — use inline fragments (... on Author { name }).

System Fields

Every typed record exposes these underscore-prefixed system fields. They never collide with your declared fields:

FieldTypeDescription
_idString!Unique record identifier
_naturalKeyStringSlug or handle
_modelKeyString!Model this record belongs to
_createdAtDateTime!When the record was created
_updatedAtDateTime!When the record was last modified
_hasDraftBooleanWhether unpublished changes exist. Populated only for keys holding drafts:read; otherwise null.

Your declared model fields are queried directly by their own keys at the top level of the object.

Version History

For versioned or publishable models, browse version history with recordVersions. It returns a cursor connection of RecordVersion:

query GetVersions { recordVersions(parentId: "rec_abc123", first: 10) { edges { node { id versionNumber changeDescription data createdAt } } pageInfo { hasNextPage endCursor } totalCount } }

Required scope: records:read

Filtering

Each model has a generated <Model>WhereInput — a typed operator tree. Top-level fields are combined with AND. Filter on declared fields by key, plus the always-available direct columns: id, naturalKey, createdAt, updatedAt, ownerId, createdBy, updatedBy.

query FilteredProducts { products( where: { category: { eq: "electronics" } price: { gte: 100 } inStock: { eq: true } } orderBy: [{ createdAt: DESC }] ) { edges { node { _id _naturalKey title } } totalCount } }

Operators by Field Type

Operators available on a field depend on its type:

Field typeOperators
Texteq, ne, in, nin, lt, gt, lte, gte, contains, startsWith, endsWith, like, ilike, isNull, isNotNull
Numbereq, ne, in, nin, lt, gt, lte, gte, isNull, isNotNull
Dateeq, ne, in, nin, lt, gt, lte, gte, isNull, isNotNull
Booleaneq, ne, isNull, isNotNull
ID (id, ownerId, …)eq, ne, in, nin, isNull, isNotNull
Select / Enumeq, ne, in, nin, isNull, isNotNull (values are the field’s typed options)

Combining Conditions

Use AND, OR, and NOT to compose conditions at any level:

query { products( where: { OR: [ { category: { eq: "electronics" } } { category: { eq: "computers" } } ] NOT: { status: { eq: "archived" } } } ) { edges { node { _id title } } } }

List and Reference Fields

  • List fields accept { some, every, none }, each taking the item’s where shape — e.g. tags: { some: { eq: "featured" } }.
  • Reference fields accept the referenced model’s where shape directly — e.g. author: { naturalKey: { eq: "jane-doe" } }. Polymorphic references use one on<Model> branch per allowed target.

Sorting

Sort the plural query with orderBy, a list of <Model>OrderByInput. Each entry sets one field to ASC or DESC. Orderable direct columns are createdAt, updatedAt, naturalKey, and id; orderable scalar fields you declared can also be used.

query { products(orderBy: [{ createdAt: DESC }, { id: ASC }]) { edges { node { _id title } } } }

Context

Context lets you deliver personalized content based on dimensions like device, locale, or custom attributes. Pass contexts (a ResolveContextInput) to any per-model query to trigger variant matching; the resolved values come back directly on the typed fields.

query PersonalizedContent { page( naturalKey: "homepage" locale: "en-US" contexts: { device: MOBILE, platform: IOS } ) { _id title hero { heading } } }

Built-in Context Dimensions

DimensionTypeExample Values
localeString"en-US", "fr-FR"
deviceDeviceType enumDESKTOP, MOBILE, TABLET
platformPlatformType enumWEB, IOS, ANDROID

Custom Dimensions

You can define custom context dimensions in your project settings (e.g. market, userSegment, channel). Each appears as a field on ResolveContextInput, typed as an enum of its configured values when you have defined them. Customer-derived dimensions such as authentication status and segment membership are filled in server-side from the customer token and are not client inputs.

Mutations

Each public model gets typed create<Model>, update<Model>, updateMany<Model>, and delete<Model> mutations. Versioned or publishable models also get publish<Model>Version and unpublish<Model>. All mutations return the typed record (or, where noted, a payload or Boolean).

Create a Record

create<Model>(input: Create<Model>Input!) takes the model’s declared fields flattened at the top level, plus two optional system keys: naturalKey and _metadata (underscore-prefixed so it never collides with a declared metadata field). It returns the created record.

mutation CreatePage { createPage(input: { naturalKey: "about" title: "About Us" body: "Welcome to our company..." }) { _id _naturalKey _createdAt title } }

Required scope: records:write

Update a Record

update<Model>(where: <Model>WhereInput!, data: <Model>UpdateInput!) identifies the target with where (which must match exactly one record) and applies a typed operator tree in data. For versioned models, the update creates a new version automatically.

Rather than flat values, each field in <Model>UpdateInput takes an operator object:

  • Scalars (text/date/select/enum/boolean): { set } or { setNull: true }.
  • Numbers: also { increment }, { decrement }, { multiply }.
  • References: { connect: { naturalKey: "..." } }, { disconnect: true }, or { setNull: true }.
  • Lists: { set }, { push }, { pull }, { splice: { start, deleteCount, insert } }, { removeWhere }.
  • Composite (inline) fields: { update: { ... } } to patch in place, or { set: { ... } } to replace wholesale.
mutation UpdatePage { updatePage( where: { naturalKey: { eq: "about" } } data: { title: { set: "About Us -- Updated" } body: { set: "Updated content..." } } changeDescription: "Updated page title" ) { _id _updatedAt title } }

changeDescription is accepted on versioned models; variantKey is accepted on variant models to route the write to a variant child (created on demand). The direct column naturalKey is settable; id is never settable.

Required scope: records:write

Update Many Records

updateMany<Model>(where: <Model>WhereInput, data: <Model>UpdateInput!) applies the same typed update to every record matching the predicate. where is optional — omitting it matches every record in the model, so always scope it. It returns { count, ids }.

mutation MarkOnSale { updateManyProduct( where: { category: { eq: "electronics" } } data: { price: { multiply: 0.9 } } ) { count ids } }

Required scope: records:write

Atomic Field Operations

Atomic field updates are expressed through the update operator tree, not a separate operations array. They are evaluated server-side without a read-modify-write round trip:

mutation IncrementViewCount { updateArticle( where: { id: { eq: "rec_abc123" } } data: { viewCount: { increment: 1 } tags: { push: ["featured"] } lastViewed: { set: "2026-04-01T12:00:00Z" } } ) { _id viewCount tags } }
OperatorApplies toEffect
setany fieldReplace the value
setNullany fieldClear the value (set to null)
increment / decrement / multiplynumberAdjust the numeric value in place
pushlistAppend items
pulllistRemove matching items
splicelistInsert/remove at an index (start, deleteCount, insert)
removeWherelistRemove items matching the item’s where shape
connect / disconnectreferencePoint at / clear a referenced record

Delete a Record

delete<Model>(id: String!) permanently deletes a record and returns true. This cannot be undone.

mutation DeletePage { deletePage(id: "rec_abc123") }

Required scope: records:delete

Publish a Version

For versioned or publishable models, publish a specific version to make it available via live keys. Returns true on success.

mutation PublishPage { publishPageVersion(versionId: "ver_xyz789") }

Required scope: records:publish

Unpublish a Record

Remove a record from the published API. Returns true on success.

mutation UnpublishPage { unpublishPage(id: "rec_abc123") }

Required scope: records:publish

Batch Operations

batchRecordOperations processes multiple create, update, and delete operations across any models in a single request. Each operation declares its type and the fields that type needs:

mutation BatchOps { batchRecordOperations(input: { operations: [ { type: CREATE modelKey: "product" naturalKey: "blue-widget" data: { name: "Blue Widget", price: 29.99 } } { type: UPDATE modelKey: "product" id: "rec_existing123" data: { price: 24.99 } } { type: DELETE id: "rec_old456" } ] }) { created updated deleted errors { type index message } } }

modelKey is required for CREATE and UPDATE; id is required for UPDATE and DELETE. The result reports per-type counts plus an errors list with the failing operation’s index and message.

Required scope: records:write

Operations API

The Operations API lets you execute custom server-side operations from your frontend or backend. Operations are registered endpoints that extend the platform with custom logic like data processing, AI workflows, or business automation.

Prerequisites

  1. Register the operation in the dashboard with an endpoint URL
  2. Enable the API touch point on the operation
  3. Create an API key with the operations:execute scope

Execute an Operation

Each operation gets its own typed mutation named execute<Operation>, where <Operation> is the PascalCase form of the operation key. An operation with the key contact-form-handler becomes executeContactFormHandler. The input argument is a typed input object generated from the operation’s input schema.

mutation RunContactForm { executeContactFormHandler( input: { name: "Jane Doe" email: "jane@example.com" message: "I have a question about your enterprise plan..." } mode: SYNC ) { execution { id operationKey status } result } }

Arguments:

ArgumentTypeDescription
inputExecute<Operation>InputTyped input matching the operation’s declared input schema
modeExecutionModeSYNC (default) blocks until the operation completes; ASYNC returns the execution row immediately so you can poll for the result. ASYNC is only accepted by operations that support it.

The mutation returns an Execute<Operation>Payload envelope:

  • execution — the execution row (always present). Use execution.id to poll the status later.
  • result — the typed result, populated on SYNC completion and null on ASYNC dispatch. The result shape is generated from the operation’s declared output schema; operations without one expose a result: JSON field.

A SYNC operation that fails returns a GraphQL error rather than a payload.

Check Execution Status

For ASYNC operations, poll the execution status using the id returned in execution:

query GetExecution { operationExecution(id: "exec_abc123") { id operationKey status result error startedAt completedAt createdAt } }

result is JSON (the raw operation output) and error is a String message present when status is FAILED.

Execution statuses: PENDING, RUNNING, COMPLETED, FAILED, CANCELLED

Cancel an Execution

Use the generic cancelOperationExecution mutation, which returns a Boolean indicating success. It is idempotent.

mutation CancelExecution { cancelOperationExecution(id: "exec_abc123") }

List Executions

operationExecutions returns a Relay-style cursor connection. Filter by operationKey and/or status, and page with first/after.

query ListExecutions { operationExecutions( operationKey: "contact-form-handler" status: COMPLETED first: 20 ) { edges { node { id operationKey status createdAt completedAt } cursor } pageInfo { hasNextPage endCursor } totalCount } }

Polling for Results

async function waitForResult(client, executionId) { const MAX_ATTEMPTS = 30; const POLL_INTERVAL = 2000; for (let i = 0; i < MAX_ATTEMPTS; i++) { const { data } = await client.query({ query: GET_EXECUTION, variables: { id: executionId }, fetchPolicy: "network-only", }); const execution = data.operationExecution; if (execution.status === "COMPLETED") return execution.result; if (execution.status === "FAILED") throw new Error(execution.error); if (execution.status === "CANCELLED") throw new Error("Operation cancelled"); await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); } throw new Error("Timed out waiting for operation result"); }

Search and Embeddings

Foir provides search powered by vector embeddings, allowing you to find content by meaning rather than exact keywords. Search queries are available for models that have embeddings enabled in their configuration.

Search a Model

Each model with embeddings enabled gets a typed search<Model>s query (the model’s plural, e.g. searchProducts). It takes a natural-language query and returns the matching records ranked by similarity.

query SearchProducts { searchProducts(query: "lightweight shoes for trail running", first: 10) { score record { _id title } } }

Arguments:

ArgumentTypeDescription
queryString!Natural-language search query
firstIntMaximum number of matches to return (default: 10)
localeStringOptional locale for the matched records

Response fields:

FieldTypeDescription
scoreFloat!Similarity score
recordthe model’s typed objectThe matching record

Required scope: search:read:<modelKey>

Search Across Models

searchRecords runs the same similarity search and returns generic results, optionally scoped to a single model:

query SearchEverything { searchRecords(query: "trail running", modelKey: "product", first: 10) { recordId modelKey naturalKey score content } }

The result fields are recordId (String!), modelKey (String!), naturalKey (String), score (Float!), and content (JSON, the matched record’s data).

Required scope: search:read

Search by Embedding Vector

If you already have an embedding vector, searchEmbeddings finds similar records directly. findSimilarRecords does the same starting from an existing record. Both return the same generic search-result shape as searchRecords.

query SimilarByVector { searchEmbeddings(input: { embedding: [0.0123, -0.045, 0.067] modelKeys: ["product"] first: 10 }) { recordId modelKey score content } } query SimilarToRecord { findSimilarRecords(input: { recordId: "rec_abc123", modelKey: "product", first: 10 }) { recordId modelKey score content } }

SearchEmbeddingsInput fields: embedding ([Float!]!), modelKeys ([String!]), key (String), first (Int), threshold (Float), hybridWeight (Float), textQuery (String). FindSimilarRecordsInput fields: recordId (ID!), modelKey (String), first (Int).

Required scope: search:semantic:read

Read a Record’s Embeddings

embeddingsForRecord lists the stored embeddings for a record:

query RecordEmbeddings { embeddingsForRecord(recordId: "rec_abc123") { id recordId modelKey key createdAt } }

Required scope: search:semantic:read

Write and Generate Embeddings

To supply your own vectors, use writeEmbeddings; to queue server-side generation for a record, use generateEmbedding; to remove them, use deleteEmbeddings. Each returns a Boolean.

mutation StoreEmbeddings { writeEmbeddings(input: { entries: [ { recordId: "rec_abc123", embedding: [0.0123, -0.045, 0.067] } ] }) } mutation GenerateEmbedding { generateEmbedding(recordId: "rec_abc123", modelKey: "product") } mutation RemoveEmbeddings { deleteEmbeddings(input: { recordId: "rec_abc123" }) }

EmbeddingEntryInput fields: recordId (ID!), embedding ([Float!]!), key (String), contentHash (String), metadata (JSON). generateEmbedding also accepts an optional source (String).

Required scope: search:semantic:write

Customer Authentication

The Customer Auth API provides authentication flows for end-user customers, supporting password, OTP, and OAuth/OIDC methods. For the hosted Login with Foir flow (Foir hosts the login UI so you don’t rebuild it), see the Login with Foir guide.

Every token-issuing mutation returns the same CustomerAuthResult:

type CustomerAuthResult { token: String! # short-lived EdDSA access token — send as a Bearer refreshToken: String! customerId: String! email: String! }

Login with Password

mutation CustomerLogin { customerLogin(email: "customer@example.com", password: "securepassword") { token refreshToken customerId email } }

Register a Customer

mutation CustomerRegister { customerRegister(email: "new@example.com", password: "securepassword") { token refreshToken customerId email } }

Logout

Invalidates the refresh-token family server-side. Returns Boolean.

mutation CustomerLogout { customerLogout }

OTP Authentication

Request a one-time password (returns Boolean):

mutation RequestOTP { customerRequestOTP(email: "customer@example.com") }

Login with the OTP:

mutation LoginWithOTP { customerLoginOTP(email: "customer@example.com", otp: "123456") { token refreshToken customerId email } }

Token Refresh

Refresh tokens rotate on every use — save the new one before responding to the user.

mutation RefreshToken { customerRefreshToken(refreshToken: "eyJ...") { token refreshToken customerId email } }

Get Current User

Requires a valid customer access token in the Authorization header:

query CurrentUser { currentUser { id email status emailVerifiedAt createdAt } }

Password Management

Request a password reset (returns Boolean):

mutation RequestReset { customerRequestPasswordReset(email: "customer@example.com") }

Reset with the emailed token — returns a CustomerAuthResult (the customer is logged in on success):

mutation ResetPassword { customerResetPassword(token: "reset_token_here", newPassword: "newsecurepassword") { token refreshToken customerId email } }

Update password (authenticated; returns Boolean):

mutation UpdatePassword { customerUpdatePassword(currentPassword: "oldpassword", newPassword: "newpassword") }

Email Verification

Returns Boolean:

mutation VerifyEmail { customerVerifyEmail(token: "verification_token") }

Auth Providers (OAuth/OIDC)

List available authentication providers:

query ListProviders { authProviders { id key name type enabled isDefault priority } }

Login with a provider — returns a server-built redirectUrl; send the browser there:

mutation ProviderLogin { customerLoginWithProvider(input: { providerKey: "google" returnTo: "https://mysite.com/callback" }) { redirectUrl token } }

Per-call redirectUri override

Multi-environment storefronts (e.g. eide.clothing plus preview.eide.clothing) can share a single upstream OAuth client by passing redirectUri on the mutation input:

mutation ProviderLogin { customerLoginWithProvider(input: { providerKey: "shopify" redirectUri: "https://preview.eide.clothing/auth/callback?provider=shopify" }) { redirectUrl } }

The supplied URI is validated against an allowlist on the provider’s config:

  • It must equal the configured primary redirect_uri exactly, or appear in the additional_redirect_uris array.
  • It must be https://, have a host, and contain no fragment.
  • It must also be pre-registered with the upstream IdP (Shopify, Google, etc.) — that side of the allowlist is your responsibility.

If redirectUri is omitted, the provider’s primary redirect_uri is used (existing behaviour, no breaking change). If supplied but not in the allowlist, the mutation fails with InvalidArgument before the IdP is contacted, so an attacker holding an API key cannot point the OAuth flow at an arbitrary URL.

Configure additional URIs in the dashboard’s Auth Provider editor under “Additional Callback URLs” (one per line), or directly via the provider’s additional_redirect_uris config field.

Handle the OAuth callback:

mutation ProviderCallback { customerProviderCallback(input: { providerKey: "google" code: "auth_code_from_redirect" state: "state_parameter" }) { token refreshToken customerId email } }

Auth Configuration

Check what authentication methods are available for your project:

query AuthConfig { authConfig { passwordEnabled otpEnabled providers { key name type } } }

Schemas API

The Schemas API returns your project’s model and block type definitions, designed for code generation and the Foir CLI.

Required scope: schemas:read

Note: This query requires a secret API key (sk_* prefix).

query GetProjectSchemas { projectSchemas { models { id key name pluralName description fields { id key type label required isTranslatable helpText placeholder defaultValue options validation { rule value message } } } blockTypes { id key name description category schema } } }

Field Types

Common field types returned in schema definitions:

TypeDescription
textSingle-line text
richtextTranslatable rich-text prose (resolves to { html, json, markdown })
flexibleStructured array of blocks
numberNumeric value
booleanTrue/false
dateDate (can include time)
selectReference to records of an option model
enumChoice from inline { label, value } options
imageImage file
videoVideo file
fileGeneric file
jsonRaw JSON
listOrdered list of items
modelEmbedded inline model
referenceRelation to another record

Using Schemas for Code Generation

Use the schema response to generate typed interfaces for your application:

curl -X POST https://api.foir.dev/graphql \ -H "Content-Type: application/json" \ -H "x-api-key: sk_your_secret_key" \ -d '{"query": "{ projectSchemas { models { key name fields { key type required } } } }"}'

The Foir CLI provides built-in support for schema-driven code generation:

foir models list --json foir models get blog-post --json

Models Query

List the models available in your project. models returns a plain list of Model (no pagination wrapper) and accepts an optional category filter. It requires a key with the schemas:read scope.

query GetModels { models { key name pluralName pluralKey description category capabilities { isVersioned isPublishable hasVariants } fields { key label type required helpText config } } }

Each Model exposes key, name, description, pluralName, pluralKey, category, a capabilities object (isVersioned, isPublishable, hasVariants), and the model’s fields. Each field carries key, label, type, required, helpText, and a config JSON blob. Only models exposed to the public API are returned.

Secret Vault

Server-side integrations can store and read project-scoped secrets through the vault. All three fields require a SECRET (sk_) key holding the matching scope — a publishable key cannot be granted these scopes. The plaintext travels base64-encoded over the String scalar so binary secrets survive the round trip.

# Store a secret — returns an opaque ref; persist only the ref, never the plaintext mutation { putSecret(plaintextBase64: "c3VwZXItc2VjcmV0", label: "stripe-webhook-key") { ref } } # Read it back by ref query { getSecret(ref: "sec_abc123", purpose: "webhook-verify") { plaintextBase64 label lastWrittenAt } } # Soft-delete mutation { deleteSecret(ref: "sec_abc123") }
FieldScopeNotes
getSecret(ref, purpose)secrets:read.projectReturns SecretPlaintext { plaintextBase64, label, lastWrittenAt }.
putSecret(plaintextBase64, label, expiresAt)secrets:put.projectReturns PutSecretResult { ref }. Always stored project-owned.
deleteSecret(ref)secrets:delete.projectReturns Boolean!.

Files

File uploads, the File type, listing, and the lifecycle mutations (deleteFile, restoreFile, permanentlyDeleteFile) are covered in the Files API. Image transforms on File.url live in the Media API.

Type Reference

Typed record objects

Each model produces its own typed object. There is no shared generic Record type. Your declared fields appear at the top level alongside the system fields. For a Page model with title and body fields:

type Page { # System fields (on every record type) _id: String! _naturalKey: String _modelKey: String! _createdAt: DateTime! _updatedAt: DateTime! _hasDraft: Boolean # Declared fields title: String body: String }

Connection types

Plural queries return a Relay connection:

type PageConnection { edges: [PageEdge!]! pageInfo: PageInfo! totalCount: Int! } type PageEdge { node: Page! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }

RecordVersion

type RecordVersion { id: String! versionNumber: Int! changeDescription: String data: JSON createdAt: DateTime! }

Operation execute payload

Each execute<Operation> mutation returns its own Execute<Operation>Payload carrying the execution row and the typed result:

type ExecuteContactFormHandlerPayload { execution: OperationExecution! result: ContactFormHandlerOutput } type OperationExecution { id: String! operationKey: String! status: OperationExecutionStatus! result: JSON error: String startedAt: DateTime completedAt: DateTime createdAt: DateTime! }

The result field on the payload is typed from the operation’s declared output schema (operations without one expose result: JSON). OperationExecution is also the type returned by the operationExecution query and the nodes of the operationExecutions connection.

CustomerAuthResult

Returned by every token-issuing customer mutation (customerLogin, customerRegister, customerLoginOTP, customerRefreshToken, customerResetPassword, customerProviderCallback).

type CustomerAuthResult { token: String! refreshToken: String! customerId: String! email: String! }

CurrentUser

Returned by the currentUser query.

type CurrentUser { id: String! email: String! status: CustomerStatus! emailVerifiedAt: DateTime createdAt: DateTime! }
Last updated on