Anyone can publish anything. That's a recipe for chaos. In production Wagtail sites, you need content governance — editors draft, reviewers approve, publishers ship. Wagtail Workflows gives you exactly that: configurable approval pipelines, task assignments, and status tracking. In this lesson, you'll build complete moderation workflows from scratch.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Understand what Wagtail workflows are and when to use them
  • Create workflow tasks with group-based approvals
  • Assign workflows to specific page types and sections
  • Submit pages for moderation and manage approvals
  • Build custom workflow tasks with Python
  • Configure email notifications for workflow events
  • Design a complete content governance strategy

2. Why Workflows Matter (Real-World Scenario)

Real-world scenario: Your marketing team has 15 content editors, 3 reviewers, and 1 publishing manager. Editors write blog posts, reviewers check for accuracy and brand compliance, and the manager gives final approval. Without workflows, anyone can publish anything — incorrect pricing, outdated branding, or even sensitive data goes live instantly. With Wagtail workflows, content flows through defined stages: Draft → In Review → Approved → Published.

Why workflows matter for your organization:

  • Quality Control: Every piece of content gets reviewed before going live
  • Accountability: Every action is logged — who approved what, when
  • Compliance: Meet regulatory requirements for content approval (finance, healthcare, legal)
  • Scalability: As your team grows, workflows prevent chaos
  • Audit Trail: Complete revision history tied to workflow states

3. Core Concepts

Before building workflows, understand the building blocks:

  • Workflow: A pipeline of stages content must pass through (e.g., Draft → Review → Approval → Publish)
  • Task: A step in the workflow, usually assigned to a group of users
  • Workflow State: Tracks where a specific page is in the workflow (In Progress, Needs Approval, Changes Requested, Approved)
  • Task State: Tracks the status of each task within the workflow
  • Page Revision: Each workflow action creates a new revision, preserving the full audit trail

Workflow Lifecycle

  1. Editor creates or edits a page (starts in Draft)
  2. Editor submits page for moderation → Workflow starts
  3. First task group receives notification → Reviews the page
  4. Reviewer approves or requests changes
  5. If approved, page moves to next task or gets published
  6. If changes requested, it bounces back to the editor
  7. Editor revises and resubmits → Process repeats

4. Hands-On Practice: Building a Moderation Workflow

Prerequisite Setup

You need an existing Wagtail project with at least one page model. If you're following from Lesson 1, use that project. Make sure you have at least two staff user accounts in different groups.

# Create groups for workflow roles
python manage.py shell

# In the Django shell:
from django.contrib.auth.models import Group, Permission

# Create editor group
editor_group, _ = Group.objects.get_or_create(name='Content Editors')

# Create reviewer group
reviewer_group, _ = Group.objects.get_or_create(name='Content Reviewers')

# Create publisher group
publisher_group, _ = Group.objects.get_or_create(name='Publishers')

print('Groups created successfully!')
exit()
Create user groups for workflow roles

Now create users in each group via the Django admin or the command line.

# In Django shell
from django.contrib.auth.models import User, Group

editor_group = Group.objects.get(name='Content Editors')
reviewer_group = Group.objects.get(name='Content Reviewers')
publisher_group = Group.objects.get(name='Publishers')

# Create editor user
editor = User.objects.create_user('alice', 'alice@example.com', 'password123')
editor.groups.add(editor_group)
editor.is_staff = True
editor.save()

# Create reviewer user
reviewer = User.objects.create_user('bob', 'bob@example.com', 'password123')
reviewer.groups.add(reviewer_group)
reviewer.is_staff = True
reviewer.save()

# Create publisher user
publisher = User.objects.create_user('carol', 'carol@example.com', 'password123')
publisher.groups.add(publisher_group)
publisher.is_staff = True
publisher.save()

print('Users created: alice (editor), bob (reviewer), carol (publisher)')
Create users for each workflow role

Step 1: Create a Workflow Task

Tasks define who needs to approve content at each stage. The simplest task is a GroupApprovalTask — any member of the assigned group can approve or reject.

# In Django shell
from wagtail.models import GroupApprovalTask
from django.contrib.auth.models import Group

reviewer_group = Group.objects.get(name='Content Reviewers')

# Create a review task
review_task = GroupApprovalTask.objects.create(
    name='Editorial Review',
    description='Content must be reviewed for accuracy, brand compliance, and quality',
)
review_task.groups.add(reviewer_group)
review_task.save()

print(f'Created task: {review_task.name}')

# Create an approval task
publisher_group = Group.objects.get(name='Publishers')
approval_task = GroupApprovalTask.objects.create(
    name='Final Approval',
    description='Final sign-off before publication',
)
approval_task.groups.add(publisher_group)
approval_task.save()

print(f'Created task: {approval_task.name}')
Create GroupApprovalTask for reviewers and publishers

Step 2: Create a Workflow

A workflow chains tasks together in order. Content must pass through each task sequentially to get published.

# In Django shell
from wagtail.models import Workflow

review_task = GroupApprovalTask.objects.get(name='Editorial Review')
approval_task = GroupApprovalTask.objects.get(name='Final Approval')

