Goal
Execute a scheduled job run: generate N posts using Brand Voice, route to Inbox for human review.
V2 Changes
- This is THE core job execution skill — called by Celery job runner for scheduled jobs
- Replaces RecurringTemplate logic — jobs are now Agent-owned with this skill as the execution engine
- Agent memory integration — recalls top topics, hook styles, posting times
- Per-account advisory locks — caller (Celery) handles locking to prevent duplicate posts
- Run logging — tokens tracked, cost computed, stored in Run record
- Always routes to Inbox — this skill never schedules directly (use
bolta.cron.generate_and_schedule for that)
When This Skill Is Called
- Trigger: Celery Beat detects
Job.next_run_at <= now and Job.status == active
- Caller:
execute_job_run(job_id, run_id, account_id) Celery task
- Context: Agent's full persona + memory + voice profile injected into system prompt
- Tool access: Filtered by agent type (content_creator gets content tools only)
Policy Rules
- Always route to Inbox — this is the "safe automation" skill
- Respect voice soft-delete — if voice_profile_id is soft-deleted, fail gracefully and pause job
- Idempotency — if run_id already exists with status=completed, skip execution
- Max retries — if this run fails, Celery retries up to Job.max_retries (default 2)
- Escalation on failure — after max retries, create inbox item with error context for human
Steps
Pre-flight checks:
bolta.get_workspace_policy(workspace_id) → extract safe_mode (informational; this skill always routes to inbox)
bolta.get_my_capabilities(workspace_id) → verify agent has content creation capability
bolta.get_voice_profile(voice_profile_id) → verify voice exists and is active
- If soft-deleted: fail with error "Voice profile deleted, job paused" → pause job
- Verify all
account_ids exist and are connected:
bolta.get_account_info(account_id) for each account
- If any account disconnected: fail with error "Account disconnected" → pause job
Load agent memory for context:
bolta.recall(agent_id, "top_performing_topics") → prioritize proven topics
bolta.recall(agent_id, "preferred_hook_style") → use successful hooks
bolta.recall(agent_id, "audience_timezone") → context for timing references
bolta.recall(agent_id, "recent_failures") → avoid repeating mistakes
Load recent posts for consistency:
bolta.list_recent_posts(account_ids, limit=10) → avoid repetition
- Extract: topics covered, hook styles used, engagement levels (if available)
Generate N posts:
For each of n_posts (default 3):
If template_id provided:
bolta.render_template(template_id, {topic_seed, voice_profile_id, agent_memory})
- Use rendered content
Else if topic_seeds provided:
- Pick next seed from array
- Generate post using voice profile + seed + agent memory
Else (no template, no seeds):
- Generate topic based on:
- Agent memory (top performing topics)
- Voice profile (brand focus areas)
- Recent posts (avoid repetition, maintain variety)
run_instructions (job-specific guidance)
Apply run_instructions if provided:
- Job-level override instructions (e.g., "Focus on product features. 3 posts per run. Instagram carousel format.")
- This overrides default agent behavior for this specific job
Create draft:
bolta.draft_post({workspace_id, voice_profile_id, account_ids: [account_id], content, status: "draft"})
- Tag with
run_id, job_id, client_tag in metadata
- Collect
post_id
Route to Inbox:
- All generated posts transition to
inbox state as a batch
- System creates single
inbox_item_id for the bundle
- Inbox item tagged with:
agent_id (who created it)
job_id (which job)
run_id (this execution)
client_tag (for agency filtering)
final_state = inbox
Update agent memory:
bolta.remember(agent_id, "last_successful_run", ISO_timestamp)
bolta.remember(agent_id, "last_run_topics", topics_used)
- If any posts generated unusually well (future enhancement): store successful patterns
Return execution summary:
run_id (for audit trail)
post_ids (array of created drafts)
inbox_item_id (bundle ID for review queue)
final_state (always "inbox")
warnings (any non-fatal issues: missing topics, template fallback, etc.)
tokens_used ({ prompt, completion }) — for cost tracking
Output
{
"run_id": "uuid",
"post_ids": ["uuid1", "uuid2", "uuid3"],
"inbox_item_id": "bundle-uuid",
"final_state": "inbox",
"warnings": [],
"tokens_used": {
"prompt": 1234,
"completion": 567
}
}
Failure Handling
Voice profile deleted:
- Fail immediately with clear error message
- Pause job (set
Job.status = paused, Job.error_message = "Voice profile deleted")
- Create inbox item with warning for human: "Job X paused - voice profile missing"
Account disconnected:
- Fail immediately with clear error message
- Pause job temporarily
- Create inbox item with reconnection instructions
Template rendering fails:
- Fall back to voice-based generation without template
- Add warning to output
- Continue execution
Individual post creation fails:
- Continue with remaining posts (don't fail entire run)
- Add warning to output
- If 0 posts succeed, mark run as failed
Max retries exceeded:
- After
Job.max_retries failures, create inbox item with:
- Full error context
- Last N run error messages
- Suggested fixes (reconnect account, restore voice, etc.)
- Job remains paused until human intervention
Idempotency
- Check at start: if
run_id exists with status=completed, return cached output
- If
run_id exists with status=failed, allow retry (Celery handles this)
Advisory Lock Context
- Caller (Celery) acquires Postgres advisory lock per account before invoking this skill
- Lock ID:
hash(account_id)
- Lock mode: non-blocking (
pg_try_advisory_lock)
- If lock not acquired: Celery requeues job for +30s later
- This skill assumes lock is held — focuses on content generation, not concurrency
Agent Types That Use This Skill
- content_creator — Primary use case (scheduled content generation)
- custom — User-defined agents with automation enabled
Example Job Configuration
{
"id": "job-uuid",
"agent_id": "hype-man-uuid",
"name": "Daily Twitter Posts",
"voice_profile_id": "bolta-voice-uuid",
"account_ids": ["twitter-uuid"],
"schedule": {"cron": "0 9 * * 1-5"},
"trigger": "scheduled",
"status": "active",
"n_posts": 3,
"client_tag": "Acme Corp",
"run_instructions": "Focus on product launches and founder stories. Keep it punchy and conversational."
}
When Celery fires this job at 9am on weekdays:
- Creates Run record (status=running)
- Acquires lock on twitter-uuid account
- Calls this skill with job config + agent context
- Skill generates 3 posts → routes to inbox
- Run record updated (status=completed, tokens logged)
- Lock released
1---2name: bolta-cron-generate-to-review3description: V2 Job execution skill - Agent generates N posts per run, routes to Inbox for review. Called by Celery job runner. Replaces RecurringTemplate logic.4---56## Goal7Execute a scheduled job run: generate N posts using Brand Voice, route to Inbox for human review.89## V2 Changes10- **This is THE core job execution skill** — called by Celery job runner for scheduled jobs11- **Replaces RecurringTemplate logic** — jobs are now Agent-owned with this skill as the execution engine12- **Agent memory integration** — recalls top topics, hook styles, posting times13- **Per-account advisory locks** — caller (Celery) handles locking to prevent duplicate posts14- **Run logging** — tokens tracked, cost computed, stored in Run record15- **Always routes to Inbox** — this skill never schedules directly (use `bolta.cron.generate_and_schedule` for that)1617## When This Skill Is Called18- **Trigger:** Celery Beat detects `Job.next_run_at <= now` and `Job.status == active`19- **Caller:** `execute_job_run(job_id, run_id, account_id)` Celery task20- **Context:** Agent's full persona + memory + voice profile injected into system prompt21- **Tool access:** Filtered by agent type (content_creator gets content tools only)2223## Policy Rules241. **Always route to Inbox** — this is the "safe automation" skill252. **Respect voice soft-delete** — if voice_profile_id is soft-deleted, fail gracefully and pause job263. **Idempotency** — if run_id already exists with status=completed, skip execution274. **Max retries** — if this run fails, Celery retries up to Job.max_retries (default 2)285. **Escalation on failure** — after max retries, create inbox item with error context for human2930## Steps31321. **Pre-flight checks:**33 - `bolta.get_workspace_policy(workspace_id)` → extract `safe_mode` (informational; this skill always routes to inbox)34 - `bolta.get_my_capabilities(workspace_id)` → verify agent has content creation capability35 - `bolta.get_voice_profile(voice_profile_id)` → verify voice exists and is active36 - If soft-deleted: fail with error "Voice profile deleted, job paused" → pause job37 - Verify all `account_ids` exist and are connected:38 - `bolta.get_account_info(account_id)` for each account39 - If any account disconnected: fail with error "Account disconnected" → pause job40412. **Load agent memory for context:**42 - `bolta.recall(agent_id, "top_performing_topics")` → prioritize proven topics43 - `bolta.recall(agent_id, "preferred_hook_style")` → use successful hooks44 - `bolta.recall(agent_id, "audience_timezone")` → context for timing references45 - `bolta.recall(agent_id, "recent_failures")` → avoid repeating mistakes46473. **Load recent posts for consistency:**48 - `bolta.list_recent_posts(account_ids, limit=10)` → avoid repetition49 - Extract: topics covered, hook styles used, engagement levels (if available)50514. **Generate N posts:**52 - For each of `n_posts` (default 3):53 54 **If `template_id` provided:**55 - `bolta.render_template(template_id, {topic_seed, voice_profile_id, agent_memory})`56 - Use rendered content57 58 **Else if `topic_seeds` provided:**59 - Pick next seed from array60 - Generate post using voice profile + seed + agent memory61 62 **Else (no template, no seeds):**63 - Generate topic based on:64 - Agent memory (top performing topics)65 - Voice profile (brand focus areas)66 - Recent posts (avoid repetition, maintain variety)67 - `run_instructions` (job-specific guidance)68 69 **Apply `run_instructions` if provided:**70 - Job-level override instructions (e.g., "Focus on product features. 3 posts per run. Instagram carousel format.")71 - This overrides default agent behavior for this specific job72 73 **Create draft:**74 - `bolta.draft_post({workspace_id, voice_profile_id, account_ids: [account_id], content, status: "draft"})`75 - Tag with `run_id`, `job_id`, `client_tag` in metadata76 - Collect `post_id`77785. **Route to Inbox:**79 - All generated posts transition to `inbox` state as a batch80 - System creates single `inbox_item_id` for the bundle81 - Inbox item tagged with:82 - `agent_id` (who created it)83 - `job_id` (which job)84 - `run_id` (this execution)85 - `client_tag` (for agency filtering)86 - `final_state = inbox`87886. **Update agent memory:**89 - `bolta.remember(agent_id, "last_successful_run", ISO_timestamp)`90 - `bolta.remember(agent_id, "last_run_topics", topics_used)`91 - If any posts generated unusually well (future enhancement): store successful patterns92937. **Return execution summary:**94 - `run_id` (for audit trail)95 - `post_ids` (array of created drafts)96 - `inbox_item_id` (bundle ID for review queue)97 - `final_state` (always "inbox")98 - `warnings` (any non-fatal issues: missing topics, template fallback, etc.)99 - `tokens_used` ({ prompt, completion }) — for cost tracking100101## Output102```json103{104 "run_id": "uuid",105 "post_ids": ["uuid1", "uuid2", "uuid3"],106 "inbox_item_id": "bundle-uuid",107 "final_state": "inbox",108 "warnings": [],109 "tokens_used": {110 "prompt": 1234,111 "completion": 567112 }113}114```115116## Failure Handling117118**Voice profile deleted:**119- Fail immediately with clear error message120- Pause job (set `Job.status = paused`, `Job.error_message = "Voice profile deleted"`)121- Create inbox item with warning for human: "Job X paused - voice profile missing"122123**Account disconnected:**124- Fail immediately with clear error message125- Pause job temporarily126- Create inbox item with reconnection instructions127128**Template rendering fails:**129- Fall back to voice-based generation without template130- Add warning to output131- Continue execution132133**Individual post creation fails:**134- Continue with remaining posts (don't fail entire run)135- Add warning to output136- If 0 posts succeed, mark run as failed137138**Max retries exceeded:**139- After `Job.max_retries` failures, create inbox item with:140 - Full error context141 - Last N run error messages142 - Suggested fixes (reconnect account, restore voice, etc.)143 - Job remains paused until human intervention144145## Idempotency146- Check at start: if `run_id` exists with `status=completed`, return cached output147- If `run_id` exists with `status=failed`, allow retry (Celery handles this)148149## Advisory Lock Context150- **Caller (Celery) acquires Postgres advisory lock** per account before invoking this skill151- Lock ID: `hash(account_id)`152- Lock mode: non-blocking (`pg_try_advisory_lock`)153- If lock not acquired: Celery requeues job for +30s later154- This skill assumes lock is held — focuses on content generation, not concurrency155156## Agent Types That Use This Skill157- **content_creator** — Primary use case (scheduled content generation)158- **custom** — User-defined agents with automation enabled159160## Example Job Configuration161```json162{163 "id": "job-uuid",164 "agent_id": "hype-man-uuid",165 "name": "Daily Twitter Posts",166 "voice_profile_id": "bolta-voice-uuid",167 "account_ids": ["twitter-uuid"],168 "schedule": {"cron": "0 9 * * 1-5"},169 "trigger": "scheduled",170 "status": "active",171 "n_posts": 3,172 "client_tag": "Acme Corp",173 "run_instructions": "Focus on product launches and founder stories. Keep it punchy and conversational."174}175```176177When Celery fires this job at 9am on weekdays:1781. Creates Run record (status=running)1792. Acquires lock on twitter-uuid account1803. Calls this skill with job config + agent context1814. Skill generates 3 posts → routes to inbox1825. Run record updated (status=completed, tokens logged)1836. Lock released