Guides · Modelling

The model in Git

Keep a site's content model next to its code as one JSON file, reviewed in pull requests and applied on merge, with management tokens, the CLI and a GitHub workflow.

3 min read

A site's content model can live next to its code as one JSON file, reviewed in pull requests and moved from develop to production like the site itself. The file is the one Content model → Download model saves; its $schema gives editors validation and autocomplete. See the model file.

Management tokens

Create one under Settings → API tokens with the kind Management, once per environment the repository syncs (agents can create one with createApiToken, outside production). A management token:

  • belongs to one space and one environment;
  • acts as the person who created it, with their current role: it can change the model only while they are an admin or developer, and stops working when they leave the organization;
  • only works on the management API. The delivery API refuses it.

Keep it in your CI's secrets. Revoke it in Settings.

The CLI

export CONTENT_MANAGEMENT_TOKEN=cms_m_…

npx @obelism/content model pull    # write the environment's model to content/content-model.json
npx @obelism/content model plan    # what applying the file would change, and what stands in the way
npx @obelism/content model apply   # apply it in one transaction, or change nothing

plan and apply exit with 1 when the file has problems (a content type without a description, a field whose type or localization changes, a content type in use changing kind, deleting a type other types use, and so on), listing each one; apply then changes nothing. In GitHub Actions they also write the plan to the job summary. See the CLI for the options.

Locking the model to code

Once an environment's model lives in Git, a change made in the dashboard would be undone, or deleted, by the next apply. Mark the environment's model as managed in code under Settings → Publishing → Where the content model is changed, or apply with --lock (npx @obelism/content model apply --lock, --unlock gives it back):

  • the content model pages show every type read-only, with a note, and keep Export;
  • the dashboard, the agent API and MCP can't create, change, duplicate, import or delete content types there (409 model_managed_by_code);
  • content model apply (the management API) and promoting the model from another environment still work, so code-managed models move through develop, staging and production as before;
  • content is never affected: editors keep editing.

It is set per environment: a common setup edits the model in the dashboard on develop and keeps staging and production locked to the file.

A workflow

This workflow plans on pull requests (against develop) and applies on merge: develop to the develop environment, main to production. A change that removes fields says how many entries lose values before you merge it. It needs two repository secrets, a management token per environment: CONTENT_TOKEN_DEVELOP and CONTENT_TOKEN_PRODUCTION.

.github/workflows/content-model.yml
name: Content model

on:
  pull_request:
    paths: [content/content-model.json]
  push:
    branches: [develop, main]
    paths: [content/content-model.json]

jobs:
  content-model:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-node@v5
        with:
          node-version: 24

      - name: Plan
        if: github.event_name == 'pull_request'
        run: npx --yes @obelism/content model plan
        env:
          CONTENT_MANAGEMENT_TOKEN: ${{ secrets.CONTENT_TOKEN_DEVELOP }}

      - name: Apply
        if: github.event_name == 'push'
        run: npx --yes @obelism/content model apply
        env:
          CONTENT_MANAGEMENT_TOKEN: ${{ github.ref_name == 'main' && secrets.CONTENT_TOKEN_PRODUCTION || secrets.CONTENT_TOKEN_DEVELOP }}

A typical loop:

  1. Change the model in the dashboard on develop, then model pull and commit the file; or edit the file directly.
  2. Open a pull request: the plan shows what will change.
  3. Merge: the workflow applies it. Applying sends model.changed webhooks.

On this page

Marks in the margin

  • Note
  • Watch out
  • Updated

Scribes marked doubtful lines in the margin. That practice is called obelism.

Stuck on a step?