> ## Documentation Index
> Fetch the complete documentation index at: https://docs.plungeai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# campaign-config reference

> A campaign-config fenced block sits after the CNL YAML. Studio parses it to open the ledger and create the scheduler job. Fields:


<!-- sources-of-truth: agents/agents/campaign-agent/campaign-agent-design-5.0.md §8, agents/agents/campaign-agent/EXAMPLES.md, apps/ocean-skills/skills/plungeai-bot-agent/references/bot-config.md (deliver section) | last-synced: 2026-09-08 -->
A `campaign-config` fenced block sits after the CNL YAML. Studio parses it to open the
ledger and create the scheduler job. Fields:

| Field | Required | Meaning |
|---|---|---|
| `list` | yes | Where items come from. One of `table:` (a data-table project+table), `agent:` (a lister task whose JSON array feeds `items_from`), `csv: true` (uploaded rows), or `inline: [key, …]`. |
| `cycle` | yes | Calendar bucket: `once` \| `hourly` \| `daily` \| `weekly` \| `monthly` \| `continuous`. |
| `cycle_start` | when re-listing | `refill` (add new list rows, keep prior) or `reset` (fresh cycle from the list). |
| `schedule` | yes | Cron for the run cadence, e.g. `"*/5 9 * * 1"`. The scheduler re-fires until the cycle drains. |
| `max_attempts` | yes | Per-item retry cap before an item is marked `failed` (integer ≥ 1; the ledger column defaults to 2). |
| `then` | no | Campaign id to hand the baton to when this one's cycle completes. |
| `deliver` | no | Channels for the cycle-complete notice (see below). |
| `local` | no | `true` runs items on the user's own computer. |
| `max_items` | no | Hard cap on total items claimed across the cycle. |
| `task_types` | no | Per-row task/worker overrides (§8a). |
| `learn` | no | Enables the ledger's recall/knowledge lane (§8a–8c). |

## Sizing (rule of thumb)

Keep `batch_size × seconds per item ÷ concurrency ≤ 600 s` — well inside the 15-minute DO
wall. The default `batch_size: 40`, `concurrency: 5` at ~30 s/item = 4 min per run; up to
~100 items per run is safe on the scheduled path. `batch_size` and `concurrency` are hidden
authoring knobs — never operator vocabulary.

## deliver channels

<!-- copied verbatim from plungeai-bot-agent/references/bot-config.md deliver section -->
- Fire-and-forget: a delivery failure never fails the run.
- Length caps per channel: telegram 3800, whatsapp 3800, discord 1900, slack 3800, email
  100000, inapp 2000 characters — keep the bot's output well under the smallest cap you use.
- telegram / whatsapp targets must be paired to the owner: generate a pairing code in Studio
  and send `/pair <code>` from that chat (Telegram additionally requires the user to have
  opened the bot once); discord requires any active pairing. Unpaired targets are skipped.
- email sends from the platform address; a bare email target means the owner's address. Any
  other address delivers only after that address holder confirmed it (a confirmed delivery
  recipient) — otherwise the target is skipped silently.
- slack: a bare target DMs the owner; a `chat_id` / `to` target needs the owner to be a
  connected workspace member.

## Worked shapes

<!-- copied verbatim from agents/agents/campaign-agent/EXAMPLES.md -->

### 1 · Weekly catalog price check (the Arhaus case)

280 active products, ~30 s each, concurrency 5, 40 per run → seven runs of ~4 min on Monday morning, exhausted
by ~09:34; analysis (10:00) and Excel (10:15) stay separate time-triggered jobs.

```yaml
workflow:
  name: "RH price check"
  description: "Weekly price check of the tracked catalog"
  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
          id: one
          agent: price-collector-v2-agent
          operation: collect_single
          product_name: "{item.key}"
          week_date: "{item.cycle}"
```

```campaign-config
list:
  table: { project: furniture-pricing, table: products, filter: { status: active }, key: product_id }
cycle: weekly
cycle_start: refill
schedule: "*/5 9 * * 1"
max_attempts: 2
deliver: [{ channel: inapp }]
local: false
```

---

### 2 · 200 leads, enriched once

A CSV uploaded in the Task app; a small harness per item; runs every five minutes until exhausted (~5 runs),
then `done`: job paused, summary delivered.

```yaml
workflow:
  name: "Lead enrichment — September list"
  description: "Enrich each lead once and store the result"
  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: harness
          id: one
          goal: "Enrich the lead {item.key} ({item.company}, {item.email}). Find the company's website, size and industry; store the result with call_agent to data-table-agent, upserting by key {item.key} in cycle {item.cycle}."
          mission: |
            You are a lead researcher. Use only public sources and cite the URL you relied on.
            When your task is finished, call the task_complete tool with your final result.
          effort: quick
          allowed_tools: [web_search, web_fetch, call_agent, task_complete]
          allowed_agents: [data-table-agent]
```

```campaign-config
list:
  csv: true
cycle: once
cycle_start: reset
schedule: "*/5 * * * *"
max_attempts: 2
deliver: [{ channel: inapp }, { channel: email }]
local: false
```

---

### 3 · Continuous monitor of 50 URLs

An inline list; each exhaustion opens the next cycle (`#n+1`). The no-progress breaker (three consecutive runs
completing nothing) pauses an all-failing list instead of looping on it.

```yaml
workflow:
  name: "Status-page watch"
  description: "Check 50 status pages continuously and record changes"
  tasks:
    - type: task
      id: claim
      agent: data-table-agent
      operation: campaignClaim
      campaign_id: "<written by Step-2 at save>"
      batch_size: 50
    - type: batch
      id: work
      items_from: claim
      concurrency: 5
      ledger: campaign
      tasks:
        - type: task
          id: one
          agent: firecrawl-agent
          operation: scrape
          url: "{item.key}"
          formats: [markdown]
```

```campaign-config
list:
  inline:
    - "https://status.example-a.com"
    - "https://status.example-b.com"
cycle: continuous
cycle_start: reset
schedule: "0 * * * *"
max_attempts: 3
deliver: [{ channel: slack }]
local: false
```

---

### 4 · Chain: finish list A, then start campaign B

Campaign A runs once; when its cycle completes, the scheduler sets campaign B's job to run now. Never use this
to chain a dependent report onto a data-collection campaign — a partial cycle must not trigger analysis on
incomplete data (the Arhaus analysis/Excel jobs stay time-triggered for exactly this reason).

```campaign-config
list:
  agent: { id: registry-sync-agent, operation: list_stale_cards }
cycle: once
cycle_start: refill
schedule: "*/5 * * * *"
max_attempts: 2
then: "<campaign B id>"
deliver: [{ channel: inapp }]
local: false
```
