Management API

Editorial operations

Bulk staging, duplication, JSON import with dry-run, full project export, and saved entry views.

Day-to-day editorial work is more than one entry at a time. These operations act on many entries — or a whole project — while keeping Content's invariant: everything stages on a change set; nothing publishes implicitly.

Bulk staging

POST /v1/projects/:project/entries/bulk stages a delete or unpublish across up to 200 entries in one call:

{ "action": "unpublish", "entryIds": ["ent_01...", "ent_02..."], "changeSetId": "chs_01..." }

Omit changeSetId to let each entry resolve its change set the normal way (its existing open change set, else an implicit one). The response reports per-entry results — an entry that already belongs to a different open change set fails alone without aborting the batch:

{ "results": [
  { "entryId": "ent_01...", "ok": true, "changeSetId": "chs_01..." },
  { "entryId": "ent_02...", "ok": false, "error": "Entry already belongs to another open change set." }
] }

delete requires the content:delete scope; unpublish requires content:write. CLI: myna entries bulk --action unpublish --ids ent_01...,ent_02... (non-zero exit if any row fails). The dashboard exposes the same operation through checkbox selection on the entry table.

Duplicate an entry

POST /v1/projects/:project/entries/:entry/duplicate creates a new draft from the source's current draft data — "create from existing". The copy derives a fresh unique slug, records Duplicated from <id> as its change summary, and stages a create item like any other new entry. Optional body: { "changeSetId": "chs_..." }.

CLI: myna entries duplicate posts/launch. Dashboard: the ⋯ menu in the entry editor.

Import with dry-run

POST /v1/projects/:project/entries/import writes entries in bulk:

{
  "collection": "posts",
  "dryRun": true,
  "mode": "upsert",
  "changeSetId": "chs_01...",
  "entries": [
    { "slug": "hello", "data": { "title": "Hello", "body": "..." } },
    { "data": { "title": "No slug — derived automatically" } }
  ]
}

Imports are safe to run repeatedly:

  • Dry run. With "dryRun": true, every row is validated against the collection's deployed schema (including locale rules) and the response reports the outcome of each row. Nothing is written. Matching is identical to a real run, so the counts are exact.
  • All-or-nothing. Without dryRun, rows are validated and checked for conflicts before anything is written. If any row fails, none are applied — including a row whose entry is already staged on another open change set.
  • Matched by slug. mode: "upsert" (the default) updates a row whose slug already exists. A row whose data is unchanged is reported unchanged and writes no revision. Use mode: "create" to reject an existing slug as an error instead.
  • Never publishes. Writes stage on a change set: the one named in changeSetId, or a new one titled Import into <collection>. If every row is unchanged, no change set is created and changeSetId is null.

The response carries per-row outcomes and a summary:

{
  "valid": true,
  "changeSetId": "chs_01...",
  "summary": { "created": 3, "updated": 1, "unchanged": 21, "invalid": 0, "skipped": 0 },
  "results": [{ "index": 0, "ok": true, "outcome": "unchanged", "slug": "hello" }]
}

Limits: 500 rows per request.

From the CLI

The CLI reads JSON, or a directory of Markdown files with frontmatter:

$ myna entries import ./content/posts --collection posts --dry-run
$ myna entries import ./content/posts --collection posts
$ myna entries import ./posts.json --change-set chs_01...

Frontmatter keys become fields, the filename becomes the slug unless frontmatter sets one, and the document body is assigned to --body-field (default body).

Frontmatter supports quoted and bare scalars, numbers, booleans, null, and inline [a, b] arrays. Anything else is rejected with the file and line.

Syncing a content directory

myna sync reconciles a directory of content files against collections in one pass:

$ myna sync ./content --dry-run
$ myna sync ./content
$ myna sync ./content --delete-missing --confirm-delete

Each immediate subdirectory becomes a collection of the same name (./content/postsposts); pass --collection to treat the whole directory as one collection. All changes stage on a single change set.

--delete-missing additionally stages deletion of entries whose slug has no matching file. It requires --confirm-delete, and lists every entry it will remove.

Run --dry-run first. An entry that was never part of your content directory is indistinguishable, to sync, from one you intended to delete.

Editorial ordering

POST /v1/projects/:project/collections/:collection/reorder sets a collection's arrangement.

$ myna entries reorder projects --slugs myna,erlcx,handitoff,fleck
$ myna entries reorder projects --move fleck --after myna

Positions are stored as fractional ranks: strings compared lexicographically, with room always available between any two (a0 < a1 < a1V < a2). Moving one entry writes exactly one row, regardless of collection size.

Ordering is not staged on a change set. It carries no revision and is visible to readers as soon as it is applied — which is why the request must include "confirm": true.

Use it to arrange content, not to change it. Anything that needs review belongs in the entry's fields.

Collections with no explicit arrangement are ordered newest first. Ranked entries sort before unranked ones, so a new entry in an arranged collection appears at the top.

Full project export

GET /v1/projects/:project/export (scopes content:read + schema:read) returns one JSON document for portability and backups:

Key Contents
project Project settings, locales, publish gates, origins
collections Each collection's key, metadata, and deployed canonical schema
entries Every live entry with both draft and published data
assets Asset metadata: filename, content type, checksum, alt, caption, tags

Asset binaries stay in storage — the metadata includes the checksum so a migration can verify copies. CLI: myna projects export > backup.json.

Export pairs with import for environment cloning: export from production, import into staging (see environments and promotion).

Saved views

Saved views are named entry-table presets — collection, status filter, and search — stored per project so humans and agents see the same lists:

  • GET /v1/projects/:project/views — list (content:read)
  • POST /v1/projects/:project/views — create, e.g. { "name": "Drafts needing review", "filters": { "collection": "posts", "status": "changed", "q": "" } } (content:write)
  • DELETE /v1/projects/:project/views/:view — remove (content:write)

filters is a free-form object interpreted by the client; the dashboard reads collection, status, and q, and applies a view from the Views menu on the content screen. SDK: client.projects.views(...), createView(...), deleteView(...).