Appearance
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
| Status | Description |
|---|---|
draft | Editable. Not yet submitted for review. |
pending | Submitted and waiting for reviewer action. Read-only until resolved. |
approved | Approved and promoted — delta has been applied to the main item. |
rejected | Rejected 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>/activateTrigger 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 /versionsjson
{
"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>/submitReturns { "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_createdEach 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/minejson
{
"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/countReturns { "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>/approvejson
{
"comment": "Looks good, approved."
}On final approval:
- Workflow completes with status
approved - Version delta is applied to the main item automatically
- Version status changes to
approved
Reject
http
POST /workflow-instances/<instance-id>/steps/<step-id>/rejectjson
{
"comment": "Title needs revision. Second paragraph is unclear."
}Comment is required on rejection.
On rejection:
- Workflow follows the reject transition path
- Version status changes to
rejected - Main item is not modified
Delegate
Reassign the review task to another user:
http
POST /workflow-instances/<instance-id>/steps/<step-id>/delegatejson
{
"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>/commentjson
{
"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>/canceljson
{
"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/allData Model Changes
odp_versions (updated)
| Column | Type | Description |
|---|---|---|
status | varchar(20) | draft, pending, approved, rejected. Default: draft |
Index: idx_versions_status on status.
odp_workflow_instances (updated)
| Column | Type | Description |
|---|---|---|
version_id | varchar(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 → EndEach 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 Type | Description |
|---|---|
promote_version | Promote the version (apply delta to main item). Useful if you want promotion at a specific step rather than at workflow completion. |
send_notification | Send a notification to specified recipients. |
update_field | Update 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/countfor the reviewer inbox badge. PollGET /versions/:idfor creator-side status updates. - Direct promote: when a collection has an active
versions.submitworkflow, hide the directPOST /versions/:id/promotebutton — route all changes through the moderation flow. - Task → Version link: the
version_idin task responses connects a workflow task to the content being reviewed. - Error messages: submit returns
400with areasonfield explaining why it failed (wrong status, no delta, duplicate workflow).