Skip to Content
AgentsActivity PlannerActivity Planner — Architecture

Activity Planner — Architecture

The Activity Planner is the orchestration brain of the platform. It sits between the strategic layer (what should be produced this month) and the execution layer (the 25 worker agents that actually produce content). It generates no content itself — its only job is to read the approved deliverable plan and produce a sequenced, dependency-linked pipeline of Activity records and BullMQ jobs.


System Context — Where It Fits

┌──────────────────────────────────────────────────────────────────────────┐ │ TENANT ADMIN │ │ │ │ "Here is our strategy for Q2" → approves deliverable plan │ │ "Start planning April" → triggers Activity Planner │ │ "Looks good, go" → approves pipeline preview │ └────────────────────────────┬─────────────────────────────────────────────┘ ┌──────────────▼──────────────┐ │ DELIVERABLE PLANNER │ Produces the approved │ (what to produce + volumes) │ DeliverablePlan record └──────────────┬──────────────┘ │ deliverablePlanId ┌──────────────────────────────┐ │ ACTIVITY PLANNER │ ← This component │ agent__activity-planner │ │ Claude Sonnet 4.6 │ └──────────────┬───────────────┘ │ creates Activities + enqueues jobs ┌───────────────────────────────────────┐ │ 25 WORKER AGENTS │ │ keyword-researcher, blog-writer, │ │ social-post-writer, site-auditor, … │ └───────────────────────────────────────┘

Component Diagram

╔══════════════════════════════════════════╗ TRIGGERS ║ ACTIVITY PLANNER ║ OUTPUTS ║ ║ BullMQ cron ─────────►║ 1. Receive job ║──────► PostgreSQL (1st of month ║ (tenantId, deliverablePlanId, ║ activities table 00:30 UTC) ║ periodStart, periodEnd) ║ (1 row per task) ║ ║ Dashboard API ────────► 2. Load context ║──────► PostgreSQL (admin clicks ║ ├── tenant settings (PG) ║ periods table "Start Planning" ║ ├── skills (MongoDB) ║ (pipeline summary) or plan approval) ║ └── RAG pre-fetch (Qdrant) ║ ║ ├── Published Content ║──────► BullMQ jobs Self-dispatch ────────► └── Client Documents ║ (one per activity, (prior period ║ ║ routed to correct finished early) ║ 3. Build prompt ║ agent queue) ║ buildActivityPrompt() ║ ║ ║──────► HITL preview ║ 4. Spawn Claude subprocess ║ (Dashboard → ║ claude -p "..." --add-dir ... ║ Campaigns screen) ║ --output-format stream-json ║ ║ ║ ║ 5. Validate + persist output ║ ║ ActivityPlannerOutput JSON ║ ╚══════════════════════════════════════════╝ DATA SOURCES ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │ PostgreSQL │ │ MongoDB │ │ Qdrant │ │ │ │ │ │ │ │ deliverable_ │ │ skills: │ │ ds_pc_{tenant} │ │ plans │ │ client-context- │ │ Published Content │ │ tenant_settings │ │ file.md │ │ (avoid repetition) │ │ agent_configs │ │ activity- │ │ │ │ periods │ │ planning-sop.md │ │ ds_cd_{tenant} │ │ │ │ deliverable- │ │ Client Documents │ │ │ │ types.md │ │ (upcoming events) │ └─────────────────┘ └──────────────────┘ └──────────────────────┘

Activity Volume Cap

The prompt enforces a hard cap on the number of activities Claude may generate per period. The resolved value follows a three-level priority chain:

  1. Per-tenant overrideTenantDeliverableConfig.activityPlannerMaxActivities (nullable Int; set in Manage → Tenants → [Tenant] → Deliverable Config tab)
  2. Global platform settingPlatformSetting key activityPlannerMaxActivities (set in Manage → System → Deliverable Settings; default 60)
  3. Code fallbackACTIVITY_PLANNER_DEFAULT_MAX = 60 in activity.worker.ts

Why this limit exists: Claude producing 50+ activities in a single response caused the output to be truncated mid-JSON, resulting in parse failures and silent retries that exhausted all BullMQ attempts (observed April 2026).

Action items are protected: The prompt rule explicitly states that one-time action items from the deliverable plan must never be deferred or dropped regardless of the cap. Regular deliverables are trimmed first if the budget is exceeded.

Historical note: Cap was originally 30 (env var ACTIVITY_PLANNER_MAX_ACTIVITIES), raised to 300 in May 2026 when action items were found to be silently dropped on plans with high deliverable volume.


