Skip to main content
The nohead CLI keeps a project’s schema in a file, nohead.schema.yaml. You change the file, review what it would change, and push it, from your machine or from CI. Push applies the file through the API under the same rules as the web app. It never deletes anything, and changes that rewrite values run as field migrations only when you allow them. Install the CLI, then sign in:
Pulling needs a project API key with schema:read. Pushing needs schema:write and records:read too, on a key with Published records only off: push checks migrations and stricter field settings against the records first. Previewing migrations in diff needs the same. In CI, set NOHEAD_API_KEY instead of logging in.

Pull, diff, push

Every flag is in the CLI reference. diff and push take another file as an argument (nohead schema diff cms/schema.yaml), and pull --output writes to one (- for stdout). schema push without --yes shows the plan, changes nothing and exits with 2 (or 0 when there is nothing to change). schema diff prints one line per change:
+ adds, ~ changes, ! is a field migration and · is something only in the project. For each migration, diff also runs a preview and reports how many records would convert and how many can’t (--no-preview skips it). With --exit-code, it exits with 1 when push would change anything. That is also the exit code for an error from the API, such as a revoked key. With --json, stderr then says which: {"error": {"type": "schema_differs", …}} for a difference, any other type for an error.

The file

  • Collections are keyed by slug and fields by key, with fields in the order editors see them. A field that is only a type can be written as one: bio: long_text.
  • A field takes type, name, required, multiple and its type’s configuration as plain keys: min_length, max_length, min, max, options, format, unique, include_time, time_zone, accepted_types, description, and the search settings searchable, filterable and sortable.
  • A relation names its target collection by slug: collection: authors.
  • public_search: true includes a collection in public search.
  • A collection takes name and description. Names left out are the key’s default: blog-posts is “Blog Posts” and cover_image “Cover Image”. schema pull writes a name only when it differs, and leaves out unique on slugs, which are always unique.
The file describes each collection and field in full: a description or option you remove from it is removed from the project on the next push, and a name you remove goes back to the key’s default.

What push does

  • It never deletes. Collections and fields that aren’t in the file are left alone, so a file can cover only part of a project. Delete them in the web app or with nohead collections delete and nohead fields delete; deleted ones can be restored.
  • It keeps the file’s order. New fields are added, then the collection’s fields are put in the file’s order. A field the file leaves out stays after the field it follows now.
  • Renames need renamed_from. A new key in the file is a new field. To rename a field and keep its values, say where it came from: the old key keeps working for six months, so clients have time to move. Once pushed, renamed_from can be removed; schema pull never writes it.
  • Migrations need --allow-migrations. A new type, multiple on or off, or a date’s time on or off rewrites record values, so push refuses it unless you pass --allow-migrations. Push previews every migration before changing anything, and stops if any record can’t convert, unless you pass --clear-invalid to clear those values. It runs migrations last, one at a time, and waits for each.
  • Tighter rules are checked first. Making a field required, or options such as a lower max_length, are checked against every record before anything changes. If records would break the rule, push stops and names the field. Fix those records, or migrate the field with a backfill: nohead fields migrate blog-posts title --required --backfill Untitled.
Push applies changes one at a time through the API, so if one is refused partway (a new required field in a collection that already has records, say), the earlier ones stay. Fix the file and push again: it picks up from where the project is. Add a reason to the schema history with --note:

In CI

Keep nohead.schema.yaml in the repository and let CI apply it once a change is merged. A GitHub Actions job could look like this, with the CLI’s version pinned so that upgrading it is a change you make:
GitHub doesn’t give workflows for pull requests from forks the repository’s secrets, so on those the diff step fails without a key (exit code 2).
Leave --allow-migrations out of CI, and run migrations by hand after reviewing their preview.
A scheduled nohead schema diff --exit-code tells you when someone changed the schema in the web app without updating the file. Add --json for one JSON document on stdout instead of text.

Copy a project

nohead export and nohead import move a whole project: its schema, records, drafts and assets. Export with --include-files to bring the files along; without them, asset values are left out of the imported records. Export needs a key with Published records only off and schema:read and records:read, plus assets:read for assets; import needs schema:write, records:write and assets:write as well.
The export’s schema.yaml is the same format as nohead.schema.yaml. Import applies it the way push does, but never runs migrations: it stops if a field in the project has another type than in the export (or differs in multiple, or in a date’s time), so import into an empty project. Records start a new history there, and an interrupted import can be run again within 24 hours without duplicating records.