# 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](/concepts/change-sets), every revision records who or what made it, one [preview](/concepts/previews) 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](/concepts/reports). ## 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](/platform/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](/concepts/reports). Keys are `myna_sk_...` bearer tokens. ## The canonical loop 1. **Open a change set** — a titled container for the whole task. 2. **Create and update entries, upload assets** — attach every write to the change set and include a `changeSummary` explaining each change. 3. **Validate the change set** — every changed resource is checked against the deployed schemas. 4. **Create one preview URL** — a single link covering everything in the change set. 5. **Human reviews** — field-level diffs in the dashboard, rendered output via the preview link. 6. **Human publishes** — atomically, in one transaction. Or a publish-scoped principal does, on explicit instruction. ### Via MCP With the [MCP server configured](/mcp/setup), the loop is a fixed tool sequence (full inputs in the [tool reference](/mcp/tools)): ```json { "tool": "myna_create_change_set", "arguments": { "title": "Q3 launch posts" } } ``` Then, referencing the returned `chs_` id on every write: ```json { "tool": "myna_create_entry", "arguments": { "collection": "posts", "data": { "title": "Hello" }, "changeSet": "chs_...", "changeSummary": "Draft launch announcement" } } ``` ```json { "tool": "myna_update_entry", "arguments": { "entry": "posts/hello", "data": { "body": "..." }, "expectedRevisionId": "rev_...", "changeSet": "chs_...", "changeSummary": "Tighten intro" } } ``` ```json { "tool": "myna_upload_asset", "arguments": { "filename": "cover.png", "path": "./cover.png" } } ``` ```json { "tool": "myna_validate_change_set", "arguments": { "changeSet": "chs_..." } } ``` ```json { "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](/sdk/management-client): ```ts 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](/cli/overview) mirrors the same steps (`MYNA_TOKEN` or `myna login`): ```bash $ 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.