Data Flow — Full Sequence

┌─────────────────────────────────────────────────────────────────────────┐ │ Step 1 — TRIGGER │ │ │ │ BullMQ dequeues job from agent__activity-planner │ │ Payload: { tenantId, deliverablePlanId, periodStart, periodEnd, │ │ deliverables[], previousPeriodSummary? } │ └─────────────────────────────────────────┬───────────────────────────────┘ ┌─────────────────────────────────────────▼───────────────────────────────┐ │ Step 2 — CONTEXT LOAD (parallel reads) │ │ │ │ PostgreSQL ──► tenant_settings (brand voice, industry, plan) │ │ PostgreSQL ──► agent_configs (model, skills list, RAG datasets) │ │ MongoDB ──► client-context-file.md │ │ MongoDB ──► activity-planning-sop.md │ │ MongoDB ──► deliverable-types.md │ │ Qdrant ──► Published Content: "content published last month" │ │ Qdrant ──► Client Documents: "upcoming events promotions" │ └─────────────────────────────────────────┬───────────────────────────────┘ ┌─────────────────────────────────────────▼───────────────────────────────┐ │ Step 3 — PROMPT ASSEMBLY │ │ │ │ buildActivityPrompt() substitutes placeholders: │ │ {{TENANT_SETTINGS}} ← from PostgreSQL │ │ {{CLIENT_CONTEXT}} ← from MongoDB skill │ │ {{DELIVERABLES}} ← from job payload │ │ {{PUBLISHED_CONTENT_RAG}} ← from Qdrant │ │ {{CLIENT_DOCUMENTS_RAG}} ← from Qdrant │ │ {{PREVIOUS_PERIOD_SUMMARY}} ← from job payload │ │ │ │ Skills written to /tmp/skills-{uuid}/ │ │ ├── client-context-file.md │ │ ├── activity-planning-sop.md │ │ └── deliverable-types.md │ └─────────────────────────────────────────┬───────────────────────────────┘ ┌─────────────────────────────────────────▼───────────────────────────────┐ │ Step 4 — CLAUDE SUBPROCESS │ │ │ │ spawn('claude', [ │ │ '--output-format', 'stream-json', │ │ '--model', 'claude-sonnet-4-6', │ │ '--add-dir', '/tmp/skills-{uuid}/', │ │ '--max-turns', '15', │ │ '-p', systemPrompt + taskPrompt │ │ ]) │ │ │ │ Tool calls during this run: │ │ rag_search × 2 (Published Content + Client Documents) │ │ ← no web_search or web_fetch; Activity Planner is DB-only │ │ │ │ Output: ActivityPlannerOutput JSON │ │ { activities: ActivitySpec[], jobsEnqueued: [], pipelineSummary } │ └─────────────────────────────────────────┬───────────────────────────────┘ ┌─────────────────────────────────────────▼───────────────────────────────┐ │ Step 5 — PERSIST │ │ │ │ PostgreSQL INSERT → activities (one row per ActivitySpec) │ │ PostgreSQL INSERT → periods (pipeline summary Markdown) │ │ PostgreSQL INSERT → activity_runs + llm_calls │ └─────────────────────────────────────────┬───────────────────────────────┘ ┌─────────────────────────────────────────▼───────────────────────────────┐ │ Step 6 — DM APPROVAL GATE (optional; per-tenant or global setting) │ │ │ │ Resolved as: TenantDeliverableConfig.requireActivityApproval │ │ ?? PlatformSetting("requireActivityApproval") │ │ ?? true (default ON) │ │ │ │ If TRUE: │ │ Period status → "dm_review" │ │ DeliverablePeriodLog: event "dm_review_started" │ │ DM reviewers notified by email │ │ STOP — no jobs dispatched yet │ │ │ │ DM reviewer → Pipeline screen (DM portal): │ │ edit / delete / add activities → approve or reject + reason │ │ Reject → activities deleted; period "rejected"; planner re-queued │ │ with rejectionFeedback injected into Claude prompt │ │ Approve → dependency-free activities dispatched; period "active" │ │ │ │ If FALSE (default): │ │ BullMQ ADD → one job per dependency-free activity immediately │ └─────────────────────────────────────────┬───────────────────────────────┘ ┌─────────────────────────────────────────▼───────────────────────────────┐ │ Step 7 — HITL GATE (client-facing pipeline preview) │ │ │ │ Dashboard → Campaigns screen shows: │ │ ├── Week-by-week breakdown of all planned tasks │ │ ├── Specific topic assigned to each content piece │ │ └── Total credit estimate for the period │ │ │ │ Admin options: │ │ ├── Edit any task topic before it runs │ │ ├── Skip a task ("not this month") │ │ ├── Add an ad-hoc task │ │ └── Approve → worker jobs start processing │ │ │ │ Auto-approve: if admin does not review within 48 h on scheduled runs │ └─────────────────────────────────────────────────────────────────────────┘