# Create the workflow with ordered tasks
workflow = Workflow.objects.create(
    name='Standard Editorial Workflow',
    active=True,
)

# Attach tasks in order (creates WorkflowTask objects)
workflow.workflow_tasks.create(task=review_task, sort_order=1)
workflow.workflow_tasks.create(task=approval_task, sort_order=2)

print(f'Created workflow: {workflow.name} with {workflow.workflow_tasks.count()} tasks')
print('Order: Editorial Review → Final Approval')
Create a two-stage workflow

Step 3: Assign Workflow to Page Types

Now tell Wagtail which pages use this workflow. You can assign it to specific page types or all pages.

# In Django shell
from wagtail.models import Workflow, Page

workflow = Workflow.objects.get(name='Standard Editorial Workflow')

# Method 1: Assign workflow to a specific page (and all children)
# This applies the workflow to the entire blog section
blog_index = Page.objects.get(slug='blog')
workflow_on_blog = workflow.workflow_pages.create(page=blog_index)

print(f'Workflow "{workflow.name}" assigned to "{blog_index.title}" and all subpages')

# Method 2: Assign to all pages of a specific content type
# (Useful for site-wide policies)
# Go to Settings → Workflows in the admin UI instead

# Verify assignments
print(f'\nActive workflow assignments:')
for wp in workflow.workflow_pages.all():
    print(f'  - {wp.page.title} (and subpages)')
Assign workflow to the blog section

Step 4: Submit a Page for Moderation (As an Editor)

Now log in as alice (the editor) in a new browser session. Create or edit a blog post, then submit it for moderation.

From the Wagtail admin:

  1. Navigate to the blog section and open a page for editing
  2. Make your changes
  3. In the bottom action bar, click the dropdown arrow next to "Save Draft"
  4. Select "Submit for Moderation"
  5. A confirmation message appears — the page is now in the workflow
kubectl get pods -w
Page submitted for moderation.
Workflow: Standard Editorial Workflow
Step 1 of 2: Editorial Review (awaiting Content Reviewers)
Confirmation after submitting for moderation

Step 5: Review and Approve (As a Reviewer)

Log in as bob (the reviewer). You'll see a notification badge on the Wagtail admin.

From the Wagtail admin:

  1. Look for the Workflows menu item in the left sidebar (or click the notification bell icon)
  2. You'll see pages awaiting your review
  3. Click the page title to view it
  4. Review the content carefully
  5. In the bottom bar, choose either:
    • "Approve" — sends it to the next workflow step
    • "Request Changes" — sends it back to the editor with comments
kubectl get pods -w
Workflows
=========

Awaiting your review:
- My Blog Post (submitted by alice, 5 minutes ago)
Step 1: Editorial Review

Actions: [Approve] [Request Changes]
Reviewer's workflow dashboard

Click Approve. The page moves to step 2 — Final Approval, now awaiting the publisher group.

Step 6: Final Approval and Publication (As a Publisher)

Log in as carol (the publisher). You'll see the page awaiting final approval.

  1. Navigate to the Workflows section
  2. Review the page (and optionally edit it)
  3. Click "Approve"
  4. Since this is the final task, Wagtail publishes the page immediately
kubectl get pods -w
Workflow completed!
"My Blog Post" has been published.

Workflow: Standard Editorial Workflow
Step 1: Editorial Review ✅ Approved by bob
Step 2: Final Approval ✅ Approved by carol (published)
Completed workflow with full audit trail

Step 7: Requesting Changes (Rejection Flow)

What happens when a reviewer finds issues?

  1. Reviewer clicks "Request Changes"
  2. A comment form appears — the reviewer must explain what needs fixing
  3. The page status changes to "Changes Requested"
  4. The editor receives a notification
  5. Editor views the comments, makes revisions, and resubmits
  6. The workflow restarts from the first task
# Programmatically check workflow status
from wagtail.models import WorkflowState

page = MyPage.objects.get(slug='my-blog-post')
latest_revision = page.get_latest_revision()
workflow_state = latest_revision.workflow_states.last()

if workflow_state:
    print(f'Status: {workflow_state.status}')
    print(f'Workflow: {workflow_state.workflow.name}')
    print(f'Current step: {workflow_state.current_task_state.task.name}')
    
    for ts in workflow_state.task_states.all():
        print(f'  {ts.task.name}: {ts.status}')
        if ts.finished_by:
            print(f'    Finished by: {ts.finished_by}')
            print(f'    Finished at: {ts.finished_at}')
else:
    print('No workflow active')
Programmatic workflow status check

5. Working with Workflows from the Admin UI

So far we used the Django shell, but you can do everything from the Wagtail admin too. Here's where to find each feature:

  • Settings → Workflows — Create and manage workflow definitions
  • Settings → Workflow Tasks — Create GroupApprovalTasks and custom tasks
  • Pages → Workflows tab — See all pages currently in workflows
  • Dashboard / notification bell — Review tasks assigned to you
  • Edit page → bottom bar — Submit, approve, or request changes

