Content Publishing Workflow System Development (draft → review → publication)
A content workflow system manages the publication lifecycle. Instead of direct publishing by any user — configured process: draft → review → approval → publication → archiving. Critical for publications, corporate blogs, and user-generated content platforms.
Statuses and Transitions
draft → review → approved → published → archived
↑ ↓
└── rejected ←───┘
└── revision_needed ←───── (partial return)
content_states (
id, content_type, content_id,
status: draft | review | approved | published | rejected | archived | scheduled,
assigned_to (editor/moderator id),
comment, -- rejection comment
scheduled_at, -- publication date for scheduled
published_at, archived_at,
transitioned_by, transitioned_at
)
content_state_history (
id, content_type, content_id,
from_status, to_status,
changed_by, comment, changed_at
)
Defining Allowed Transitions
class ContentWorkflow
{
private array $transitions = [
'draft' => ['review'],
'review' => ['approved', 'rejected', 'revision_needed'],
'approved' => ['published', 'scheduled'],
'rejected' => ['draft'],
'revision_needed' => ['draft'],
'published'=> ['archived', 'draft'], // return to draft = depublication
'scheduled'=> ['published', 'draft']
];
private array $permissions = [
'draft → review' => 'content.submit_for_review',
'review → approved' => 'content.approve',
'review → rejected' => 'content.approve',
'approved → published' => 'content.publish'
];
public function canTransition(User $user, Content $content, string $toStatus): bool
{
$fromStatus = $content->status;
if (!in_array($toStatus, $this->transitions[$fromStatus] ?? [])) {
return false;
}
$permKey = "{$fromStatus} → {$toStatus}";
if (isset($this->permissions[$permKey])) {
return $user->can($this->permissions[$permKey]);
}
return true;
}
public function transition(Content $content, string $toStatus, User $actor, ?string $comment = null): void
{
if (!$this->canTransition($actor, $content, $toStatus)) {
throw new WorkflowException("Transition {$content->status} → {$toStatus} unavailable");
}
DB::transaction(function () use ($content, $toStatus, $actor, $comment) {
ContentStateHistory::create([
'content_type' => get_class($content),
'content_id' => $content->id,
'from_status' => $content->status,
'to_status' => $toStatus,
'changed_by' => $actor->id,
'comment' => $comment
]);
$content->update([
'status' => $toStatus,
'published_at' => $toStatus === 'published' ? now() : $content->published_at
]);
event(new ContentStatusChanged($content, $toStatus, $actor, $comment));
});
}
}
Reviewer Assignment
// On review submission — auto-assign free editor
class AssignReviewer
{
public function assign(Content $content): User
{
$reviewer = User::where('role', 'editor')
->withCount(['assignedContent' => fn($q) => $q->where('status', 'review')])
->orderBy('assigned_content_count') // least loaded
->first();
$content->update(['assigned_to' => $reviewer->id]);
$reviewer->notify(new ContentAssignedForReview($content));
return $reviewer;
}
}
Deadlines and Reminders
// Scheduled job: find content pending review for more than X hours
$overdue = Content::where('status', 'review')
->where('submitted_for_review_at', '<', now()->subHours(24))
->get();
foreach ($overdue as $content) {
$content->assignedEditor?->notify(new ReviewOverdueNotification($content));
Notification::sendToRole('chief_editor', new EscalationNotification($content));
}
Scheduled Publishing
class PublishScheduledContent implements ShouldQueue
{
public function handle(): void
{
Content::where('status', 'scheduled')
->where('scheduled_at', '<=', now())
->each(function (Content $content) {
app(ContentWorkflow::class)->transition(
$content, 'published', User::find($content->created_by)
);
});
}
}
Job runs every 5 minutes via scheduler.
Editorial Dashboard Interface
Columns by status (Kanban-like view) or list with filters. For each entry:
- Current status and responsible person
- Available transition buttons (depend on role)
- Moderator comments
- Status change history
Development timeline: 3–5 weeks for full workflow system with roles, assignment, deadlines, and history.