Downstream: Worker Agent Queues

Once the admin approves the pipeline, jobs flow to the worker agents. The Activity Planner knows and enforces the dependency chain.

Activity Planner ├── SEO / Blog track │ ├── agent__keyword-researcher │ │ └── dependsOn: none (runs immediately) │ ├── agent__content-brief-writer │ │ └── dependsOn: keyword-researcher │ ├── agent__research-note-writer │ │ └── dependsOn: content-brief-writer + HITL approval │ └── agent__blog-writer │ └── dependsOn: research-note-writer + HITL approval ├── Social track │ ├── agent__social-calendar-planner │ │ └── dependsOn: none (runs immediately) │ └── agent__social-post-writer (batch) │ └── dependsOn: social-calendar-planner + HITL approval ├── Ads track │ ├── agent__google-ads-writer │ │ └── dependsOn: keyword-researcher │ └── agent__meta-ads-writer │ └── dependsOn: none (brief provided in payload) ├── Local / Email track │ ├── agent__gbp-post-writer │ │ └── dependsOn: none (standalone) │ └── agent__email-writer │ └── dependsOn: none (standalone) ├── Research track │ ├── agent__client-researcher │ ├── agent__competitor-researcher │ ├── agent__backlink-researcher │ └── agent__backlink-outreach-writer │ └── dependsOn: backlink-researcher ├── Site / Strategy track │ ├── agent__site-auditor │ ├── agent__landing-page-writer │ ├── agent__strategy-writer │ └── agent__context-file-writer └── Reporting (always last) └── agent__report-writer └── dependsOn: ALL other activities in period

Multi-Tenancy

Queues are shared across all tenants — one queue per agent role. The Activity Planner worker handles jobs for all tenants from a single agent__activity-planner queue. Tenant isolation is enforced via tenantId in the job payload (the worker verifies it before execution).

agent__activity-planner → agent__blog-writer agent__keyword-researcher agent__content-brief-writer ...

The Activity Planner worker processes jobs for all tenants concurrently (concurrency: 2 — it is CPU-intensive due to Claude reasoning load). Per-tenant throughput limits can be applied via BullMQ rate limiters keyed on tenantId.


Upstream: Deliverable Planner

The Activity Planner does not decide what to produce. That decision lives one level up.

┌─────────────────────────────────────────────────────┐ │ DELIVERABLE PLANNER │ │ │ │ Reads: strategy doc, client goals, plan tier │ │ Produces: DeliverablePlan │ │ e.g. "4 blog posts, 8 GBP posts, 1 monthly │ │ report, 20 social posts — April 2026" │ │ │ │ Human approves this plan in Dashboard │ └──────────────────────────┬──────────────────────────┘ │ approval triggers ┌─────────────────────────────────────────────────────┐ │ ACTIVITY PLANNER │ │ │ │ Reads: DeliverablePlan (what + volumes) │ │ Decides: which topics, which sequence, which weeks │ │ Produces: Activity records + BullMQ job pipeline │ └──────────────────────────────────────────────────────┘

Error Handling — Mid-Period Block

If a critical-path task fails (e.g. keyword research fails → all blog posts blocked):

keyword-researcher → FAILED (3 retries exhausted) content-brief-writer → BLOCKED (dependsOn not satisfied) blog-writer → BLOCKED Notification sent to admin Blocked tasks surfaced in Dashboard → Campaigns ├── Admin retries the failed task ├── Admin provides input manually (bypasses failed step) └── Admin cancels blocked tasks for this period

Key Numbers

AttributeValue
Queueagent__activity-planner (shared, all tenants)
ModelClaude Sonnet 4.6
Concurrency1
Timeout15 min (900 s; lockDuration 960 s)
Avg input tokens~14,000
Avg output tokens~2,200
Est. cost / run~$0.90
Tool callsrag_search × 2 only — no web tools
Downstream queues touchedUp to 25
PlanFree+ (all tenants)

© 2026 Leadmetrics — Internal use only