For AI agents: a documentation index is available at https://www.plungeai.com/docs/llms.txt. Append .md to any page URL, or send Accept: text/markdown, to get markdown. Setup instructions for agents are at https://www.plungeai.com/docs/agents.md. One ozk_ key opens every plane, models included.

Documentation Index: fetch the complete documentation index at /llms.txt. Use this file to discover all available pages before exploring further.

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).

FieldRequiredMeaning
listyesWhere 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, …].
cycleyesCalendar bucket: once | hourly | daily | weekly | monthly | continuous.
cycle_startnorefill (add new list rows, keep prior) or reset (fresh cycle from the list). Default over REST/MCP: reset for csv / inline, else refill.
scheduleno5-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_attemptsyesPer-item retry cap before an item is marked failed (integer ≥ 1; the ledger column defaults to 2).
thennoCampaign or workflow id (A-Za-z0-9_-, ≤ 64) to hand the baton to when this one's cycle completes.
deliverno[{channel, to?}] for the cycle-complete notice (below); channel is inapp (default), email, slack, telegram, whatsapp, discord or teams.
localnotrue runs items on the user's own computer.
max_itemsnoHard cap on total items claimed across the cycle (required when an inner task can reach data-table-agent).
batch_size, pacenoPositive integer claim slice per run (default 40); seconds between runs (pace is inert today).
task_typesnoPer-row task/worker overrides.
learnnoEnables 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):

OperationPurpose
campaignBeginCreate/upsert the campaign header at save time (list source, cycle kind, batch/concurrency/max_attempts, optional seed items).
campaignClaimLease 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.
campaignCompleteMark one leased item done with its result — only a currently-leased row transitions (a stale duplicate is a safe no-op).
campaignCompleteHumanComplete a worker_kind: "human" row directly — human rows are never leased, so this is the only way one closes out.
campaignFailRecord a failed attempt; the item returns to pending until max_attempts, then flips to failed.
campaignReleaseUn-lease claimed-but-unfinished rows (e.g. a crashed run) back to pending.
campaignStatusRead the header + per-status item counts.
campaignAddItemsAdd items to the current (or a named) cycle; refuses past max_items.
campaignRetryFailedRe-queue failed items (optionally a specific key list) back to pending.
campaignSetStatusPause/resume/mark done; pausing stamps paused_by (default user:<owner>; the scheduler's own zero-progress breaker stamps auto:zero-progress).
campaignDeleteSoft-delete (deleted_at + status='done'); releases held leases; history stays queryable.
campaignTasks / campaignUpsertTasks / campaignSetTaskActiveRead/edit the campaign's editable task-row list (the Task-app grid).
campaignResults / campaignPivotRecent per-item results (optionally rolled up per cycle) / a tasks-×-runs reporting grid.
chainStepThe 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.
counterStepA 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

  1. 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 when campaign-config.schedule is set.
  2. runAgain self-scheduling — a campaign saved without a cron schedule ends its pipeline with agent: scheduler, operation: runAgain, in: "1h" (or another duration). This creates exactly ONE @once job for the workflow and never stacks — an existing pending @once job for the same workflow is re-timed, not duplicated. See plungeai-scheduling for @once/runAgain mechanics 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/to needs 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

  • upsertRow is insert-only, not keyed — it does not match matchFields or update in place, and a JSON-string data is not parsed. For an idempotent per-item write use chainStep or explicit top-level col_<name> columns (insertRow/upsertRow substitute {item.*} only into top-level string fields); to update an existing row use updateRows.
  • 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.

Planned: TI-34

The docs assistant is not available yet. You can hand these docs to your own assistant instead.