# 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**: 1. **`status`** - Editorial workflow status - `draft` - Being created/edited - `review` - Submitted for review - `approved` - Ready for publishing - `published` - Legacy (not used for external publishing) 2. **`site_status`** - External site publishing status - `not_published` - Not yet published to WordPress - `scheduled` - Has a scheduled_publish_at time - `publishing` - Currently being published - `published` - Successfully published to WordPress - `failed` - 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: 1. Finds all sites with `PublishingSettings.auto_publish_enabled = True` 2. Gets approved content (`status='approved'`, `site_status='not_published'`, `scheduled_publish_at=null`) 3. 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 day - `weekly_publish_limit` - max posts per week - `monthly_publish_limit` - max posts per month 4. Assigns `scheduled_publish_at` datetime and sets `site_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: 1. Finds all content where: - `site_status='scheduled'` - `scheduled_publish_at <= now` 2. For each content item: - Updates `site_status='publishing'` - Gets the site's WordPress integration - Queues `publish_content_to_wordpress` Celery task 3. 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: 1. **Load Content & Integration** - Gets content and WordPress credentials 2. **Check Already Published** - Skips if `external_id` exists 3. **Generate Excerpt** - Creates excerpt from HTML content 4. **Get Taxonomy Terms** - Loads categories and tags from `ContentTaxonomy` 5. **Get Images** - Loads featured image and gallery images 6. **Build API Payload** - Constructs WordPress REST API payload 7. **Call WordPress API** - POSTs to WordPress via IGNY8 Bridge plugin 8. **Update Content** - Sets `external_id`, `external_url`, `site_status='published'` 9. **Log Sync Event** - Records in `SyncEvent` model #### 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: - `status` tracks editorial approval - `site_status` tracks external publishing - Content typically has `status='approved'` AND `site_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`: ```python 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 ```bash 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 ```python # 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 ```python # 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_wordpress` has `max_retries=3` - Automatic retry on transient failures - Failed content marked with `site_status='failed'` --- ## Troubleshooting ### Content Not Being Scheduled 1. Check `PublishingSettings.auto_publish_enabled` is `True` 2. Verify content has `status='approved'` and `site_status='not_published'` 3. Check `scheduled_publish_at` is null (already scheduled content won't reschedule) 4. Verify publish limits haven't been reached ### Content Not Publishing 1. Check Celery Beat is running: `docker compose logs igny8_celery_beat` 2. Check Celery Worker is running: `docker compose logs igny8_celery_worker` 3. Look for errors in worker logs 4. Verify WordPress integration is active 5. Test WordPress API connectivity ### Resetting Failed Content ```python # 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 │ │ │ └─────────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────────┘ ``` --- ## Related Documentation - [Publisher Module](../10-MODULES/PUBLISHER.md) - [WordPress Integration](../60-PLUGINS/WORDPRESS-INTEGRATION.md) - [Content Pipeline](CONTENT-PIPELINE.md)