# Developer Guide

Source: https://dev.arcxp.com/custom-content/core-concepts/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`, `is_required`, `enum_values`, and [validation rules](https://dev.arcxp.com/custom-content/reference/structured-content-field-types#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

```bash
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 .
```

```json
{
  "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:

```json
"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 holding exactly one index kind. 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                     |
| array of enum (multi-select)                     | keyword               | keyword                 |
| rich-text, reference, object, other array fields | not indexable         | not 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](https://dev.arcxp.com/custom-content/reference/limits-and-quotas-for-structured-content-types)); `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:

```json
{
  "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](https://dev.arcxp.com/custom-content/reference/structured-content-field-types#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.
