Skip to content

Editorial review

Included free on every install, with one review workflow per tenant. More workflows, escalation, approvals from several reviewers and stages that apply only to some entries need a license with the review-pro feature. See pricing.

Editorial review puts an entry through sign-off before it goes live. You define a review for a content type as an ordered list of stages, each with the role allowed to approve it and an optional deadline. Reviewers approve, reject or ask for changes and leave comments, and the last approval publishes the entry.

PartWhat it is
DefinitionA named review for one content type, with its stages in order and whether the last approval publishes.
StageOne step, with the role that may approve it and an optional deadline.
AssignmentOne entry under review: its current stage, status, assignee and due time.
TransitionAn action on an assignment, recorded in its log with the actor and an optional comment.

An assignment's status is one of draft, pending_review, in_review, changes_requested, approved, rejected or published. A new one starts as pending_review.

ActionEffect
approveMoves to the next stage. At the last stage, approves the entry and, when the definition has publish_on_approve, publishes it and sets the status to published.
rejectRejects the entry. Its content status is left as it was.
request_changesSends the entry back to the first stage for changes.
publishPublishes an approved assignment, for a definition with publish_on_approve off.
unpublishTakes a published assignment back to approved, and returns the entry to draft.

publish_on_approve is true unless you turn it off. Publishing changes the entry's status everywhere, the Content API included, at once.

You need a token in TOKEN for an admin or super_admin. The quickstart shows how to get one.

  1. Create a post content type with draft and publish, and a draft entry in it:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/schemas \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"name": "post", "with_draft_publish": true, "fields": [{"name": "title", "field_type": "text"}, {"name": "text", "field_type": "text"}]}'
    curl -X POST http://localhost:3001/api/admin/content \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"schema": "post", "slug": "first", "title": "Draft post", "body": {"text": "Body."}}'

    Copy the entry's id into ENTRY.

  2. Define a review with two stages:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/review/definitions \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
    "name": "Blog review",
    "slug": "blog",
    "content_schema": "post",
    "stages": [
    {"name": "Copy edit", "required_role": "editor", "sla_duration_seconds": 86400},
    {"name": "Legal", "required_role": "legal"}
    ]
    }'

    The answer is 201 with the definition and "publish_on_approve": true. Copy its id into DEF.

  3. Put the entry into review, assigned to yourself:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/review/assignments \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"entry_id\": \"$ENTRY\", \"definition_id\": \"$DEF\", \"assignee_id\": \"<your user id>\"}"

    The answer is 201 with "status": "pending_review" and a due_at one day out. Copy its id into ASSIGNMENT.

  4. Approve the first stage:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/review/assignments/$ASSIGNMENT/transition \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"action": "approve", "comment": "Copy is clean."}'
    {
    "id": "5e2c9b7a-1f3d-4a6e-8b0c-7d9e2f4a1c63",
    "entry_id": "0f8d3a1c-6b2e-4c7f-9a5d-3e1b8c4f2a90",
    "definition_id": "8a4e1d2c-3b5f-4e6a-9c7d-1f2e3a4b5c6d",
    "current_stage_id": "2b7c9e1a-4d3f-4a8b-b6c5-9e8d7f6a5b4c",
    "assignee_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f",
    "assigned_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f",
    "assigned_at": "2026-10-01T12:00:00Z",
    "status": "in_review",
    "overdue": false,
    "created_at": "2026-10-01T12:00:00Z",
    "updated_at": "2026-10-01T12:05:00Z"
    }
  5. Send the same request again. The second stage is the last, so the answer has "status": "published" and the entry is published.

  6. Read the log:

    Terminal window
    curl http://localhost:3001/api/admin/review/assignments/$ASSIGNMENT/logs \
    -H "Authorization: Bearer $TOKEN"

    Each transition is listed with its action, actor_id and comment. In the admin console, Reviews lists the assignment, and the entry's Review card shows its stage.

Open Reviews. New definition creates a definition with its stages, each with a Stage name, a Required role and SLA, hours. Assignments lists every entry under review, filtered by status, with overdue ones marked Overdue.

On an entry's page, the Review card shows Not under review. until someone chooses Start review. Under review it shows the stage, status, assignee and due time, a comment box, and the actions you may take: Approve, Reject, Request changes, Publish and Unpublish.

