Four eyes, before it goes live
Switch protection on for a content type and publishing it needs approvals — from somebody other than whoever wrote the version being published. It is branch protection, not a workflow: no stages, no assignment, no new status, and an approval that stops counting the moment the text changes underneath it.
For editors-in-chief, and whoever answers the auditor
The third gate, and the only one that asks who
Publishing already passes two checks: the caller holds content:publish, and the entry satisfies its type’s readiness rules. Protection is the third, and it never looks at a field value — whether an entry is complete has one owner, and a second opinion on it would eventually disagree. This gate answers a different question: may this person ship it, now, on this version.
It sits behind a port in the content plugin rather than inside the admin, so it meets every caller of the publish path at once — the button in the editor, the REST route, the GraphQL mutation, an agent over MCP. There is no list of entry points to keep up to date, and a route written next month is covered the day it is written.
A refusal is a 409 carrying what was required and what had been given — deliberately distinct from the 403 you get without the permission, because one means ask an administrator for a permission and the other means ask a colleague to read your work.
Three callers — an editor, an API token and an agent — arriving at one rule demanding two approvals on the current version, with publication behind it.
What a rule says
One rule per content type per workspace. No condition and no per-entry exception, so “why is this blocked and the one beside it not” has a one-word answer: the type.
| Setting | What it decides |
|---|---|
| Enabled | Off, and the type behaves exactly as it does with no rule at all. Every type starts here. |
| Required approvals | How many approvals the current version needs before publication is allowed. The editor counts up to it beside the status. |
| Require somebody else | On by default: the author of the version being published cannot approve it. The four-eyes switch, and it applies to administrators too. |
| Count stale approvals | Off by default. On, approvals given on earlier versions keep counting — it exists because somebody always asks, and the editor labels it as the weaker choice. |
| Administrator bypass | On by default: an administrator may publish short of approvals, after confirming a dialog that says it will be logged. Off makes the rule absolute. |
| Allow token publish | Off by default. A bearer token names no person, so it cannot satisfy a rule about people — and a rule you can escape by minting a key is not a rule. |
Switching a rule on or off, or lowering the number it demands, is itself written to the activity log — otherwise the quiet way past a rule is not a button somebody had to confirm, it is a settings tab left open for two minutes.
An approval belongs to a version, not to an entry
Every save already appends an immutable version of the whole document. An approval is recorded against that version — so the moment somebody saves again, the approvals that were counted are approvals of something else, and the count returns to zero with no dismissal logic anywhere and no rule to remember.
This is the failure that makes the feature meaningless everywhere it is done differently: approve, then edit, then publish something nobody read. Nothing is deleted — the earlier approval stays in the panel, struck through and named with the version it was given on, because a counter that silently rolls back after a save is unexplainable to whoever just pressed Save.
Translations get it for free. Versions are per language already, so an approval on the German entry says nothing about the French one, and publishing every locale at once reports which languages are held.
How a review actually goes
Four moves, none of which is a new status on your content.
-
Ask
Request review from the entry’s rail and pick the people — the picker offers the workspace members who may approve, and nobody else. Asking twice replaces who was named rather than opening a second request.
-
Be found
A reviewer opens Reviews in the workspace: what is waiting on them and what they asked for themselves, across every content type, with the age of each request. The records list carries a review column and a review-state filter too, so “everything awaiting approval” is a saved view like any other.
-
Approve
One kind of vote, and no notes. A reviewer who is not satisfied simply does not approve — the sentence explaining why belongs in a message to a colleague rather than in a workflow engine.
-
Publish
The button says where the count stands — 0 of 2, 1 of 2, then ordinary — and its verdict is about the version the press will actually ship: unsaved edits are saved first, so the editor predicts what the server will decide rather than what was true a minute ago.
Nothing happens until somebody writes a rule
With no rule for a content type, publication behaves exactly as it does with the plugin uninstalled — no extra query on the publish path, no chip in the editor, no column in the list. Turning it on is one switch on one type, never a mode the whole CMS enters.
Around the edges
- A bypass is loud
- An administrator publishing short of approvals confirms a dialog first, and the publish writes one activity row naming them, the rule, and how far short the count was. An override nobody can count is, three months later, indistinguishable from no rule at all.
- No agent can approve
- There is no approve tool, for the assistant or over MCP, and there will not be one. An agent can read where a review stands, show what changed since you last approved, and ask people on your behalf — but the vote is the authorisation, and handing that to a model hands over the key.
- Readers never see it
- Review state is derived, never stored on the entry and never returned as a status. Your front end, the REST and GraphQL APIs, exports and webhooks cannot tell a protected type from an unprotected one. Drafts are edited as freely as on any other type: the gate is on the step that makes work public.
The Friday afternoon
The pricing page needs one number changed. The writer edits it, asks the editor-in-chief, and goes home. The approval arrives at five, the editor-in-chief’s Publish button turns ordinary, and the page goes live. Then somebody spots a typo and saves a fix — and the count is back to zero, because the version that was read is not the version that would ship.
Questions before you switch it on
Is this an editorial workflow?
No, and deliberately not. No stages, no assignment deciding whose approval counts, and no third entry status — your content is still draft or published, and every system reading that column is untouched. A rule is a gate on one transition, which is why it comes off as easily as it went on.
Who is allowed to approve?
Anybody in the workspace holding content:approve, except the author of the version being published. A request names people so they know they are wanted, but naming them never changes whose approval counts — that would be a second permission system living inside workspace membership.
What happens in a one-person workspace?
A rule requiring somebody else blocks everything, so the settings screen says so at the moment you switch it on rather than a week later when the first publish fails.
Can a scheduled publish sneak past it?
There is no scheduled publishing in Apograph, so there is nothing to sneak past today — and the decision for the day it exists is already written down: the rule is evaluated when the timer fires, not when the schedule was set, because scheduling and then editing would otherwise publish work nobody approved.
Next
Every feature is included, free
The core is MIT licensed and every feature is in it — none of them is paid-only. Community runs free in production; a plan buys room and governance, not a different product.