plungeai-campaigns
Campaigns — running a list to completion (the campaign ledger)
A campaign claims a list of items (a data-table query, an agent's lister output, an uploaded CSV, or an inline list), runs each item through a CNL pipeline with per-item retries, and repeats on a cadence until the list ("cycle") is drained — then either re-cycles (refill/reset) or hands off to…
A campaign claims a list of items (a data-table query, an agent's lister
output, an uploaded CSV, or an inline list), runs each item through a CNL pipeline
with per-item retries, and repeats on a cadence until the list ("cycle") is
drained — then either re-cycles (refill/reset) or hands off to another
campaign (then). It is PlungeAI's answer to "process these 280 rows every
Monday" or "watch these 50 URLs forever."
Plain fact about the surface: there is no dedicated public One API route or MCP
tool for managing a campaign today (https://api.plungeai.com and
https://mcp.plungeai.com/v1 expose neither) — a campaign is authored visually in
Ocean Studio via the plungeai-campaign-agent kind (CNL YAML + a
campaign-config fenced block), and its runtime primitives are plain CNL
agent: data-table-agent operations you can also call directly from a workflow you
build yourself. This skill documents what exists; for the authoring UI itself use
plungeai-campaign-agent.
Discover the pieces first
A campaign's pipeline is ordinary CNL, so discover its agents the normal way —
never guess an id: plungeai_list_agents {search: "<what the item-processing step needs>"}
(REST: GET /v1/discovery/search?kind=agents&q=…). data-table-agent itself is a
registry agent id — confirm it live the same way.
The campaign-config block
A fenced ```campaign-config block sits after the CNL YAML; Studio parses
it to open the ledger and create the scheduler job.
| Field | Required | Meaning |
|---|---|---|
list | yes | Where items come from: 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 (plungeai-scheduling). |
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 (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. |
learn | no | Enables the ledger's recall/knowledge lane. |
The CNL shape — claim, batch, complete
workflow:
name: "RH price check"
tasks:
- type: task
id: claim
agent: data-table-agent
operation: campaignClaim
campaign_id: "<written by Studio 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}"type: batch with ledger: campaign is what wires the batch's per-item outcome
back into the campaign ledger automatically (complete/fail bookkeeping) — see
plungeai-workflows for the general batch task type. An item's work can be a
plain task (as above) or an open-ended type: harness mission
(plungeai-missions) when the per-item work needs judgment, not a fixed call.
The ledger primitives (agent: data-table-agent operations)
Every operation is owner-scoped (owner_uuid — resolved from your account, not a
field you pass by hand):
| Operation | Purpose |
|---|---|
campaignBegin | Create/upsert the campaign header at save time (list source, cycle kind, batch/concurrency/max_attempts, optional seed items). |
campaignClaim | Lease up to batch_size pending items for this run (run_id); opens a new cycle if the prior one drained and cycle_start allows it. Refuses to lease from a non-active campaign. |
campaignComplete | Mark one leased item done with its result — only a currently-leased row transitions (a stale duplicate is a safe no-op). |
campaignCompleteHuman | Complete a worker_kind: "human" row directly — human rows are never leased, so this is the only way one closes out. |
campaignFail | Record a failed attempt; the item returns to pending until max_attempts, then flips to failed. |
campaignRelease | Un-lease claimed-but-unfinished rows (e.g. a crashed run) back to pending. |
campaignStatus | Read the header + per-status item counts. |
campaignAddItems | Add items to the current (or a named) cycle; refuses past max_items. |
campaignRetryFailed | Re-queue failed items (optionally a specific key list) back to pending. |
campaignSetStatus | Pause/resume/mark done; pausing stamps paused_by (default user:<owner>; the scheduler's own zero-progress breaker stamps auto:zero-progress). |
campaignDelete | Soft-delete (deleted_at + status='done'); releases held leases; history stays queryable. |
campaignTasks / campaignUpsertTasks / campaignSetTaskActive | Read/edit the campaign's editable task-row list (the Task-app grid). |
campaignResults / campaignPivot | Recent per-item results (optionally rolled up per cycle) / a tasks-×-runs reporting grid. |
chainStep | The zero-AI "forever campaign" unit of work: bump a lane counter once per cycle (idempotent under at-least-once re-claims) and log a per-run row. |
counterStep | A pure per-cycle counter (prev + step), for the simplest possible repeating tally. |
Full parameter shapes live in core/core-data-table-agent/campaign-ledger.ts,
chain-step.ts, counter-step.ts — read those before hand-building a pipeline
around one of these operations; nothing here is invented beyond what that code
implements.
Cadence — two ways a campaign keeps going
- Cron
schedule— an ordinary recurring scheduler job re-fires the pipeline until the cycle drains (the Forever Campaign ran on*/5 * * * *). This is the normal path whencampaign-config.scheduleis set. runAgainself-scheduling — a campaign saved without a cron schedule ends its pipeline withagent: scheduler, operation: runAgain, in: "1h"(or another duration). This creates exactly ONE@oncejob for the workflow and never stacks — an existing pending@oncejob for the same workflow is re-timed, not duplicated. Seeplungeai-schedulingfor@once/runAgainmechanics and cron/date-token reference in full.
Sizing
Keep batch_size × seconds-per-item ÷ concurrency ≤ 600s — comfortably inside the
scheduler's execution window. The defaults (batch_size: 40, concurrency: 5 at
~30s/item ≈ 4 minutes per run) are safe up to roughly 100 items per scheduled run;
batch_size/concurrency are hidden authoring knobs, not something you expose to
an end operator.
Delivery on cycle-complete (deliver)
- 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 output well under the smallest cap you use.
- telegram / whatsapp targets must be paired to the owner first (a pairing code
from Studio,
/pair <code>from that chat); discord requires any active pairing. Unpaired targets are skipped. - email sends from the platform address; a bare target means the owner's address — any other address needs that holder's prior confirmation, else it's skipped silently.
- slack: a bare target DMs the owner; a specific
chat_id/toneeds the owner to be a connected workspace member.
Gotchas
upsertRowis insert-only, not keyed — it does not matchmatchFieldsor update in place, and a JSON-stringdatais not parsed. For an idempotent per-item write usechainStepor explicit top-levelcol_<name>columns (insertRow/upsertRowsubstitute{item.*}only into top-level string fields); to update an existing row useupdateRows.- A partial cycle should never trigger a dependent report via
then— chain only full completions to full completions (a data-collection campaign feeding a time-triggered analysis job is the safer pattern than chaining the analysis itself). - The zero-progress breaker auto-pauses a campaign after several consecutive
completed-runs-that-completed-nothing (an all-failing list) — check
campaignStatus/the Task app before assuming a stalled campaign needs a manual nudge. - Every ledger operation is scoped by the caller's own
owner_uuid— there is no cross-tenant campaign management surface.