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 another
campaign (then). It is PlungeAI's answer to "process these 280 rows every
Monday" or "watch these 50 URLs forever."
The surface today. A campaign is a saved workflow of kind: campaign. Create it over the One API
(POST /v1/workflows with kind: "campaign" and a campaign_config object), over MCP
(plungeai_workflow {action: "create", kind: "campaign", campaign_config}) or visually in Ocean Studio via
the plungeai-campaign-agent kind (CNL YAML + a campaign-config fenced block): all three write the
same row and arm the same ledger (campaignBegin) and cron job. Listing, reading, changing and deleting are
the ordinary workflow routes (GET /v1/workflows?kind=campaign, PATCH, DELETE). Ledger status, pause,
resume, stop and completing a human row are Studio-session routes (ocean-cli campaigns status | pause | resume | stop | complete | setup); over an ozk_ key you pause the campaign's schedule instead
(POST /v1/schedules/{id}/pause). The ledger operations in this page are the engine's own: you write them
into the campaign YAML, you do not call them directly.
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 is the exception: an
internal engine agent with no registry card (a lookup answers "no such card"); it appears only in the
claim task of the campaign YAML and in the ledger operations below.
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. Over REST and MCP the same fields are the
campaign_config JSON object of POST /v1/workflows / plungeai_workflow (a list object instead of
the block's list: line; task_types and learn are Studio-only and dropped there).
| 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 | no | refill (add new list rows, keep prior) or reset (fresh cycle from the list). Default over REST/MCP: reset for csv / inline, else refill. |
schedule | no | 5-field cron for the run cadence, e.g. "*/5 9 * * 1" — the scheduler re-fires until the cycle drains (plungeai-scheduling). Without it no scheduler job is created and the pipeline schedules its own next run (runAgain, below). |
max_attempts | yes | Per-item retry cap before an item is marked failed (integer ≥ 1; the ledger column defaults to 2). |
then | no | Campaign or workflow id (A-Za-z0-9_-, ≤ 64) to hand the baton to when this one's cycle completes. |
deliver | no | [{channel, to?}] for the cycle-complete notice (below); channel is inapp (default), email, slack, telegram, whatsapp, discord or teams. |
local | no | true runs items on the user's own computer. |
max_items | no | Hard cap on total items claimed across the cycle (required when an inner task can reach data-table-agent). |
batch_size, pace | no | Positive integer claim slice per run (default 40); seconds between runs (pace is inert today). |
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.
Saving one over the API — validation
POST /v1/workflows lints a kind: campaign YAML before anything is written (422 invalid_workflow, errors
listed): exactly one claim task (agent: data-table-agent, operation: campaignClaim) and exactly one type: batch
task with items_from (the claim task id) and ledger: campaign; concurrency ≤ 5 (open-connection cap); no
ask_user / platform_action inside the batch (unattended); an inner type: harness task must list
task_complete in allowed_tools; max_items is required when an inner task can reach data-table-agent. A
missing {item.key} in the per-item task is a warning (the ledger is at-least-once: write idempotently). Setup then
stamps the campaign id into the claim and batch tasks, opens the ledger and, when schedule is set, creates the
cron job named <name> - Campaign. Failures after the row is saved come back as warnings on the 201.
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.