Skip to content

Developer Guide

Last updatedView as Markdown

Structured Content Types (SCT) lets you define custom schemas and manage records through the Arc XP Draft API and editorial interface. This guide explains the underlying data model, how SCT fits into the broader Arc XP platform, what the update constraints mean in practice, and how to think about search indexing before you build.

The data model

SCT has three core concepts: schemas, fields, and records.

A schema is a named template that defines the structure of a content type. It has a machine-readable name (e.g., calendar-event), a human-readable display_name, and an ordered list of field definitions. The name is immutable after creation.

A field defines one attribute of a record. Each field has a key (the machine identifier), a type, and optional metadata like display_name, description, is_required, enum_values, and validation rules. Field types: short-text, long-text, rich-text, date, date-time, integer, decimal, boolean, enum, geo-point, array, object, reference. The field key and type are immutable after creation.

A record (also called a document) is one instance of a schema, one event, one recipe, one person. Records are JSON objects whose keys map to the field keys defined in the schema. Required fields must be present on create; optional fields can be omitted.

Searchable (indexed) fields are fields with a search_settings object set. search_settings has two independent array properties, editorial and retrieval. We’ll explain this more in the “two search indexing dimensions” section below for what each one does. The constraint is on how many of those fields can be searchable (indexed). Your org has a shared searchable (indexed) field capacity, distributed however you like across all schemas, non-searchable fields don’t count against it. You can purchase more searchable fields capacity if you need more than your org’s limit.

SCT in the Arc XP content graph

Arc XP manages several types of content: stories, images, videos, redirects, and sections. Stories are Arc XP Native Specification (ANS) documents. SCT adds a distinct resource type to this set: structured records.

ANS is the format for story documents. It has a defined structure: headlines, content_elements, taxonomy, credits, first_publish_date, and more. Stories created in Composer conform to ANS. SCT records do not. A structured record has no content_elements, no headlines, no ANS-defined fields at all. The schema defines exactly what fields exist, and the record’s data sits in a schema_content wrapper.

What SCT shares with the rest of Arc XP:

  • The same Content API delivery infrastructure (same base URL, same authentication)
  • Content retrieval API scale and capacity remain the same, this is not a CDN caching layer. Content API responses, including schema search, are not CDN-fronted
  • The same Draft API authentication model (bearer tokens, org-scoped access)
  • The same document volume metering (records count toward your document total)
  • The same draft/publish/revision workflow. A schema record is created as a draft and appears in the Content API once it is published and circulated to a website, the same document/revision model used for story content

What is different:

  • SCT records are schema-defined, not ANS-conformant
  • SCT records are managed in Data Manager, not Composer

Think of SCT as a parallel track in the Arc content system. Stories go through Composer and ANS validation against the story schema; structured records go through Data Manager or the Draft API directly and are validated against their own schema instead, but both still move through the same draft/publish/revision workflow underneath.

Schema definition: API vs. UI

Schemas can be created, updated, and deleted either in Data Manager or through the Draft API. Both paths hit the same endpoints and produce the same object, so which one you use is a question of who is doing the work rather than what is possible.

Data Manager suits a person defining a schema once, by hand. Content Types is an admin surface: a user needs the AADM (admin) permission, globally or on any site, to see it. It shows how many schemas and indexed fields the organization is using against its limits, and it refuses changes the API would reject rather than letting you discover them at save time. GET /draft/v1/schema-usage returns those same counts and limits if you want them from a script.

The Draft API suits everything repeatable. Provisioning the same schema across sandbox, staging, and production, or scripting schema setup as part of a deployment pipeline, is API work, and POST /draft/v1/schema-type is the only way to do it without a person clicking through a form in each environment.

A schema with records cannot be deleted from either surface; the API rejects it with a 400. Delete its records first.

The name field is the schema’s unique identifier and is set once, at creation, in whichever surface you create it. It follows the pattern ^[a-z0-9]+(-[a-z0-9]+)*$, lowercase and hyphen-separated. This is the identifier your content sources will reference (schema_name: "calendar-event"). Choose it carefully. It is permanent.

Searching

Records are stored in Arc XP and exposed through the Content API. The GET /content/v5/search/schemas/ endpoint returns records by schema name, with an optional q filter on fields configured for retrieval search. Individual records are fetchable by ID at /content/v5/search/schemas/by-id/.

The q filter expression

