plungeai-campaign-agent
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:
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
- 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/totarget needs the owner to be a connected workspace member.
Worked shapes
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.
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}"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: false2 · 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.
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]list:
csv: true
cycle: once
cycle_start: reset
schedule: "*/5 * * * *"
max_attempts: 2
deliver: [{ channel: inapp }, { channel: email }]
local: false3 · 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.
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]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: false4 · 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).
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