18 KiB
Scheduled Content Publishing Workflow
Last Updated: January 16, 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.
Current System (v2.0):
- WordPress credentials stored directly on
Sitemodel (wp_api_key,domain) - No
SiteIntegrationmodel required - Publishing via
PublisherService(not legacy Celery tasks) - API endpoint:
POST /api/v1/publisher/publish
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:
- Validates WordPress configuration exists on Site (
wp_api_key,domain) - Updates
site_status='publishing' - Calls
PublisherService.publish_content()directly (synchronous) - Updates
site_status='published'on success or'failed'on error
- Validates WordPress configuration exists on Site (
- Logs results and any errors
Current Implementation (v2.0):
- Uses
PublisherService(current system) - Gets config from
Site.wp_api_keyandSite.domain - No
SiteIntegrationrequired - Synchronous publishing (not queued to Celery)
3. PublisherService.publish_content
Type: Service method (called by scheduler)
File: backend/igny8_core/business/publishing/services/publisher_service.py
What It Does:
- Load Content - Gets content by ID
- Get WordPress Config - Reads from
Site.wp_api_keyandSite.domain - Call Adapter - Uses
WordPressAdapterto handle API communication - WordPress API Call - POSTs to
{domain}/wp-json/igny8/v1/publish - Update Content - Sets
external_id,external_url,site_status='published' - Create Record - Logs in
PublishingRecordmodel
WordPress Connection:
- Uses the IGNY8 WordPress Bridge plugin installed on the site
- API endpoint:
{site.domain}/wp-json/igny8/v1/publish - Authentication: API key from
Site.wp_api_key - No SiteIntegration model needed
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 be published immediately or rescheduled via API:
Publish Now
POST /api/v1/publisher/publish
{
"content_id": 123,
"destinations": ["wordpress"]
}
Schedule for Later
POST /api/v1/writer/content/{id}/schedule/
{
"scheduled_at": "2026-01-20T14:00:00Z"
}
Reschedule Failed Content
POST /api/v1/writer/content/{id}/reschedule/
{
"scheduled_at": "2026-01-20T15:00:00Z"
}
Note: Reschedule works from any site_status (failed, published, scheduled, etc.)
Unschedule
POST /api/v1/writer/content/{id}/unschedule/
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 WordPress API key | Site.wp_api_key is empty | Configure API key in Site settings |
| No domain configured | Site.domain is empty | Set domain 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 | Use reschedule to republish |
Retry Policy
- Failed content marked with
site_status='failed' - Use the
rescheduleaction to retry publishing - Can reschedule from any status (failed, published, etc.)
Rescheduling Failed Content
# Via API
POST /api/v1/writer/content/{id}/reschedule/
{
"scheduled_at": "2026-01-20T15:00:00Z"
}
# Via Django shell
from igny8_core.business.content.models import Content
from django.utils import timezone
from datetime import timedelta
failed_content = Content.objects.filter(site_status='failed')
for content in failed_content:
# Reschedule for 1 hour from now
content.site_status = 'scheduled'
content.scheduled_publish_at = timezone.now() + timedelta(hours=1)
content.site_status_updated_at = timezone.now()
content.save(update_fields=['site_status', 'scheduled_publish_at', 'site_status_updated_at'])
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 Site has
wp_api_keyconfigured - Verify Site has
domainconfigured - Test WordPress API connectivity
Resetting Failed Content
Use the reschedule API endpoint:
# Reschedule single content
curl -X POST https://api.igny8.com/api/v1/writer/content/344/reschedule/ \
-H "Authorization: Api-Key YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"scheduled_at": "2026-01-20T15:00:00Z"}'
Or via Django shell:
# Reset all failed content to reschedule in 1 hour
from igny8_core.business.content.models import Content
from django.utils import timezone
from datetime import timedelta
failed_content = Content.objects.filter(site_status='failed')
for content in failed_content:
content.site_status = 'scheduled'
content.scheduled_publish_at = timezone.now() + timedelta(hours=1)
content.site_status_updated_at = timezone.now()
content.save(update_fields=['site_status', 'scheduled_publish_at', 'site_status_updated_at'])
print(f"Rescheduled content {content.id}")
Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ IGNY8 Backend │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────Validate Site.wp_api_key & domain │ │
│ │ - Call PublisherService.publish_content() │ │
│ │ │ │
│ │ 3. PublisherService │ │
│ │ - Get config from Site model │ │
│ │ - Call WordPressAdapter │ │
│ │ - Update content status │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
└───────────────────────────────────┼─────────────────────────────┘
│
▼ HTTPS
┌───────────────────────────────────────────────────────────────┐
│ WordPress Site │
├───────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ IGNY8 Bridge Plugin │ │
│ │ │ │
│ │ /wp-json/igny8/v1/publish │ │
│ │ - Receives content payload │ │
│ │ - Creates/updates WordPress post │ │
│ │ - Handles images, categories, tags │ │
│ │ - Returns post ID and URL │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
API Reference
Schedule Content
POST /api/v1/writer/content/{id}/schedule/
Authorization: Api-Key YOUR_KEY
Content-Type: application/json
{
"scheduled_at": "2026-01-20T14:00:00Z"
}
Reschedule Content (Failed or Any Status)
POST /api/v1/writer/content/{id}/reschedule/
Authorization: Api-Key YOUR_KEY
Content-Type: application/json
{
"scheduled_at": "2026-01-20T15:00:00Z"
}
Unschedule Content
POST /api/v1/writer/content/{id}/unschedule/
Authorization: Api-Key YOUR_KEY
Publish Immediately
POST /api/v1/publisher/publish
Authorization: Api-Key YOUR_KEY
Content-Type: application/json
{
"content_id": 123,
"destinations": ["wordpress"]
}│
│ └────────────────────────────────────────────────────────┘ │
│ │ │
└───────────────────────────────────┼─────────────────────────────┘
│
▼ 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 │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