Reviews follow the same rules as the rest of the instance, set on the Permissions page or through access rules. Two resources name reviews:

  • reviews: every review definition in the tenant.
  • review:<slug>: one definition, by its slug.

A rule grants read to see assignments, logs and comments, and activate to make a transition. A rule for one definition wins over the rule for reviews.

  • A super admin is always allowed.
  • An admin is allowed until a rule names admin for the resource, so an install with no rules works for admins out of the box.
  • Any other role needs a rule.

approve, reject and request_changes also need the stage's required_role, or the admin or super_admin role. A refusal answers 403, such as permission denied: stage requires role legal. Creating definitions, starting an assignment and reassigning need admin or super_admin.

A stage's sla_duration_seconds, at most 864000 (ten days), sets the assignment's due_at when the entry enters the stage. Once it passes and the assignment is still open, the assignment gets overdue_at, its overdue flag turns true, and review.sla.breached is published once for that stage. A transition clears the mark. GET /api/admin/review/assignments/{id}/sla shows the time spent in each stage against its deadline.

A definition takes name, slug, content_schema, publish_on_approve and stages. The slug is a lower-case letter followed by up to 39 lower-case letters, digits, hyphens or underscores, made from the name when you leave it out. A slug already taken answers 409. content_schema is default when you leave it out. PUT changes name or publish_on_approve, or replaces the stages. The slug never changes.

Every install holds one definition per tenant, with as many stages as it needs. A second one answers 402:

{"error": "cap_exceeded", "cap": "review.workflows", "limit": 1, "current": 1, "upgrade_url": ""}

Deleting a definition frees its place. GET /api/admin/review/definitions carries licensed and limits, as {"workflows": {"limit": 1, "current": 1}}, where limit is null when there is no ceiling.

Escalate, ask for several approvals, and skip stages

Section titled “Escalate, ask for several approvals, and skip stages”

With review-pro, a tenant holds any number of definitions, and each stage can take three more settings:

Stage settingWhat it does
escalate_to_user_id or escalate_to_roleWhen the stage misses its deadline, hands the assignment to that person, or lets anyone with that role act on the stage. Needs sla_duration_seconds. Set one, never both.
quorumThe number of approvals the stage needs, from the people assigned to it. 0 or 1 is a single approval.
conditionA test on one field of the entry. A stage whose test is false is skipped.
{
"name": "Legal review",
"required_role": "legal",
"sla_duration_seconds": 86400,
"escalate_to_role": "legal-lead",
"quorum": 2,
"condition": {"field": "category", "op": "equals", "value": "legal"}
}
  • Escalation. The deadline check, every five minutes, hands the assignment over once per visit to the stage, records an escalate entry in the log and publishes review.escalated. Moving to another stage ends it.
  • Quorum. Name the reviewers in assignee_ids when you start the assignment, at least as many as the largest quorum, or the start answers 422. Each assignee approves once. The approval that reaches the quorum moves the assignment on. Until then it stays on the stage, and review.transitioned carries approvals and approvals_needed. A rejection or a request for changes ends the visit and drops the approvals collected. Every assignment read, the list, one assignment and an entry's current assignment, carries assignee_ids, the current stage's approvals and its required_approvals, so you can show progress without reading the definition.
  • Conditions. op is equals, not_equals, in (with values, up to 100) or exists. A value is a string, a number or a boolean. The test runs when the assignment would enter the stage. A field the entry does not have reads as absent. An approval that skips every stage left approves the entry, and a start that skips every stage answers 409. review.transitioned lists the skipped stages in skipped_stage_ids.

Without review-pro, a definition create or update that sets any of these answers 402 naming feature:review-pro. If the license lapses, every definition keeps running as it is: stored escalations still escalate, quorums still count and conditions still route. An update that sends a stage's stored settings back unchanged, matched by the stage's name, stays free, and so do clearing a setting and deleting a definition.

The review.transition node moves an entry's current assignment, the same way the transition route does.

SettingPurpose
entry_idRequired. The entry, usually {{ trigger.record_id }}.
actionRequired. approve, reject, request_changes, publish or unpublish.
commentOptional. Recorded in the log.
actor_idOptional. The user the transition is recorded against.

It outputs assignment_id, entry_id, definition_id, definition_slug, content_schema, action, from_stage, stage, status, assignee_id, due_at and dry_run. A test run reports the transition without making it. An entry with no assignment, or an action its status does not allow, fails the step.

