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:
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
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,multipleand 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 settingssearchable,filterableandsortable. - A relation names its target collection by slug:
collection: authors. public_search: trueincludes a collection in public search.- A collection takes
nameanddescription. Names left out are the key’s default:blog-postsis “Blog Posts” andcover_image“Cover Image”.schema pullwrites a name only when it differs, and leaves outuniqueon slugs, which are always unique.
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 deleteandnohead 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_fromcan be removed;schema pullnever writes it. -
Migrations need
--allow-migrations. A new type,multipleon 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-invalidto 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.
--note:
In CI
Keepnohead.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).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.
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.