Assigning Workflows in the Admin

  1. Go to Settings → Workflows
  2. Click your workflow name
  3. Go to the "Pages" tab
  4. Click "Add pages" and select which sections use this workflow
  5. You can assign different workflows to different parts of the site

Pro tip: Assign workflows high up in the page tree (like your blog index or homepage). Child pages automatically inherit the workflow.

6. Custom Workflow Tasks

Sometimes you need more than just "approve or reject." Custom tasks let you run Python code during workflow execution — for example, running a spell-check, validating metadata, or calling an external API.

# home/workflow_tasks.py

from wagtail.models import Task
from wagtail.models.task import TaskState

class SpellCheckTask(Task):
    """
    A custom task that checks page content for spelling errors.
    If errors are found, the page is rejected automatically.
    """
    
    def start(self, task_state: TaskState, user=None):
        """Called when the workflow reaches this task."""
        page = task_state.revision.as_object()
        
        # Simple spell check on the page body
        from spellchecker import SpellChecker
        spell = SpellChecker()
        
        # Extract text from the page body (adjust for your model)
        body_text = page.body  # or your body field
        words = body_text.lower().split()
        
        misspelled = spell.unknown(words)
        
        if misspelled:
            # Reject automatically with details
            task_state.reject(
                user=user,
                comment=f'Spell check failed: {len(misspelled)} errors found.'
            )
        else:
            # Auto-approve
            task_state.approve(user=user)
    
    
    class Meta:
        app_label = 'home'
        verbose_name = 'Spell Check Task'
Custom workflow task that validates content
# Register the custom task
# First install the spellchecker library
pip install pyspellchecker

# Then make migrations and migrate
python manage.py makemigrations
python manage.py migrate

# Now it appears in Workflow Tasks admin
# You can use it like any other task!
Register and use the custom task

7. Email Notifications for Workflows

Wagtail can send email notifications when pages need review. Configure this in your settings.

# settings/base.py

# Wagtail email notifications
WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'noreply@example.com'
WAGTAILADMIN_NOTIFICATION_USE_HTML = True

# Notifications are enabled by default for workflow events.
# Wagtail notifies:
# - The task group when a page is submitted
# - The submitter when a page is approved or changes are requested
# - The submitter when the workflow completes

# Customize email subject prefix
WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS = True
Configure email notifications for workflow events

8. Common Errors & Solutions

  • Error: "You don't have permission to submit this page for moderation"
    Solution: The user needs at least change permission on the page. Make sure staff users have proper page permissions set.
  • Error: Workflow option doesn't appear in the page editor
    Solution: The page type or parent page doesn't have a workflow assigned. Go to Settings → Workflows and verify the assignment.
  • Error: "Cannot submit for moderation because the page is locked"
    Solution: Another user is editing the page. Wait for them to finish or force-unlock from the page listing.
  • Error: Reviewer sees blank tasks in workflows dashboard
    Solution: The reviewer isn't in the correct group. Verify group membership in Users → Groups in admin.
  • Error: Email notifications not sending
    Solution: Check email backend configuration. For development, use EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' to test.

9. Summary Checklist

  • ✓ I understand Wagtail workflows and when to use them
  • ✓ I can create GroupApprovalTask for different roles
  • ✓ I can chain tasks into a workflow
  • ✓ I can assign workflows to page types and sections
  • ✓ I submitted a page for moderation and saw the workflow run
  • ✓ I approved and rejected content as a reviewer
  • ✓ I completed a full editorial workflow
  • ✓ I understand how to build custom workflow tasks
  • ✓ I configured email notifications
  • ✓ I built a complete content governance strategy

10. Practice Exercise (Try to Solve Yourself)

Challenge: Build a three-stage workflow for a news website:

  • Stage 1: Copy Editor — checks grammar, style, and factual accuracy
  • Stage 2: Legal Review — ensures no compliance issues
  • Stage 3: Editor-in-Chief — final sign-off

Requirements:

  • Create three groups: Copy Editors, Legal Team, Executive Editors
  • Create a GroupApprovalTask for each group
  • Chain them into a workflow in the correct order
  • Assign the workflow to a News section
  • Test with users in each role

Bonus: Create a custom task that automatically checks pages for blocked words or sensitive terms and rejects them before human review.

11. Next Steps

Next lesson: Wagtail Headless CMS — Using Wagtail as a Backend with Modern Frontends

You'll learn to:

  • Expose Wagtail content via REST and GraphQL APIs
  • Build a Next.js frontend that consumes Wagtail content
  • Implement live preview for headless setups
  • Handle authentication and draft previews

Practice resources:

Common pitfalls to avoid:

  • ❌ Forgetting that child pages inherit parent workflows — assign high in the tree
  • ❌ Assigning too many groups to a single task — keep approval groups focused
  • ❌ Not testing the full workflow path with actual user accounts
  • ❌ Custom tasks that run indefinitely — always test with a timeout mechanism
  • ❌ Ignoring notification settings — users won't know they have pending reviews

AI Agent

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen June 24, 2026

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment