Agents
The agent workflow
How agents draft, validate, and preview content in Myna while humans keep the publish gate.
Every write in Content lands as a draft on a change set, every revision records who or what made it, one preview URL covers the whole change set, and publishing is a separate, explicitly scoped operation. An agent can work for hours and nothing is public until a human — or a deliberately publish-scoped principal — publishes.
For the Feedback product's agent loop, see reports and boards.
What the workflow guarantees
| Property | Mechanism |
|---|---|
| Reviewable units of work | A change set groups entry and asset operations into one atomic, previewable batch |
| Attribution | Every revision and asset records created_by_type (user, api_key, agent, system) and created_by_id |
| Safe by default | Writes create draft revisions; nothing is published as a side effect |
| One link to review | A single preview URL renders the entire change set as it would look published |
| Human gate | Publishing requires the content:publish scope, which agent keys should not have |
| Concurrency safety | expectedRevisionId gives optimistic concurrency across concurrent agents |
Scoping agent keys
Create a dedicated API key for each agent with these scopes:
content:read, content:write, assets:read, assets:write, preview:write, schema:read
Deliberately omit content:publish. The agent can draft, validate, and preview everything, but a publish attempt fails with PERMISSION_DENIED (HTTP 403) — the agent stops, reports the change set id and preview URL, and a human publishes after review.
Add feedback:read and feedback:write if the agent also works the report queue. Keys are myna_sk_... bearer tokens.
The canonical loop
- Open a change set — a titled container for the whole task.
- Create and update entries, upload assets — attach every write to the change set and include a
changeSummaryexplaining each change. - Validate the change set — every changed resource is checked against the deployed schemas.
- Create one preview URL — a single link covering everything in the change set.
- Human reviews — field-level diffs in the dashboard, rendered output via the preview link.
- Human publishes — atomically, in one transaction. Or a publish-scoped principal does, on explicit instruction.
Via MCP
With the MCP server configured, the loop is a fixed tool sequence (full inputs in the tool reference):
{ "tool": "myna_create_change_set", "arguments": { "title": "Q3 launch posts" } }
Then, referencing the returned chs_ id on every write:
{ "tool": "myna_create_entry", "arguments": { "collection": "posts", "data": { "title": "Hello" }, "changeSet": "chs_...", "changeSummary": "Draft launch announcement" } }
{ "tool": "myna_update_entry", "arguments": { "entry": "posts/hello", "data": { "body": "..." }, "expectedRevisionId": "rev_...", "changeSet": "chs_...", "changeSummary": "Tighten intro" } }
{ "tool": "myna_upload_asset", "arguments": { "filename": "cover.png", "path": "./cover.png" } }
{ "tool": "myna_validate_change_set", "arguments": { "changeSet": "chs_..." } }
{ "tool": "myna_create_preview", "arguments": { "changeSet": "chs_..." } }
The agent reports the preview URL and stops. Destructive tools additionally require confirm: true, and myna_publish_change_set fails without content:publish regardless.
Via the SDK
The same loop with the management client:
import { createManagementClient } from "@myna-sh/sdk/management";
const myna = createManagementClient({ token: process.env.MYNA_TOKEN! });
const project = "my-site";
const cs = await myna.changeSets.create(project, { title: "Q3 launch posts" });
const entry = await myna.entries.create(project, {
collection: "posts",
data: { title: "Hello", body: "..." },
changeSetId: cs.id,
changeSummary: "Draft launch announcement",
});
await myna.entries.update(project, entry.id, {
data: { body: "Revised body" },
expectedRevisionId: entry.draftRevisionId ?? undefined,
changeSetId: cs.id,
changeSummary: "Tighten intro",
});
const asset = await myna.assets.upload(project, "./cover.png");
const validation = await myna.changeSets.validate(project, cs.id);
if (!validation.valid) throw new Error(JSON.stringify(validation.errors));
const preview = await myna.previews.create(project, { changeSetId: cs.id });
console.log(preview.url); // hand this to the human
Mutations automatically carry an Idempotency-Key, so retried calls are replay-safe.
Via the CLI
The CLI mirrors the same steps (MYNA_TOKEN or myna login):
$ myna changes create --title "Q3 launch posts"
$ myna entries create posts --set title="Hello" --change-set chs_... --summary "Draft launch announcement"
$ myna entries update posts/hello --set body="Revised" --expected-revision rev_... --change-set chs_... --summary "Tighten intro"
$ myna assets upload ./cover.png
$ myna changes validate chs_...
$ myna changes preview chs_...
# Human reviews, then:
$ myna changes publish chs_... --confirm-publish
myna changes validate exits non-zero when the change set is invalid, and publish refuses to run without the explicit --confirm-publish flag.
Attribution
The field-level diff shows agent-authored revisions distinctly from human edits, annotated with their changeSummary. Include one on every write, so the review reads like a commit log.
When an unauthorized publish is attempted
Publishing is a single transaction: it applies every item in the change set or none. A principal without content:publish — via myna_publish_change_set, changeSets.publish(), or myna changes publish — gets PERMISSION_DENIED (HTTP 403) and nothing is partially applied.
Concurrent agents and optimistic concurrency
When multiple agents (or an agent and a human) may touch the same entry, pass expectedRevisionId on updates. If another writer created a newer revision in the meantime, the update fails with STALE_REVISION (HTTP 409) instead of silently clobbering it. Recover by re-reading the entry, merging against the latest data, and retrying with the new revision id. Revisions are immutable, so nothing is ever lost — myna_get_entry_revisions lists the full history and myna_restore_revision can restore any prior state into a new draft.
