---
name: plungeai-campaign-agent
description: "Design and emit ONE PlungeAI (Ocean Studio) campaign agent — a long-running, list-driven, resumable agent that works an owned ledger of items in short scheduled runs until the current cycle is exhausted — as CNL YAML (a claim task plus a `type: batch` with `ledger: campaign`) and a campaign-config block (list source, cycle, schedule, max_attempts, then, deliver). Use when the Studio Think composer has \"Campaign agent\" selected, or when a user asks for an agent that works through a list, a backlog, a catalog, \"N things to check\", \"run until the list is done\", \"start again every week\", or \"then move to the next list\". NOT for single scheduled reports (plungeai-bot-agent), interactive agents (plungeai-agentic-agent) or multi-step pipelines (plungeai-workflows)."
---

# Campaign agent — a list worked to completion over many runs

A campaign is a task list with an owner. Each run claims a slice of the not-yet-done items, works them at
bounded concurrency, and records every outcome before it ends; the scheduler re-fires runs until the cycle is
exhausted. Runs are short by law — the platform cannot hold one invocation past ~10 minutes — so never design
a run that "keeps going". Repetition is the schedule's job, resumption is the ledger's job.

## The conversation (before any YAML)

1. **Unit.** What is one item? A product, a lead, a URL, a document. Name the key that identifies it.
2. **List source.** Exactly one: a Task-app **table + filter** (point-and-pick; never raw SQL), an **agent op**
   that returns a JSON array (discover the id with `registry_search`, never from memory), a **CSV** the user
   will upload in the Task app after save, or an **inline** list for a handful of items.
3. **Per-item job.** A plain agent op when the item is "call one agent, store one row" (cheapest). A harness
   mission only when the item genuinely needs a tool loop; then the same rules as a bot: no `ask_user`, no
   `platform_action`, `effort: quick` unless argued, mission ends with the task_complete sentence.
   The job MUST upsert by `{item.key}` + `{item.cycle}` — say so in the mission or pass both to the op.
4. **Cycle.** `once` (a backlog), `hourly | daily | weekly | monthly` (a recurring pass), `continuous`
   (restart as soon as exhausted). `cycle_start: reset` reuses the same keys; `refill` re-queries the source.
5. **Schedule.** When runs may happen — a 5-field cron. Propose a window that finishes the list with margin:
   `batch_size × seconds per item ÷ concurrency ≤ 600 s`, then enough ticks to cover the list.
6. **Deliver.** Where the cycle summary and alerts go — same channels and pairing rules as a bot.

Confirm, then emit BOTH blocks.

## Output contract — two fenced blocks, in this order

```yaml
workflow:
  name: "<short campaign name>"
  description: "<one line: what one item is and how often the list is worked>"
  tasks:
    - type: task
      id: claim
      agent: data-table-agent
      operation: campaignClaim
      campaign_id: "<written by Step-2 at save>"
      batch_size: 40
    - type: batch
      id: work
      items_from: claim
      concurrency: 5
      ledger: campaign
      tasks:
        - type: task            # or type: harness when a tool loop is needed
          id: one
          agent: <agent id>
          operation: <op>
          key: "{item.key}"
          cycle: "{item.cycle}"
```

```campaign-config
list:
  table: { project: <project>, table: <table>, filter: { <column>: <value> }, key: <column> }
cycle: weekly
cycle_start: refill
schedule: "*/5 9 * * 1"
max_attempts: 2
then: ""
deliver:
  - channel: inapp
local: false
```

Rules the lint enforces — violating them makes the save fail:
- Exactly one top-level claim task (`agent: data-table-agent`, `operation: campaignClaim`) and exactly one
  `type: batch` with `items_from: claim`, `ledger: campaign`, and `concurrency` ≤ 5.
- Inner tasks are unattended: no `ask_user`, no `platform_action`; a harness inner task MUST include
  `task_complete` in `allowed_tools` and end its mission with the task_complete sentence.
- The per-item task must reference `{item.key}` (warning if it looks insert-only — the ledger is at-least-once).
- `max_attempts` is required. No `memory_owner`. No emails or ids in the YAML — recipients belong in `deliver`.
- `local: true` only with a harness inner task pinned to `agent: harness-agent`, and in BOTH blocks.

## campaign-config fields

- `list` — one of `table {project, table, filter, key}`, `agent {id, operation}`, `csv: true`, `inline: [...]`.
- `cycle` — `once | hourly | daily | weekly | monthly | continuous`. Calendar cycles never restart inside their bucket.
- `cycle_start` — `reset | refill`. Default `refill` for table/agent sources, `reset` for csv/inline.
- `schedule` — 5-field cron; each tick is one run (an empty claim returns in about a second).
- `max_attempts` — dead-letter threshold on explicit failures; required.
- `then` — optional campaign id to start when a cycle completes. Never chain a dependent report onto a
  collection campaign; keep reports time-triggered.
- `deliver` — as for bots (`inapp`, `email`, `slack`, `telegram`, `whatsapp`, `discord`).
- `local` — run-on-my-computer, as for bots.

Worked examples for all four use cases plus the full field reference: `references/campaign-config.md`.

Outside Studio the same campaign ships over MCP (`https://mcp.plungeai.com/v1`): `plungeai_schedule`
with `action: create`, `job_type: workflow` creates the scheduler job against the saved campaign
workflow. Discover the agent ids you name in the per-item task live — `plungeai_list_agents` from a
client, or `registry_search` inside the loop — never from memory.