q is not a free-text search box. It’s a structured filter expression, field:value pairs, whitespace-separated, implicit AND, that supports equality, range, and geo-distance queries against any field with search_settings.retrieval set:

  • String equality: schema_content.category:"music"
  • Number or boolean equality: schema_content.paid:true
  • Range, inclusive (≥/≤): schema_content.event_date:[2026-07-11 TO 2026-07-13]
  • Range, exclusive (>/<): schema_content.venue_capacity:{100 TO 500}
  • Open-ended range: schema_content.event_date:[2026-07-11 TO *]
  • Geo distance: schema_content.location_geo:33.77,-84.37,8km

Range syntax only works on a field whose retrieval array includes "range". Not every searchable (indexed) field supports it. Which index kinds are valid depends on the field’s type (see the table below).

Example: search by tag and date range: “events this weekend”

Say your calendar-event schema has a category field with retrieval: ["keyword"] and an event_date field with retrieval: ["range"]. Here’s one query for music events between July 11 and July 13, combining a tag filter with a date range:

Terminal window
curl -sS -G "https://api.{org}.arcpublishing.com/content/v5/search/schemas/" \
--data-urlencode "schema_name=calendar-event" \
--data-urlencode "website=demo-site" \
--data-urlencode 'q=schema_content.category:"music" schema_content.event_date:[2026-07-11 TO 2026-07-13]' \
-H "Authorization: Bearer {token}" | jq .
{
"data": [
{
"document_id": "abc123",
"schema_name": "calendar-event",
"schema_content": {
"title": "Jazz Night at the Park",
"category": "music",
"event_date": "2026-07-11T19:00:00Z",
"location": "City Park"
}
},
{
"document_id": "def456",
"schema_name": "calendar-event",
"schema_content": {
"title": "Summer Block Party",
"category": "music",
"event_date": "2026-07-12T18:00:00Z",
"location": "Main Street"
}
}
]
}

Both filters are one query, resolved server-side by OpenSearch. No overfetching, no client-side date filtering needed.

Two search indexing dimensions

This is where most teams get tripped up. A field’s search_settings object has two independent array properties (editorial and retrieval) set on the same object, in the same Draft API request:

"search_settings": {
"editorial": ["keyword"],
"retrieval": ["range"]
}

search_settings.retrieval is what makes a field usable in q filters, range queries, and geo-distance queries via the Content API. A field with no retrieval array is invisible to /content/v5/search/schemas/, no matter what else is configured on it.

search_settings.editorial is what makes a field searchable inside the Arc XP editorial interface, when editors browse and filter records in Data Manager. It’s a sibling property on the same search_settings object, not a separate configuration surface reached through a different tool. Setting retrieval on a field does not automatically set editorial, and vice versa. You configure each independently, in the same request.

Index types

Both editorial and retrieval take an array holding exactly one index kind. Valid kinds: keyword, semantic, geo, range. Which kinds are valid depends on the field’s type:

Field typeValid editorial typesValid retrieval types
boolean, enumkeywordkeyword
short-textkeyword, semantickeyword
long-textsemanticnot retrieval-indexable
date, date-time, integer, decimalrangerange
geo-pointgeogeo
array of enum (multi-select)keywordkeyword
rich-text, reference, object, other array fieldsnot indexablenot indexable

Notice long-text has no valid retrieval kind today. semantic search on long-form text is an editorial-only capability right now, not something you can query via the Content API.

Why this matters

A field can be:

  • Searchable via retrieval but not editorial: queryable via the Content API, invisible to Data Manager’s search
  • Searchable via editorial but not retrieval: searchable in Data Manager, invisible to the Content API
  • Both: the right call for fields editors need to browse and your site needs to query
  • Neither: stored and returned in record responses, but not searchable anywhere

rich-text and reference fields take no index in either dimension. That also means they use none of the organization’s searchable-field capacity (25 by default, see limits); GET /draft/v1/schema-usage reports how much of that capacity is in use, the same figure Data Manager shows in Content Types.

The mistake teams make is setting one and assuming it covers both. A category field with retrieval: ["keyword"] is filterable via q=schema_content.category:"music" in the Content API, but editors in Data Manager can’t search by category unless editorial is also set on that field.

The more important constraint: both arrays need to be planned before you create a schema with real data. Changing or removing either array is a breaking change (see below). You can index a field later, but you cannot change or remove its index. Under-indexing is recoverable; over-indexing is not. Be intentional, not generous, when setting up search on a new schema.

How structured content feeds into PageBuilder (or your experiences)

Even though the layers we’ll explain below are described for Arc XP’s experience product PageBuilder, they apply to headless experiences as well.

The architecture from schema definition to a rendered page has four layers.

Layer 1: Data Manager or Draft API

