Files
igny8/docs/40-WORKFLOWS/SCHEDULED-CONTENT-PUBLISHING.md
2026-01-16 13:28:24 +00:00

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 Site model (wp_api_key, domain)
  • No SiteIntegration model 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:

  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:
    • 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
  3. Logs results and any errors

Current Implementation (v2.0):

  • Uses PublisherService (current system)
  • Gets config from Site.wp_api_key and Site.domain
  • No SiteIntegration required
  • 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:

  1. Load Content - Gets content by ID
  2. Get WordPress Config - Reads from Site.wp_api_key and Site.domain
  3. Call Adapter - Uses WordPressAdapter to handle API communication
  4. WordPress API Call - POSTs to {domain}/wp-json/igny8/v1/publish
  5. Update Content - Sets external_id, external_url, site_status='published'
  6. Create Record - Logs in PublishingRecord model

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

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:

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 reschedule action 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

  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 Site has wp_api_key configured
  5. Verify Site has domain configured
  6. 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                              │  │
│  └─────────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────┘