Features
Workflow & Review Queues
The workflow module adds multi-stage content approval to any collection. Documents flow through configurable stages (e.g., draft, editorial review, legal check, published), with support for reviewer assignment, approval/rejection tracking, and automatic stage advancement.
Server-side execution
Workflow functions run inside Convex mutations and queries. Mutations are transactional -- if a workflow action fails, nothing is written. See Key Concepts for background.
Setting up workflows
1. Define workflow stages
Each collection with workflows needs a WorkflowCollectionConfig that lists the custom stages between draft and published:
import { createVextroWorkflowModule } from "vextro/convex/workflow";
const workflow = createVextroWorkflowModule({
query,
mutation,
components,
getCurrentUser,
requireAdminRead,
requireAdminWrite,
workflowConfigs: [
{
collectionSlug: "articles",
enabled: true,
stages: [
{
name: "editorial-review",
label: "Editorial Review",
color: "blue",
requiredRole: "editor",
autoTransition: { afterApprovals: 2 },
},
{
name: "legal-check",
label: "Legal Check",
color: "orange",
requiredRole: "legal",
autoTransition: { afterApprovals: 1 },
},
],
},
],
}); The full stage flow is always: draft → your custom stages in order → published. The draft and published stages are implicit -- you only define the stages in between.
2. Export the workflow functions
Expose the workflow module from your Convex functions file:
// convex/workflow.ts
export const {
getWorkflowState,
listReviewQueue,
getWorkflowHistory,
getWorkflowConfig,
getReviewQueueCounts,
submitForReview,
approveDocument,
rejectDocument,
assignReviewer,
advanceWorkflow,
recallFromReview,
} = workflow; 3. Add the review queue page
Vextro includes a built-in review queue page that shows documents awaiting review, with stage filter tabs, assignment filters, and quick action buttons:
---
import VextroReviewQueuePage from "vextro/pages/VextroReviewQueuePage.astro";
---
<VextroReviewQueuePage
convex={convex}
api={api}
basePath="/admin"
/> Stage configuration
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | * | Unique identifier for the stage (used in URLs, queries, history) | |
label | string | * | Human-readable label shown in the admin UI | |
color | string | Stage pill color in the review queue UI | ||
requiredRole | string | Role required to approve at this stage | ||
autoTransition | object | Auto-advance configuration | ||
autoTransition.afterApprovals | number | Number of approvals required to auto-advance to the next stage |
Collection config
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
collectionSlug | string | * | The collection this workflow applies to | |
enabled | boolean | * | Whether the workflow is active for this collection | |
stages | WorkflowStageConfig[] | * | Ordered list of custom stages between draft and published |
Workflow lifecycle
Status transitions
draft ──submit──► stage-1 ──approve──► stage-2 ──approve──► published
▲ │ │
└──────reject──────┘ │
└──────reject───────────────────────────┘
└──────recall───────────────────────────┘ - Submit: Moves a document from
draftto the first custom stage. Creates or resets the workflow entry. - Approve: Records an approval on the current stage. If
autoTransition.afterApprovalsis configured and met, the document auto-advances to the next stage. - Reject: Moves the document back to
draftwith a required reason. Clears approvals. - Advance: Manually moves a document to the next stage (regardless of approvals).
- Recall: Author recalls a document back to
draftfrom any stage. - Publish: When a document advances past the last custom stage, the workflow entry is deleted and the document reaches
published.
Auto-advance
When a stage has autoTransition.afterApprovals set, reaching that number of unique approvals automatically advances the document. Each user can only approve once per stage -- duplicate approvals are rejected.
Queries
getWorkflowState
Returns the current workflow entry for a document, or null if the document is not in a workflow.
const state = await ctx.runQuery(api.workflow.getWorkflowState, {
documentId: "abc123",
});
// { _id, documentId, collectionSlug, stage, assignedTo, approvals, rejections, ... } listReviewQueue
Lists documents in the review queue with optional filters. Results are enriched with document titles and collection labels.
// All review items
const queue = await ctx.runQuery(api.workflow.listReviewQueue, {});
// Filter by stage
const inReview = await ctx.runQuery(api.workflow.listReviewQueue, {
stage: "editorial-review",
});
// Filter by assignee
const myQueue = await ctx.runQuery(api.workflow.listReviewQueue, {
assignedTo: userId,
});
// Filter by collection and stage
const articles = await ctx.runQuery(api.workflow.listReviewQueue, {
collectionSlug: "articles",
stage: "legal-check",
limit: 20,
}); getWorkflowHistory
Returns the history of workflow actions for a document, ordered newest first.
const history = await ctx.runQuery(api.workflow.getWorkflowHistory, {
documentId: "abc123",
limit: 20,
});
// [{ action, fromStage, toStage, userId, userName, comment, timestamp }, ...] getWorkflowConfig
Returns the workflow configuration for a collection, or null if workflows are not enabled.
const config = await ctx.runQuery(api.workflow.getWorkflowConfig, {
collectionSlug: "articles",
}); getReviewQueueCounts
Returns aggregate counts for the review queue -- total items, items assigned to the current user, and per-stage breakdowns.
const counts = await ctx.runQuery(api.workflow.getReviewQueueCounts, {});
// { total: 12, assignedToMe: 3, byStage: { "editorial-review": 7, "legal-check": 5 } } Mutations
submitForReview
Submits a document for review. Creates a new workflow entry or resets an existing one to the first stage.
const entryId = await ctx.runMutation(api.workflow.submitForReview, {
documentId: "abc123",
collectionSlug: "articles",
assignTo: reviewerUserId, // optional
comment: "Ready for editorial review", // optional
}); approveDocument
Records an approval for the current stage. Returns whether the document auto-advanced.
const result = await ctx.runMutation(api.workflow.approveDocument, {
documentId: "abc123",
comment: "Looks good", // optional
});
// { advanced: true, newStage: "legal-check" }
// or { advanced: false, newStage: "editorial-review" } if more approvals needed
A user cannot approve the same document twice at the same stage. Attempting to do so throws an error.
rejectDocument
Rejects a document and moves it back to draft. A reason is required.
await ctx.runMutation(api.workflow.rejectDocument, {
documentId: "abc123",
reason: "Needs revision -- factual errors in section 3",
}); assignReviewer
Assigns a specific user as the reviewer for a document in the workflow.
await ctx.runMutation(api.workflow.assignReviewer, {
documentId: "abc123",
assignTo: reviewerUserId,
}); advanceWorkflow
Manually advances a document to the next stage, bypassing approval requirements.
const result = await ctx.runMutation(api.workflow.advanceWorkflow, {
documentId: "abc123",
comment: "Fast-tracked by admin", // optional
});
// { newStage: "legal-check" }
When advancing to published (past the final custom stage), the workflow entry is deleted. The history is preserved.
recallFromReview
Recalls a document back to draft from any stage. Clears approvals and reviewer assignment.
await ctx.runMutation(api.workflow.recallFromReview, {
documentId: "abc123",
comment: "Pulling back for major edits", // optional
}); Convex tables
The workflow module uses two tables:
| Table | Purpose |
|---|---|
adminWorkflowEntries | Active workflow state for each document (stage, approvals, rejections, assignment) |
adminWorkflowHistory | Immutable audit log of all workflow actions |
These tables are created automatically when you use the Vextro Convex component.
Review queue UI
The built-in VextroReviewQueuePage provides:
- Summary cards showing total items, items assigned to you, and per-stage counts
- Stage filter tabs to view items at a specific workflow stage
- Assignment filter to see only items assigned to you
- Document table with collection label, stage pill, assigned reviewer, and last-updated timestamp
- Quick actions -- approve or open a document directly from the queue
The page accepts stage and assigned URL query parameters for deep linking to filtered views.