Developer Guide
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, and is_required. Field types: short-text, long-text, date, date-time, integer, decimal, boolean, enum, geo-point, array, object. 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 (ANS documents), images, videos, redirects, and sections. SCT adds a distinct resource type to this set: structured records.
ANS (Arc Native Schema) 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 way View API responses are; that caching is a View API concern, not a Content API one
- 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 goes through an explicit publish step before it circulates, 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
Schema creation is API-only. Every schema starts life through POST /draft/v1/schema-type. There’s no UI path to create a new schema, even though the code base has scaffolding that suggests one. Don’t design a workflow that assumes an editor or admin can spin up a new schema from Data Manager; they can’t.
Modifying an existing schema is a different story. Once a schema exists, updates can go through either the UI or PATCH /draft/v1/schema-type/{name}. Both paths hit the same endpoint underneath and produce the same object. The UI is a facade over the Draft API for updates, not for creation.
This means every team needs at least one Draft API call somewhere in their setup process, even if editors handle day-to-day field additions through Data Manager afterward. Teams provisioning schemas consistently across sandbox, staging, and production, or scripting schema setup as part of a deployment pipeline, will be doing all of this through the API anyway, creation and updates both.
One thing to know: the API surfaces everything on updates, too. Some field-level properties available in the API request body (like enum_values for enum fields) may not be visible or editable in the current UI version. If your schema uses enum fields or needs precise control over description and is_required settings, update via API rather than the UI.
The name field maps to the schema’s unique identifier, set once at creation via the API. It follows the pattern ^[a-z0-9]+(-[a-z0-9]+)*$ lowercase, 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:
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"]}Programmatic Retrieval: Content API search
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.
Editorial: Data Manager UI search
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 of index kinds, not a single string. Valid kinds: keyword, semantic, geo, range. Which kinds are valid depends on the field’s type:
| Field type | Valid editorial types | Valid retrieval types |
|---|---|---|
boolean, enum | keyword | keyword |
short-text | keyword, semantic | keyword |
long-text | semantic | not retrieval-indexable |
date, date-time, integer, decimal | range | range |
geo-point | geo | geo |
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
retrievalbut noteditorial: queryable via the Content API, invisible to Data Manager’s search - Searchable via
editorialbut notretrieval: 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
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. Removing a kind from either array (or removing the array itself) is a breaking change (see below). You can add kinds later, but you cannot remove them. 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/schema-type/document/schema with JSON payload of the new schema definition. 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 records exist. 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, 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 fromshort-texttodate-timeafter the fact would corrupt the index mapping
Once records exist in a schema, additional constraints apply:
- Fields cannot be removed
is_required: falsecannot becomeis_required: true(tightening validation on existing records)- An index kind cannot be removed from a field’s
search_settings.editorialorsearch_settings.retrievalarray, and neither array can be removed once set
What is mutable (what can I change after schema creation)
Always safe to change, at any time:
display_nameanddescriptionon the schemadisplay_nameanddescriptionon any fieldis_required: truetois_required: false(relaxing validation)- Adding new kinds to a field’s
editorialorretrievalarray, or setting either array for the first time on a previously non-searchable field (non-breaking, uses available searchable-field capacity)
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
- Removing a kind from
search_settings.editorialorsearch_settings.retrieval, or removing either array entirely once set
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:
- Delete all documents in the schema
- Delete the schema
- 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.