15 KiB
Scheduled Content Publishing Workflow
Last Updated: January 12, 2026
Module: Publishing / Automation
Overview
IGNY8 provides automated content publishing to WordPress sites. Content goes through a scheduling process before being published at the designated time.
Content Lifecycle for Publishing
Understanding Content.status vs Content.site_status
Content has TWO separate status fields:
-
status- Editorial workflow statusdraft- Being created/editedreview- Submitted for reviewapproved- Ready for publishingpublished- Legacy (not used for external publishing)
-
site_status- External site publishing statusnot_published- Not yet published to WordPressscheduled- Has a scheduled_publish_at timepublishing- Currently being publishedpublished- Successfully published to WordPressfailed- Publishing failed
Publishing Flow
┌─────────────────┐
│ DRAFT │ ← Content is being created/edited
│ status: draft │
└────────┬────────┘
│ User approves content
▼
┌─────────────────┐
│ APPROVED │ ← Content is ready for publishing
│ status: approved│ status='approved', site_status='not_published'
│ site_status: │
│ not_published │
└────────┬────────┘
│ Hourly: schedule_approved_content task
▼
┌─────────────────┐
│ SCHEDULED │ ← Content has a scheduled_publish_at time
│ status: approved│ site_status='scheduled'
│ site_status: │ scheduled_publish_at set to future datetime
│ scheduled │
└────────┬────────┘
│ Every 5 min: process_scheduled_publications task
│ (when scheduled_publish_at <= now)
▼
┌─────────────────┐
│ PUBLISHING │ ← WordPress API call in progress
│ status: approved│ site_status='publishing'
│ site_status: │
│ publishing │
└────────┬────────┘
│
┌────┴────┐
│ │
▼ ▼
┌────────┐ ┌────────┐
│PUBLISHED│ │ FAILED │
│status: │ │status: │
│approved │ │approved│
│site_ │ │site_ │
│status: │ │status: │
│published│ │failed │
└─────────┘ └────────┘
Celery Tasks
1. schedule_approved_content
Schedule: Every hour at :00
Task Name: publishing.schedule_approved_content
File: backend/igny8_core/tasks/publishing_scheduler.py
What It Does:
- Finds all sites with
PublishingSettings.auto_publish_enabled = True - Gets approved content (
status='approved',site_status='not_published',scheduled_publish_at=null) - Calculates available publishing slots based on:
publish_days- which days are allowed (e.g., Mon-Fri)publish_time_slots- which times are allowed (e.g., 09:00, 14:00, 18:00)daily_publish_limit- max posts per dayweekly_publish_limit- max posts per weekmonthly_publish_limit- max posts per month
- Assigns
scheduled_publish_atdatetime and setssite_status='scheduled'
Configuration Location:
PublishingSettings model linked to each Site. Configurable via:
- Admin:
/admin/integration/publishingsettings/ - API:
/api/v1/sites/{site_id}/publishing-settings/
2. process_scheduled_publications
Schedule: Every 5 minutes
Task Name: publishing.process_scheduled_publications
File: backend/igny8_core/tasks/publishing_scheduler.py
What It Does:
- Finds all content where:
site_status='scheduled'scheduled_publish_at <= now
- For each content item:
- Updates
site_status='publishing' - Gets the site's WordPress integration
- Queues
publish_content_to_wordpressCelery task
- Updates
- Logs results and any errors
3. publish_content_to_wordpress
Type: On-demand Celery task (queued by process_scheduled_publications)
Task Name: publishing.publish_content_to_wordpress
File: backend/igny8_core/tasks/wordpress_publishing.py
What It Does:
- Load Content & Integration - Gets content and WordPress credentials
- Check Already Published - Skips if
external_idexists - Generate Excerpt - Creates excerpt from HTML content
- Get Taxonomy Terms - Loads categories and tags from
ContentTaxonomy - Get Images - Loads featured image and gallery images
- Build API Payload - Constructs WordPress REST API payload
- Call WordPress API - POSTs to WordPress via IGNY8 Bridge plugin
- Update Content - Sets
external_id,external_url,site_status='published' - Log Sync Event - Records in
SyncEventmodel
WordPress Connection:
- Uses the IGNY8 WordPress Bridge plugin installed on the site
- API endpoint:
{site_url}/wp-json/igny8-bridge/v1/publish - Authentication: API key stored in
Site.wp_api_key
Database Models
Content Fields (Publishing Related)
| Field | Type | Description |
|---|---|---|
status |
CharField | Editorial workflow: draft, review, approved |
site_status |
CharField | WordPress publishing status: not_published, scheduled, publishing, published, failed |
site_status_updated_at |
DateTimeField | When site_status was last changed |
scheduled_publish_at |
DateTimeField | When content should be published (null if not scheduled) |
external_id |
CharField | WordPress post ID after publishing |
external_url |
URLField | WordPress post URL after publishing |
Important: These are separate concerns:
statustracks editorial approvalsite_statustracks external publishing- Content typically has
status='approved'ANDsite_status='not_published'before scheduling
PublishingSettings Fields
| Field | Type | Description |
|---|---|---|
site |
ForeignKey | The site these settings apply to |
auto_publish_enabled |
BooleanField | Whether automatic scheduling is enabled |
publish_days |
JSONField | List of allowed days: ['mon', 'tue', 'wed', 'thu', 'fri'] |
publish_time_slots |
JSONField | List of times: ['09:00', '14:00', '18:00'] |
daily_publish_limit |
IntegerField | Max posts per day (null = unlimited) |
weekly_publish_limit |
IntegerField | Max posts per week (null = unlimited) |
monthly_publish_limit |
IntegerField | Max posts per month (null = unlimited) |
Celery Beat Schedule
From backend/igny8_core/celery.py:
app.conf.beat_schedule = {
# ...
'schedule-approved-content': {
'task': 'publishing.schedule_approved_content',
'schedule': crontab(minute=0), # Every hour at :00
},
'process-scheduled-publications': {
'task': 'publishing.process_scheduled_publications',
'schedule': crontab(minute='*/5'), # Every 5 minutes
},
# ...
}
Manual Publishing
Content can also be published immediately via:
API Endpoint
POST /api/v1/content/{content_id}/publish/
Admin Action
In Django Admin, select content and use "Publish to WordPress" action.
Monitoring & Debugging
Log Files
- Publish Logs:
backend/logs/publish-sync-logs/ - API Logs:
backend/logs/wordpress_api.log
Check Celery Status
docker compose -f docker-compose.app.yml -p igny8-app logs igny8_celery_worker
docker compose -f docker-compose.app.yml -p igny8-app logs igny8_celery_beat
Check Scheduled Content
# Django shell
from igny8_core.business.content.models import Content
from django.utils import timezone
# Past due content (should have been published)
Content.objects.filter(
site_status='scheduled',
scheduled_publish_at__lt=timezone.now()
).count()
# Upcoming scheduled content
Content.objects.filter(
site_status='scheduled',
scheduled_publish_at__gt=timezone.now()
).order_by('scheduled_publish_at')[:10]
Manual Task Execution
# Django shell
from igny8_core.tasks.publishing_scheduler import (
schedule_approved_content,
process_scheduled_publications
)
# Run scheduling task
schedule_approved_content()
# Process due publications
process_scheduled_publications()
Error Handling
Common Failure Reasons
| Error | Cause | Solution |
|---|---|---|
| No active WordPress integration | Site doesn't have WordPress connected | Configure integration in Site settings |
| API key invalid/expired | WordPress API key issue | Regenerate API key in WordPress plugin |
| Connection timeout | WordPress site unreachable | Check site availability |
| Plugin not active | IGNY8 Bridge plugin disabled | Enable plugin in WordPress |
| Content already published | Duplicate publish attempt | Check external_id field |
Retry Policy
publish_content_to_wordpresshasmax_retries=3- Automatic retry on transient failures
- Failed content marked with
site_status='failed'
Troubleshooting
Content Not Being Scheduled
- Check
PublishingSettings.auto_publish_enabledisTrue - Verify content has
status='approved'andsite_status='not_published' - Check
scheduled_publish_atis null (already scheduled content won't reschedule) - Verify publish limits haven't been reached
Content Not Publishing
- Check Celery Beat is running:
docker compose logs igny8_celery_beat - Check Celery Worker is running:
docker compose logs igny8_celery_worker - Look for errors in worker logs
- Verify WordPress integration is active
- Test WordPress API connectivity
Resetting Failed Content
# Reset failed content to try again
from igny8_core.business.content.models import Content
Content.objects.filter(site_status='failed').update(
site_status='not_published',
scheduled_publish_at=None
)
Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ IGNY8 Backend │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Celery Beat │ │ Celery Worker │ │
│ │ │ │ │ │
│ │ Sends tasks at │───▶│ Executes tasks │ │
│ │ scheduled times │ │ │ │
│ └──────────────────┘ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Publishing Tasks │ │
│ │ │ │
│ │ 1. schedule_approved_content (hourly) │ │
│ │ - Find approved content │ │
│ │ - Calculate publish slots │ │
│ │ - Set scheduled_publish_at │ │
│ │ │ │
│ │ 2. process_scheduled_publications (every 5 min) │ │
│ │ - Find due content │ │
│ │ - Queue publish_content_to_wordpress │ │
│ │ │ │
│ │ 3. publish_content_to_wordpress │ │
│ │ - Build API payload │ │
│ │ - Call WordPress REST API │ │
│ │ - Update content status │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
└───────────────────────────────────┼─────────────────────────────┘
│
▼ HTTPS
┌───────────────────────────────────────────────────────────────┐
│ WordPress Site │
├───────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ IGNY8 Bridge Plugin │ │
│ │ │ │
│ │ /wp-json/igny8-bridge/v1/publish │ │
│ │ - Receives content payload │ │
│ │ - Creates/updates WordPress post │ │
│ │ - Handles images, categories, tags │ │
│ │ - Returns post ID and URL │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