Skip to content

Content Moderation

Overview

Content Moderation builds on top of the [[Versioning]] and [[Workflow Engine]] systems to provide a review-before-publish flow. Instead of editing items directly, users create draft versions containing their changes, then submit them for approval. Reviewers approve or reject through a configurable workflow. On approval, the version's changes are automatically applied to the main item.

The main (published) item remains unchanged throughout the review process.

Version Status Lifecycle

StatusDescription
draftEditable. Not yet submitted for review.
pendingSubmitted and waiting for reviewer action. Read-only until resolved.
approvedApproved and promoted — delta has been applied to the main item.
rejectedRejected by a reviewer. Main item unchanged.
              ┌─── cancel ───┐
              v              │
  draft ──> pending ──> approved   (delta applied to main item)

                └──> rejected      (main item unchanged)

Setup

Prerequisites

  • A collection with items (e.g. articles)
  • A workflow definition bound to that collection with trigger_event: "versions.submit"
  • The workflow must be active

Create a Moderation Workflow

bash
# 1. Create the workflow
POST /workflows
{
  "name": "Article Review",
  "collection": "articles",
  "trigger_event": "versions.submit"
}

# 2. Add steps: start → approval → end
POST /workflows/<wf-id>/steps
{ "key": "start", "name": "Start", "type": "start", "sort_order": 1 }

POST /workflows/<wf-id>/steps
{
  "key": "review",
  "name": "Editor Review",
  "type": "approval",
  "assign_type": "role",
  "assign_value": "<editor-role-uuid>",
  "approval_mode": "any",
  "sort_order": 2
}

POST /workflows/<wf-id>/steps
{ "key": "done", "name": "Done", "type": "end", "sort_order": 3 }

# 3. Add transitions
POST /workflows/<wf-id>/transitions
{ "from_step_id": "<start-id>", "to_step_id": "<review-id>", "trigger": "approve" }

POST /workflows/<wf-id>/transitions
{ "from_step_id": "<review-id>", "to_step_id": "<done-id>", "trigger": "approve" }

POST /workflows/<wf-id>/transitions
{ "from_step_id": "<review-id>", "to_step_id": "<done-id>", "trigger": "reject" }

# 4. Activate
POST /workflows/<wf-id>/activate

Trigger Filter (Optional)

Only trigger moderation for specific items using trigger_filter:

json
{
  "trigger_filter": {
    "category": { "_eq": "news" },
    "status": { "_eq": "published" }
  }
}

The filter evaluates against the merged data (current main item + version delta), so conditions can reference both existing and proposed values.


Content Creator Guide

Create a Draft Version

Save proposed changes as a version. The delta contains only the fields being changed, not the entire item.

http
POST /versions
json
{
  "collection": "articles",
  "item": "<article-id>",
  "key": "revision-june",
  "name": "Updated headline and body",
  "delta": {
    "title": "New Headline",
    "body": "Revised article body..."
  }
}

Returns { "data": "<version-id>" }. The version is created with status: "draft".

Edit a Draft

Update the delta before submitting:

http
PATCH /versions/<version-id>
json
{
  "delta": {
    "title": "Final Headline",
    "body": "Final article body..."
  }
}

Preview

Fetch the version to see proposed changes:

http
GET /versions/<version-id>
json
{
  "data": {
    "id": "<version-id>",
    "collection": "articles",
    "item": "<article-id>",
    "status": "draft",
    "delta": {
      "title": "Final Headline",
      "body": "Final article body..."
    }
  }
}

To render a full preview, merge the delta onto the current item:

js
const mainItem = await api.get(`/items/articles/${articleId}`);
const version = await api.get(`/versions/${versionId}`);
const preview = { ...mainItem.data, ...version.data.delta };

Submit for Review

http
POST /versions/<version-id>/submit

Returns { "data": { "id": "<version-id>", "status": "pending" } }.

This changes the version status to pending and automatically triggers any matching workflow on the collection.

Submit will fail (400) if:

  • Version status is not draft
  • Version has no delta
  • A workflow instance is already running for this version

Track Status

List versions for a specific item:

http
GET /versions?filter[item][_eq]=<article-id>&sort=-date_created

Each version includes a status field: draft, pending, approved, or rejected.

After Rejection

  • The main item is not modified
  • The version status becomes rejected
  • Create a new version with updated content and submit again

After Cancellation

If an admin cancels the workflow:

  • The version reverts to draft
  • You can edit the delta and re-submit the same version

Reviewer Guide

Task Inbox

See tasks assigned to you:

http
GET /workflow/tasks/mine
json
{
  "data": [
    {
      "id": "<instance-step-id>",
      "step_id": "<step-id>",
      "instance_id": "<instance-id>",
      "workflow_name": "Article Review",
      "step_name": "Editor Review",
      "collection": "articles",
      "item_id": "<article-id>",
      "version_id": "<version-id>",
      "status": "active",
      "activated_at": "2026-06-08T10:05:00Z",
      "timeout_at": null,
      "actions_available": ["approve", "reject", "delegate", "comment"]
    }
  ],
  "meta": { "total_count": 3 }
}

For a badge count:

http
GET /workflow/tasks/mine/count

Returns { "data": { "count": 3 } }.

Review Content

Use the version_id from the task to fetch the submission:

http
GET /versions/<version-id>

Fetch the current published item for comparison:

http
GET /items/articles/<article-id>

Display a diff by comparing each key in delta against the main item:

js
const changes = Object.entries(version.delta).map(([field, newValue]) => ({
  field,
  current: mainItem[field],
  proposed: newValue,
}));

Approve

http
POST /workflow-instances/<instance-id>/steps/<step-id>/approve
json
{
  "comment": "Looks good, approved."
}

On final approval:

  1. Workflow completes with status approved
  2. Version delta is applied to the main item automatically
  3. Version status changes to approved

Reject

http
POST /workflow-instances/<instance-id>/steps/<step-id>/reject
json
{
  "comment": "Title needs revision. Second paragraph is unclear."
}

Comment is required on rejection.

On rejection:

  1. Workflow follows the reject transition path
  2. Version status changes to rejected
  3. Main item is not modified

Delegate

Reassign the review task to another user:

http
POST /workflow-instances/<instance-id>/steps/<step-id>/delegate
json
{
  "to_user": "<other-user-id>",
  "comment": "Needs legal review."
}

Comment Without Decision

Leave feedback without approving or rejecting:

http
POST /workflow-instances/<instance-id>/steps/<step-id>/comment
json
{
  "comment": "Consider rephrasing the introduction."
}

View Workflow History

Full audit trail for an instance:

http
GET /workflow-instances/<instance-id>

Response includes instance_steps (step statuses and timestamps), actions (every approve/reject/delegate/comment), and workflow_steps (the step definitions).

Cancel (Admin Only)

Stop a running workflow:

http
POST /workflow-instances/<instance-id>/cancel
json
{
  "comment": "No longer needed."
}

Cancellation reverts the version to draft (if it was pending), allowing the creator to re-submit.

All Tasks (Admin Only)

View pending tasks across all reviewers:

http
GET /workflow/tasks/all

Data Model Changes

odp_versions (updated)

ColumnTypeDescription
statusvarchar(20)draft, pending, approved, rejected. Default: draft

Index: idx_versions_status on status.

odp_workflow_instances (updated)

ColumnTypeDescription
version_idvarchar(255)FK to the version being reviewed. Nullable (null for non-version workflows).

Index: idx_wfi_version_id on version_id.

Task response (updated)

The GET /workflow/tasks/mine and GET /workflow/tasks/all responses now include a version_id field, linking the task to the specific version under review.


Multi-Step Approval

For content that requires multiple levels of review:

Start → Editor Review → Legal Review → Publisher Sign-off → End

Each approval step can have:

  • Different assignees (by role, user, or dynamic field)
  • Different approval modes (any, all, majority)
  • Independent timeouts with auto-approve, auto-reject, escalate, or notify actions

Rejection at any step follows the reject transition — typically going directly to End, but it can also loop back to an earlier step for revision.

Action Steps

Add automated actions between steps:

Action TypeDescription
promote_versionPromote the version (apply delta to main item). Useful if you want promotion at a specific step rather than at workflow completion.
send_notificationSend a notification to specified recipients.
update_fieldUpdate a field on the main item (e.g. set review_status).

Example: add a promote_version action step between the final approval and the end step to control exactly when promotion happens.


Frontend Integration Notes

  • Delta merge: { ...mainItem, ...version.delta } produces the full preview. The delta only contains changed fields.
  • Diff rendering: iterate over Object.keys(delta) and compare each value against the main item to highlight changes.
  • Polling: poll GET /workflow/tasks/mine/count for the reviewer inbox badge. Poll GET /versions/:id for creator-side status updates.
  • Direct promote: when a collection has an active versions.submit workflow, hide the direct POST /versions/:id/promote button — route all changes through the moderation flow.
  • Task → Version link: the version_id in task responses connects a workflow task to the content being reviewed.
  • Error messages: submit returns 400 with a reason field explaining why it failed (wrong status, no delta, duplicate workflow).

ODP Internal API Documentation