Summary
Studio's Socials tab Submit-for-Review path creates regular WP posts on blog 12 with social meta (`studio_social*`) attached. When those posts publish, `on_publish_crosspost` fires DM's `SocialCrossPostTask` to broadcast to social platforms. The post itself stays around indefinitely as a record.
This issue tracks the cleanup primitive that removes those posts after broadcast completes successfully. Social posts are NOT editorial content — they're a vehicle for cross-posting. They should not accumulate on Studio (or anywhere) past the point where their broadcast is confirmed complete.
Why SystemTask, not Retention API
DM core has both:
-
Retention API (`wp datamachine retention run`) — generic age-based pruning of well-defined DM-owned tables (jobs, logs, processed items, AS actions, chat sessions). Domains are hardcoded in `RetentionCommand::run()`. Adding a new domain requires a PR to data-machine core.
-
SystemTask API (`wp datamachine system run `) — extension-friendly, registry-based. Plugins register tasks via `add_filter('datamachine_tasks', ...)`. Each task is a class extending `SystemTask`. Studio already uses this for the giveaway task (`'giveaway' => GiveawayTask::class`).
Social post cleanup is non-DM data (WP posts on Studio, with custom meta) and has business-specific logic (what counts as 'fully broadcast' is a Studio decision, not DM core's concern). This belongs in a SystemTask, not the Retention API.
Proposed Architecture
`SocialPostCleanupTask` extends `SystemTask`
Lives in `extrachill-studio/inc/tasks/social-post-cleanup-task.php`. Registered via:
```php
add_filter( 'datamachine_tasks', function ( array $tasks ): array {
$tasks['social_post_cleanup'] = \ExtraChillStudio\Tasks\SocialPostCleanupTask::class;
return $tasks;
} );
```
Two execution modes
```php
public function executeTask( array $params ) {
$mode = $params['mode'] ?? 'sweep';
if ( 'single' === \$mode ) {
return \$this->cleanupSinglePost( (int) ( \$params['post_id'] ?? 0 ) );
}
return \$this->cleanupSweep();
}
```
Single mode — surgical cleanup of one specific post. Used by the immediate-cleanup trigger (post_id passed in by the SocialCrossPostTask completion hook).
Sweep mode — query all eligible posts, delete in batch. Used by the weekly cron safety net.
Eligibility criteria
A post is eligible for cleanup when ALL of the following:
- `post_status === 'publish'` (broadcast already happened — this is what fires the cross-post task in the first place)
- `get_post_meta( post_id, '_studio_social_publish_log', true )` shows successful broadcast on EVERY targeted platform in `_studio_social_platforms`
- `post_date` is older than the configured retention window (default 7 days, filterable)
The age check matters even in immediate mode — it gives editors a small window to inspect the publish log if something went wrong with the broadcast. Cleanup runs immediately on success but the actual delete waits for the post to clear the retention window.
Actually — open question. See 'Trigger options' below.
Filter for retention window
```php
$days = (int) apply_filters( 'extrachill_studio_social_post_retention_days', 7 );
```
Defaults to 7. Sites can pin to 0 (delete immediately on success) or 30 (keep posts around for a month) by hooking the filter.
Trigger Options (pick one for Phase 1)
Option A: Immediate cleanup on broadcast success
Hook into `SocialCrossPostTask` completion. As soon as every platform reports success, schedule a single-mode cleanup task with `post_id`. Posts disappear within seconds of full broadcast.
- ✅ No accumulation
- ✅ Studio stays tidy without operator intervention
- ❌ No window to inspect publish log unless someone happened to be watching
- ❌ Edge case: what if a platform reports success but the post URL ends up dead? You can't go back.
Option B: Weekly sweep only
`wp_schedule_event( time(), 'weekly', 'extrachill_studio_social_cleanup_sweep' )` → invokes the sweep mode of the task.
- ✅ Posts hang around for at least 7 days as a record
- ✅ Single trigger, simple
- ❌ Studio admin shows posts that have been published to social hours ago — confusing UX
- ❌ Up to a week of accumulation
Option C: Configurable retention window with weekly sweep (Recommended)
Default retention window is 7 days (filterable). Weekly sweep uses that window. No immediate-cleanup hook.
- ✅ Posts remain inspectable for the configured period
- ✅ Single trigger, single source of truth (the retention filter)
- ✅ Sites can opt into immediate-cleanup by setting filter to 0 + a daily-or-faster cron
- ❌ Up to 7 days of stale visibility in Studio admin during the window
Option D: Hybrid — immediate cleanup on success + weekly sweep for stragglers
Both triggers active. Immediate firing handles the happy path. Weekly sweep catches posts where one or more platforms failed (so they never hit 'fully successful') and gives the operator a chance to retry before the sweep eventually deletes them.
- ✅ Tidiest under normal conditions
- ✅ Stragglers don't accumulate forever
- ❌ Two triggers, two places to debug
- ❌ Can't both fire — need ordering
Phase 1 Scope (what to ship first)
- ✓ `SocialPostCleanupTask` class with both `single` and `sweep` modes
- ✓ Registration via `datamachine_tasks` filter
- ✓ `extrachill_studio_social_post_retention_days` filter (default 7)
- ✓ Pick a trigger from above (recommend Option C for Phase 1; Option D as enhancement)
- ✓ `wp datamachine system run social_post_cleanup` works for manual operator runs
- ✓ Dry-run support: `--dry-run` reports what would be deleted without deleting
Acceptance Criteria
Out of Scope (for Phase 1)
Relationship to Other Issues
Notes
- The DM `SystemTask` infrastructure is already battle-tested by other tasks (OG card generation, alt text, internal linking, image optimization). Studio uses it for the giveaway task. New tasks plug in cleanly.
- The retention window filter pattern (`apply_filters('extrachill_studio_social_post_retention_days', 7)`) matches DM's own convention (`datamachine_completed_jobs_max_age_days` etc.).
- This task is LOW RISK — only deletes posts that meet strict criteria. Worst case if criteria are wrong: a post survives the sweep and gets cleaned up next time. No data loss across the network.
Summary
Studio's Socials tab Submit-for-Review path creates regular WP posts on blog 12 with social meta (`studio_social*`) attached. When those posts publish, `on_publish_crosspost` fires DM's `SocialCrossPostTask` to broadcast to social platforms. The post itself stays around indefinitely as a record.
This issue tracks the cleanup primitive that removes those posts after broadcast completes successfully. Social posts are NOT editorial content — they're a vehicle for cross-posting. They should not accumulate on Studio (or anywhere) past the point where their broadcast is confirmed complete.
Why SystemTask, not Retention API
DM core has both:
Retention API (`wp datamachine retention run`) — generic age-based pruning of well-defined DM-owned tables (jobs, logs, processed items, AS actions, chat sessions). Domains are hardcoded in `RetentionCommand::run()`. Adding a new domain requires a PR to data-machine core.
SystemTask API (`wp datamachine system run `) — extension-friendly, registry-based. Plugins register tasks via `add_filter('datamachine_tasks', ...)`. Each task is a class extending `SystemTask`. Studio already uses this for the giveaway task (`'giveaway' => GiveawayTask::class`).
Social post cleanup is non-DM data (WP posts on Studio, with custom meta) and has business-specific logic (what counts as 'fully broadcast' is a Studio decision, not DM core's concern). This belongs in a SystemTask, not the Retention API.
Proposed Architecture
`SocialPostCleanupTask` extends `SystemTask`
Lives in `extrachill-studio/inc/tasks/social-post-cleanup-task.php`. Registered via:
```php
add_filter( 'datamachine_tasks', function ( array $tasks ): array {
$tasks['social_post_cleanup'] = \ExtraChillStudio\Tasks\SocialPostCleanupTask::class;
return $tasks;
} );
```
Two execution modes
```php
public function executeTask( array $params ) {
$mode = $params['mode'] ?? 'sweep';
}
```
Single mode — surgical cleanup of one specific post. Used by the immediate-cleanup trigger (post_id passed in by the SocialCrossPostTask completion hook).
Sweep mode — query all eligible posts, delete in batch. Used by the weekly cron safety net.
Eligibility criteria
A post is eligible for cleanup when ALL of the following:
The age check matters even in immediate mode — it gives editors a small window to inspect the publish log if something went wrong with the broadcast. Cleanup runs immediately on success but the actual delete waits for the post to clear the retention window.
Actually — open question. See 'Trigger options' below.
Filter for retention window
```php
$days = (int) apply_filters( 'extrachill_studio_social_post_retention_days', 7 );
```
Defaults to 7. Sites can pin to 0 (delete immediately on success) or 30 (keep posts around for a month) by hooking the filter.
Trigger Options (pick one for Phase 1)
Option A: Immediate cleanup on broadcast success
Hook into `SocialCrossPostTask` completion. As soon as every platform reports success, schedule a single-mode cleanup task with `post_id`. Posts disappear within seconds of full broadcast.
Option B: Weekly sweep only
`wp_schedule_event( time(), 'weekly', 'extrachill_studio_social_cleanup_sweep' )` → invokes the sweep mode of the task.
Option C: Configurable retention window with weekly sweep (Recommended)
Default retention window is 7 days (filterable). Weekly sweep uses that window. No immediate-cleanup hook.
Option D: Hybrid — immediate cleanup on success + weekly sweep for stragglers
Both triggers active. Immediate firing handles the happy path. Weekly sweep catches posts where one or more platforms failed (so they never hit 'fully successful') and gives the operator a chance to retry before the sweep eventually deletes them.
Phase 1 Scope (what to ship first)
Acceptance Criteria
Out of Scope (for Phase 1)
Relationship to Other Issues
Notes