Records are created and managed here. Editors use the Data Manager UI. Automated ingestion uses POST /draft/v1/document/schema with a JSON payload containing schema_name, schema_content (the field values), and contributors. Either way, records end up in Arc XP with a system-assigned document_id and field values in schema_content.

Layer 2: Content API

Records are queryable/searchable at /content/v5/search/schemas/. The API accepts schema_name, website, pagination (size, page), and an optional q filter expression (see The q filter expression above). Only fields with search_settings.retrieval set can appear in q. The response wraps results in a data array:

{
"data": [
{
"document_id": "abc123",
"schema_name": "calendar-event",
"schema_content": {
"title": "Summer Block Party",
"event_date": "2026-07-04T18:00:00Z"
}
}
]
}

Layer 3: Content sources

Content sources are server-side modules in your PageBuilder bundle (content/sources/) that call the Content API and return data to blocks. For structured content, you typically need two: one for a list of records (calling /content/v5/search/schemas/) and one for a single record by ID (calling /content/v5/search/schemas/by-id/). The content source’s params array defines the configuration fields editors see in the PageBuilder settings panel. These can be your curation configuration you want to provide to your editorial users. Example: “get events” content source, with params: category, date range, provides same re-usable content source to be used in various pages and templates with these two configuration options by Editorial users.

Layer 4: Feature blocks

Feature blocks (i.e: components), your presentation layer in components/features/ consume data from content sources via useContent(). The data arrives with the same schema_content structure as the raw API response. Your block reads field values from item.schema_content and renders them. Editors configure the content source (schema name, size, sort) through the block’s settings panel in PageBuilder with no code changes needed to switch which schema or records a block displays.

The contentConfig prop type is what wires this together. It renders the content source picker in PageBuilder and passes the selected source and its param values to the block. A block built with contentConfig is schema-agnostic. It can display any structured content type the editor points it at, not just the one you originally built it for.

Schema update constraints

Schema fields in SCT are intentionally difficult to change once created. Understanding why makes the constraint easier to work with.

The shared OpenSearch index

All schemas in an organization share a single OpenSearch index. The index mapping is the union of every searchable (indexed) field across every schema in the org. When you create a field with search_settings.retrieval, a mapping entry is written to this shared index.

OpenSearch index mappings are append-only in practice. You can add new field mappings, but changing an existing mapping (changing a field’s data type, removing a mapping entry) either corrupts the index or requires a full reindex. Arc XP enforces schema immutability at the API layer to protect this shared structure.

This is also why a deleted schema’s name is permanently retired. The underlying index mapping fragment persists even after the schema is soft-deleted. Reusing a name would conflict with the leftover mapping.

What is immutable (what I can not change on schema after creation)

Once a schema is created:

  • name: permanent identifier, cannot be changed or reused after deletion
  • Field key: the machine identifier for each field
  • Field type: example, changing a field from short-text to date-time after the fact would corrupt the index mapping
  • A reference field’s target: reference_type, reference_schema, and the items of an array field. Changing any of them changes what existing values point at, so they are treated like a type change.

In addition:

  • Fields cannot be removed
  • is_required: false cannot become is_required: true (tightening validation on existing records)
  • A field’s search_settings.editorial or search_settings.retrieval cannot be changed or removed once set

What is mutable (what can I change after schema creation)

Always safe to change, at any time:

  • display_name and description on the schema
  • display_name and description on any field
  • is_required: true to is_required: false (relaxing validation)
  • Setting editorial or retrieval for the first time on a field that doesn’t have it (non-breaking, uses available searchable-field capacity)
  • Adding or removing enum_values
  • Adding, changing, or removing validation rules; they apply to records saved afterwards

Always safe regardless of document count:

  • Adding new fields to an existing schema

What the API rejects

Breaking change detection runs on every PATCH request. The API rejects any of the following, even if the schema has zero documents:

  • Removing a field
  • Changing a field’s type
  • Making an optional field required
  • Changing or removing search_settings.editorial or search_settings.retrieval once set
  • Changing reference_type, reference_schema, or an array field’s items

Deleting a schema that another schema’s reference field points at (reference_schema) is also rejected. The 400 names the referencing schemas; remove those fields first.

This is worth repeating: the zero-document check does not unlock these changes. Breaking changes are rejected unconditionally, regardless of whether the schema has any data.

Recovering from a schema design mistake

If you need to remove a field or change a type, the path is:

  1. Delete all documents in the schema
  2. Remove any reference fields in other schemas that point at it
  3. Delete the schema
  4. Create a new schema with a different name

The original name is permanently retired and cannot be reused; even after deletion. Plan your schema name alongside the rest of the schema structure.