adapto:schema-apply
The gated writer of the schema pair. It reads .adapto/schema-plan.json (produced by
adapto:schema-design) and creates the Article categories and custom collections it describes via
the adapto CLI. It is idempotent and re-runnable — existing collections/categories are reused or
updated, never duplicated.
When to use
- After
adapto:schema-design produced (and you've eyeballed or hand-edited) .adapto/schema-plan.json, and
you're ready to create those collections + categories in Adapto.
- Triggers: "apply the schema", "create the collections", "build my schema in Adapto".
When not to use
- No plan file yet → run
adapto:schema-design first.
- Seeding content rows into the collections →
adapto:content-seed.
Inputs
.adapto/schema-plan.json — the approved plan (collections, categories, language). Missing/invalid →
stop and route to adapto:schema-design.
- The working tenant — confirm it explicitly; never assume the saved/active one. With
2+ tenants, have the user pick; with one, state it and proceed.
Outputs
- The plan's Article categories and custom collections created (or updated) in the CMS,
draft
unless the plan says otherwise.
- The reserved
_adapto_seo collection ensured (the CMS home for per-piece SEO metadata —
reserved-slugs.md), so adapto:content-upload can write metadata.
- A realized
.adapto/schema.json — a {"<slug>": "<id>", …} map of every collection's slug to its CMS
id (incl. _adapto_seo) — for adapto:content-upload (and adapto:content-seed) to target.
- A report: which collections/categories were created vs reused, with ids.
- Next step: suggest
adapto:content-research to start a content cycle (research → plan → create →
upload), or adapto:content-seed for a quick set of starter drafts. Both read .adapto/schema.json.
Preconditions
- Preflight with the
adapto:doctor checks.
- Hard-block on an authenticated CLI (
adapto auth me) and a selected tenant — this skill writes.
.adapto/schema-plan.json must exist (else route to adapto:schema-design).
adapto CLI >= 0.1.3.
Plan phase
Read and validate .adapto/schema-plan.json, then print a machine-parseable plan and wait for an explicit
approve:
- For each category and collection: whether it will be created or reused (resolved live via
get-by-slug), plus its fields, the target language, and draft status.
- The realized
.adapto/schema.json it will write.
- That the reserved
_adapto_seo collection will be ensured (created if absent) for content metadata.
- No cost/token figures. If the plan is empty, say so and stop — nothing to apply.
Validate before proposing: every field type is in the safe vocabulary (cheatsheet §5), and every
reference field's related_collection slug exists in the plan. Surface any problem here, before writing.
Apply phase
Runs only after approval. Deterministic CLI calls — --json on every one.
Validate the plan before the first write. Server-side rejections are per-request, so a bad field in
collection #3 still leaves collections #1–2 and every category already created — a partial apply. Catching
it locally costs nothing and keeps the run all-or-nothing. Check each field:
type is in the vocabulary (text, textarea, rich_text, number, date, date_range, boolean, select, multi_select, reference, image, file, url, email, color).
multiple: true only on text, textarea, number, date, select, reference, image, file, url, email, color. On multi_select, boolean, rich_text, or date_range the server returns
Bad request: Field <name> of type <type> cannot be multiple and the run dies mid-way.
multi_select is already multi-valued — drop the multiple key rather than adding it.
reference fields name a related_collection that exists in the plan or the CMS.
On a violation: stop before writing anything, show the offending field, and offer the fix
(drop multiple, or switch multi_select → select + multiple: true).
Resolve language. Use the plan's language if the tenant has it enabled; otherwise fall back to the
tenant's first enabled code and note the substitution. Discover with:
adapto auth orgs --json
Categories — idempotent (no --status flag on categories):
adapto categories get-by-slug <slug> --json # reuse the id on a hit
adapto categories create --name "<name>" --slug <slug> --language <lang> [--description "<desc>"] --json
Collections — TWO PASS (robust even to circular references):
- Pass 1 — create each collection with its non-reference fields only; capture each
slug → id.
On a get-by-slug hit, reuse the id (and update if fields differ):adapto collections get-by-slug <slug> --json
adapto collections create --name "<name>" --slug <slug> \
--description "<desc>" --language <lang> --status <status> \
--fields-json '<fields WITHOUT type:reference entries>' --json
- Pass 2 — add the
reference fields now that their targets exist, resolving each
related_collection slug → the real id captured in Pass 1:adapto collections update <id> --fields-json '<full fields incl. resolved reference ids>' --json
3b. Ensure the reserved _adapto_seo collection (idempotent) — the CMS home for per-piece SEO metadata
that adapto:content-upload writes and adapto:seo-wire reads (reserved-slugs.md):
adapto collections get-by-slug _adapto_seo --json # reuse the id on a hit
adapto collections create --name "Adapto SEO" --slug _adapto_seo \
--description "Per-piece SEO metadata for Adapto content" --language <lang> --status draft \
--fields-json '<the _adapto_seo field-set — reserved-slugs.md>' --json
⚠️ Reserved-slug fallback: if _adapto_ is rejected, retry once with adapto-seo; record which slug
worked. Capture its id into .adapto/schema.json alongside the user collections.
4. Report + persist. Print created-vs-reused with ids, then write .adapto/schema.json as
{"<slug>": "<id>", …} (incl. _adapto_seo) for adapto:content-upload. Loop cleanly — judge success from each call's --json,
not the shell exit code, and make the loop/function exit 0 on success so a created batch never surfaces as a
red Error: Exit code 1 (conventions.md §8). Then restart the dev server (stop→start) and keep it running so
the new collections/categories appear — never kill it (starters sync content at startup — conventions §14).
--fields-json is a FieldDefinitionModel[]. No --source — collections and categories carry no provenance.
Errors and recovery
- A create fails mid-run (partial state) → whatever was created before the failure stays. This is safe:
every step is
get-by-slug-first, so a re-run reuses those ids instead of duplicating. Report what
landed, fix the offending field in schema-plan.json, and re-run. Step 0 exists so this rarely happens.
Field <name> of type <type> cannot be multiple → multiple: true on a type that forbids it (usually
multi_select). Drop the key or use select + multiple: true, then re-run.
- Plan missing/invalid → stop; tell the user to run
adapto:schema-design.
- Server rejects a field
type → surface which collection/field, and suggest a safe-vocabulary type
(cheatsheet §5); don't retry blindly.
- A
reference's related_collection slug isn't in the plan → stop before writing; the plan is
inconsistent.
- Collection/category already exists → reuse/update via
get-by-slug; never create a duplicate.
- Partial failure mid-apply (collections have no batch — they're per-call) → report what was created so
far, then stop. Re-running is safe (idempotent).
- Not authenticated / no tenant → stop; offer both auth paths —
Log in or Register (conventions §11) — then tenant selection.
- Language discovery fails → ask the user for a language code the tenant has enabled; don't guess.
Forbidden actions
- Never write without an approved plan (plan-then-apply).
- Never pass
--source here — collections/categories have no provenance field.
- Never create built-in Articles/Pages — the plan's
advisory map is documentation only.
- Never assume the working tenant — confirm it before any write.
- Never modify the scaffolded read-client (forbidden-actions.md).
1---2name: adapto-schema-apply3description: Apply an approved .adapto/schema-plan.json to the CMS — create Article categories and custom collections (with a two-pass step for references), idempotently, via the adapto CLI. Plan-then-apply; writes content. Pairs with adapto:schema-design.4---56# adapto:schema-apply78The **gated writer** of the schema pair. It reads `.adapto/schema-plan.json` (produced by9`adapto:schema-design`) and creates the **Article categories** and **custom collections** it describes via10the `adapto` CLI. It is **idempotent and re-runnable** — existing collections/categories are reused or11updated, never duplicated.1213## When to use14- After `adapto:schema-design` produced (and you've eyeballed or hand-edited) `.adapto/schema-plan.json`, and15 you're ready to create those collections + categories in Adapto.16- Triggers: "apply the schema", "create the collections", "build my schema in Adapto".1718## When not to use19- No plan file yet → run `adapto:schema-design` first.20- Seeding content rows into the collections → `adapto:content-seed`.2122## Inputs23- **`.adapto/schema-plan.json`** — the approved plan (collections, categories, language). Missing/invalid →24 stop and route to `adapto:schema-design`.25- **The working tenant** — confirm it explicitly; never assume the saved/active one. With26 2+ tenants, have the user pick; with one, state it and proceed.2728## Outputs29- The plan's **Article categories** and **custom collections** created (or updated) in the CMS, `draft`30 unless the plan says otherwise.31- The reserved **`_adapto_seo`** collection ensured (the CMS home for per-piece SEO metadata —32 [reserved-slugs.md](../../shared/reserved-slugs.md)), so `adapto:content-upload` can write metadata.33- A realized **`.adapto/schema.json`** — a `{"<slug>": "<id>", …}` map of every collection's slug to its CMS34 id (incl. `_adapto_seo`) — for `adapto:content-upload` (and `adapto:content-seed`) to target.35- A report: which collections/categories were created vs reused, with ids.36- **Next step:** suggest **`adapto:content-research`** to start a content cycle (research → plan → create →37 upload), or **`adapto:content-seed`** for a quick set of starter drafts. Both read `.adapto/schema.json`.3839## Preconditions40- **Preflight** with the `adapto:doctor` checks.41- **Hard-block** on an authenticated CLI (`adapto auth me`) **and** a selected tenant — this skill writes.42- `.adapto/schema-plan.json` must exist (else route to `adapto:schema-design`).43- `adapto` CLI `>= 0.1.3`.4445## Plan phase46Read and validate `.adapto/schema-plan.json`, then print a machine-parseable plan and wait for an explicit47`approve`:48- For each **category** and **collection**: whether it will be **created** or **reused** (resolved live via49 `get-by-slug`), plus its fields, the **target language**, and `draft` status.50- The realized `.adapto/schema.json` it will write.51- That the reserved **`_adapto_seo`** collection will be **ensured** (created if absent) for content metadata.52- No cost/token figures. If the plan is empty, say so and stop — nothing to apply.5354Validate before proposing: every field `type` is in the safe vocabulary (cheatsheet §5), and every55`reference` field's `related_collection` slug exists in the plan. Surface any problem here, before writing.5657## Apply phase58Runs only after approval. Deterministic CLI calls — `--json` on every one.59600. **Validate the plan before the first write.** Server-side rejections are per-request, so a bad field in61 collection #3 still leaves collections #1–2 and every category already created — a partial apply. Catching62 it locally costs nothing and keeps the run all-or-nothing. Check each field:63 - `type` is in the vocabulary (`text, textarea, rich_text, number, date, date_range, boolean, select,64 multi_select, reference, image, file, url, email, color`).65 - **`multiple: true` only on** `text, textarea, number, date, select, reference, image, file, url, email,66 color`. On `multi_select`, `boolean`, `rich_text`, or `date_range` the server returns67 `Bad request: Field <name> of type <type> cannot be multiple` and the run dies mid-way.68 `multi_select` is already multi-valued — drop the `multiple` key rather than adding it.69 - `reference` fields name a `related_collection` that exists in the plan or the CMS.70 On a violation: **stop before writing anything**, show the offending field, and offer the fix71 (drop `multiple`, or switch `multi_select` → `select` + `multiple: true`).72731. **Resolve language.** Use the plan's `language` if the tenant has it enabled; otherwise fall back to the74 tenant's first enabled code and note the substitution. Discover with:75 ```bash76 adapto auth orgs --json77 ```782. **Categories** — idempotent (no `--status` flag on categories):79 ```bash80 adapto categories get-by-slug <slug> --json # reuse the id on a hit81 adapto categories create --name "<name>" --slug <slug> --language <lang> [--description "<desc>"] --json82 ```833. **Collections — TWO PASS** (robust even to circular references):84 - **Pass 1** — create each collection with its **non-reference** fields only; capture each `slug → id`.85 On a `get-by-slug` hit, reuse the id (and `update` if fields differ):86 ```bash87 adapto collections get-by-slug <slug> --json88 adapto collections create --name "<name>" --slug <slug> \89 --description "<desc>" --language <lang> --status <status> \90 --fields-json '<fields WITHOUT type:reference entries>' --json91 ```92 - **Pass 2** — add the `reference` fields now that their targets exist, resolving each93 `related_collection` slug → the real id captured in Pass 1:94 ```bash95 adapto collections update <id> --fields-json '<full fields incl. resolved reference ids>' --json96 ```973b. **Ensure the reserved `_adapto_seo` collection** (idempotent) — the CMS home for per-piece SEO metadata98 that `adapto:content-upload` writes and `adapto:seo-wire` reads ([reserved-slugs.md](../../shared/reserved-slugs.md)):99 ```bash100 adapto collections get-by-slug _adapto_seo --json # reuse the id on a hit101 adapto collections create --name "Adapto SEO" --slug _adapto_seo \102 --description "Per-piece SEO metadata for Adapto content" --language <lang> --status draft \103 --fields-json '<the _adapto_seo field-set — reserved-slugs.md>' --json104 ```105 ⚠️ **Reserved-slug fallback:** if `_adapto_` is rejected, retry once with `adapto-seo`; record which slug106 worked. Capture its id into `.adapto/schema.json` alongside the user collections.1074. **Report + persist.** Print created-vs-reused with ids, then write `.adapto/schema.json` as108 `{"<slug>": "<id>", …}` (incl. `_adapto_seo`) for `adapto:content-upload`. **Loop cleanly** — judge success from each call's `--json`,109 not the shell exit code, and make the loop/function exit 0 on success so a created batch never surfaces as a110 red `Error: Exit code 1` ([conventions.md](../../shared/conventions.md) §8). **Then restart the dev server (stop→start) and keep it running** so111 the new collections/categories appear — **never kill it** (starters sync content at startup — conventions §14).112113`--fields-json` is a `FieldDefinitionModel[]`. No `--source` — collections and categories carry no provenance.114115## Errors and recovery116- **A create fails mid-run (partial state)** → whatever was created before the failure stays. This is safe:117 every step is `get-by-slug`-first, so a **re-run reuses** those ids instead of duplicating. Report what118 landed, fix the offending field in `schema-plan.json`, and re-run. Step 0 exists so this rarely happens.119- **`Field <name> of type <type> cannot be multiple`** → `multiple: true` on a type that forbids it (usually120 `multi_select`). Drop the key or use `select` + `multiple: true`, then re-run.121- **Plan missing/invalid** → stop; tell the user to run `adapto:schema-design`.122- **Server rejects a field `type`** → surface which collection/field, and suggest a safe-vocabulary type123 (cheatsheet §5); don't retry blindly.124- **A `reference`'s `related_collection` slug isn't in the plan** → stop before writing; the plan is125 inconsistent.126- **Collection/category already exists** → reuse/update via `get-by-slug`; never create a duplicate.127- **Partial failure mid-apply** (collections have no batch — they're per-call) → report what was created so128 far, then stop. Re-running is safe (idempotent).129- **Not authenticated / no tenant** → stop; offer both auth paths — `Log in` or `Register` (conventions §11) — then tenant selection.130- **Language discovery fails** → ask the user for a language code the tenant has enabled; don't guess.131132## Forbidden actions133- Never write without an **approved plan** (plan-then-apply).134- Never pass `--source` here — collections/categories have no provenance field.135- Never create built-in Articles/Pages — the plan's `advisory` map is documentation only.136- Never assume the working tenant — confirm it before any write.137- Never modify the scaffolded read-client ([forbidden-actions.md](../../shared/forbidden-actions.md)).