22 KiB
Scheduled Content Publishing Workflow
Last Updated: January 20, 2026
Version: 1.8.4
Module: Publishing / Automation
Status: ✅ Site filtering fixed and verified
Overview
IGNY8 provides automated content publishing to WordPress, Shopify, and custom sites. Content goes through a scheduling process before being published at the designated time.
Current System (v2.0):
- Site credentials stored directly on
Sitemodel (api_key,domain,platform_type) - Multi-platform support: WordPress, Shopify, Custom APIs
- No
SiteIntegrationmodel required - Publishing via
PublisherService(not legacy Celery tasks) - API endpoint:
POST /api/v1/publisher/publish - Site Filtering: All content queries filtered by
site_id(fixed Jan 2026)
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 status (platform-agnostic)not_published- Not yet published to any sitescheduled- Has a scheduled_publish_at timepublishing- Currently being publishedpublished- Successfully published to site (WordPress, Shopify, or Custom)failed- Publishing failed
Note: site_status works across all platforms (WordPress, Shopify, Custom) and is filtered by site_id to show only content for the selected site.
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 │
└─────────┘ └────────┘
Frontend Integration
Site Selector & Content Filtering
Content Calendar (frontend/src/pages/Publisher/ContentCalendar.tsx):
- Automatically filters all content by selected site
- Reloads data when site selector changes
- Shows scheduled, publishing, published, and failed content for active site only
Critical Implementation Details:
- All API queries include
site_id: activeSite.idparameter - Backend
ContentFilterincludessite_idin filterable fields - useEffect hook reacts to
activeSite?.idchanges to trigger reload - Content cleared when no site selected
Site Selector Fix (Jan 2026):
- Fixed circular dependency in useEffect (lines 285-294)
- Only depends on
activeSite?.id, not on callback functions - Added console logging for debugging site changes
- Pattern follows Dashboard and Approved pages
Backend Filter Configuration:
# backend/igny8_core/modules/writer/views.py
class ContentFilter(django_filters.FilterSet):
class Meta:
model = Content
fields = [
'cluster_id',
'site_id', # Required for site filtering
'status',
'site_status', # Required for status filtering
'content_type',
# ...
]
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
Frontend Debugging
Browser Console Logs: When changing sites in Content Calendar, you should see:
[ContentCalendar] Site changed to: 45 My Site Name
[ContentCalendar] Triggering loadQueue...
Check Site Filtering:
- Open browser DevTools → Network tab
- Change site in site selector
- Look for API calls to
/api/v1/writer/content/ - Verify
site_idparameter is included in query string - Verify count matches database for that site
Common Issues:
- No console logs when changing sites → useEffect not triggering (refresh page)
- API calls missing
site_idparameter → backend filter not working - Wrong count displayed → database query issue or cache problem
Backend 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]
# Check scheduled content for specific site
site_id = 45
Content.objects.filter(
site_id=site_id,
site_status='scheduled'
).count()
# Compare with frontend display
# Should match count shown in Content Calendar for that site
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
Site Selector Not Updating Content Calendar
Symptoms:
- Changing sites doesn't reload calendar content
- Count shows wrong number of scheduled items
- Content from wrong site displayed
Solution:
- Hard refresh browser: Ctrl+F5 (Windows/Linux) or Cmd+Shift+R (Mac)
- Clear browser cache
- Check console logs: Should see
[ContentCalendar] Site changed to: ... - Verify API calls: Check Network tab for
site_idparameter
What Was Fixed (Jan 2026):
- Frontend: Fixed useEffect circular dependency in ContentCalendar.tsx
- Backend: Added
site_idandsite_statusto ContentFilter fields - All API queries now properly filter by
site_id: activeSite.id - Content clears when no site selected
If Problem Persists:
# Check backend filter configuration
grep -n "site_id" backend/igny8_core/modules/writer/views.py
# Should show site_id in ContentFilter.Meta.fields
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
- Verify correct site selected in site selector
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 │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