Three events can start a flow. All appear in GET /api/admin/flows/event-types.

review.transitioned fires on every transition, including the node's. It carries tenant_id, definition_id, definition_slug, content_schema, entry_id, assignment_id, action, from_stage_id, from_stage, to_stage_id, to_stage, from_status, to_status, actor_id, assignee_id and comment, plus skipped_stage_ids when a condition skipped stages, and approvals and approvals_needed when a quorum stage recorded an approval without moving.

review.sla.breached fires once per stage when an assignment becomes overdue there. It carries tenant_id, definition_id, definition_slug, content_schema, entry_id, assignment_id, stage_id, stage, assignee_id, status, due_at and overdue_at.

review.escalated fires right after it when the stage names an escalation. It carries tenant_id, definition_id, definition_slug, content_schema, entry_id, assignment_id, stage_id, stage, from_assignee_id, assignee_id, escalated_role (empty for a person) and escalated_at.

Editorial review has no settings of its own. It runs on every install. If you set LYEVE_PLUGINS to choose which features start, include review in it. See licensing and tiers.

Deleting a tenant deletes its reviews. A privacy erasure request removes the person's identity from assignments, logs and comments.

Every route takes a signed-in session. An admin token cannot call them.

Definitions, assignments, logs and comments
MethodPathRolePurpose
GET/api/admin/review/definitionsadminList definitions, paginated, with licensed and limits. Filter with ?content_schema=.
POST/api/admin/review/definitionsadminCreate a definition with its stages. Past one per tenant, needs review-pro.
GET/api/admin/review/definitions/{id}adminA definition with its stages, as {"definition", "stages"}.
PUT/api/admin/review/definitions/{id}adminChange name or publish_on_approve, or replace the stages.
DELETE/api/admin/review/definitions/{id}adminDelete a definition and its assignments.
POST/api/admin/review/assignmentsadminPut an entry into review: entry_id, definition_id, assignee_id, and assignee_ids for a quorum stage.
GET/api/admin/review/assignmentsread ruleList assignments, paginated, each with assignee_ids, approvals and required_approvals. Filter with ?assignee_id= and ?status=.
GET/api/admin/review/assignments/{id}read ruleOne assignment, with assignee_ids, the current stage's approvals and its required_approvals.
GET/api/admin/review/entries/{entryID}/assignmentread ruleThe entry's current assignment, with assignee_ids, approvals and required_approvals.
POST/api/admin/review/assignments/{id}/transitionactivate ruleMake a transition: action, optional comment.
POST/api/admin/review/assignments/{id}/reassignadminGive the assignment to someone else: assignee_id.
GET/api/admin/review/assignments/{id}/logsread ruleThe assignment's transitions.
GET/api/admin/review/assignments/{id}/slaread ruleTime spent in each stage against its deadline.
POST/api/admin/review/entries/{entryID}/stages/{stageID}/commentsread ruleComment on an entry at a stage: body.
GET/api/admin/review/entries/{entryID}/commentsread ruleThe entry's comments. Filter with ?stage_id=.
Every error these routes return
StatusMessageCause
400name is required or at least one stage is requiredThe definition is incomplete.
400slug must be a letter followed by up to 39 letters, digits, hyphens or underscoresBad slug, including one with capitals.
400action must be approve, reject, request_changes, publish or unpublishUnknown action.
400comment body is requiredA comment with no body.
400failed to transition assignmentThe last approval on a content type without draft and publish.
400invalid assignee_id, invalid assignee_idsAn assignee is not a valid id.
400A message naming the stage setting, such as condition op must be equals, not_equals, in or existsA stage setting is malformed.
402cap_exceeded with "cap": "review.workflows"A second definition without review-pro.
402payment_required, naming feature:review-proEscalation, a quorum or a condition without review-pro.
403A message naming the action and resource, or the stage's roleNo rule or stage role admits the caller.
403only an assignee may approve a quorum stageThe caller is not one of the stage's reviewers.
409failed to create review definitionThe slug is taken.
409definition has no stagesThe definition cannot take assignments.
409no stage of the definition applies to the entryEvery stage's condition skipped the entry.
409you have already approved this stageA second approval from the same reviewer.
422a quorum stage needs more approvals than the assignment names assigneesFewer assignees than the stage's quorum.
409failed to transition assignmentThe action does not fit the status, such as publish on an assignment that is not approved, or any stage action on a finished one.