Skip to content

campaign-manager

Fresh

Source: BIBLE.md, bible/campaigns.md

Orchestrates campaigns with an account state machine. Rotates accounts, handles failures, respects cooldowns.

What it does

A campaign is a managed loop. It picks accounts from your pool, runs CTR (or other plugin) sessions through them, handles failures, respects cooldowns, and stops when complete.

Campaign config

json
{
  "name": "Dallas Junk Removal - May 2026",
  "plugin": "ctr-engine",
  "pluginExport": "runCTR",
  "accountSource": "store",
  "accountFilter": { "status": "active" },
  "concurrency": 3,
  "scheduleEverySec": 900,
  "maxRunsPerAccount": 30,
  "maxFailures": 5,
  "jobConfig": {
    "keyword": "junk removal dallas",
    "targetDomain": "journeyjunkremoval.com",
    "city": "Dallas",
    "state": "TX",
    "dwellTime": "medium"
  }
}

Fields

FieldRequiredDefaultDescription
nameyes-Human-readable label
pluginyes-Plugin name (ctr-engine, cascade-runner, etc.)
pluginExportyes-Function to call (runCTR, runCascade)
accountSourcenomanualstore or manual
accountsif manual-Array of emails
accountFilterif store{}{"status":"active","phase":2}
concurrencyno3Simultaneous accounts
scheduleEverySecno600Min seconds between runs per account
maxFailuresno5Consecutive failures = dead
maxRunsPerAccountnoInfinityTotal per account
maxClicksPerActivationnoInfinityTotal across all accounts
jobConfigyes-Params for the plugin

Account state machine

stateDiagram-v2
  [*] --> Idle
  Idle --> Running: Lock account
  Running --> Cooling: Success (1-3 min)
  Running --> Cooling: Failure (exponential backoff)
  Cooling --> Idle: Cooldown done
  Running --> Dead: 5 consecutive failures
  Dead --> [*]

States:

  • IDLE: waiting for turn
  • RUNNING: browser open, doing the work
  • COOLING: mandatory cooldown between runs (randomized)
  • DEAD: too many failures, removed from rotation

Backoff after failure: 1 min, 2 min, 4 min, 8 min, up to 60 min max.

accountSource: store mode

javascript
// campaign-manager flow with accountSource: 'store'
const accounts = await accountStore.list(accountFilter);
for (const account of accounts) {
  await accountStore.lock(account.id, campaignId);
  try {
    const result = await runJob(account);
    await accountStore.markAction(account.id, { type: 'ctr', result: 'success' });
  } catch (err) {
    await accountStore.markAction(account.id, { type: 'ctr', result: 'failure' });
  } finally {
    await accountStore.unlock(account.id);
  }
}

Custom campaigns

For dynamic per-run config (keyword rotation, multi-city, A/B testing), use a JavaScript module instead of JSON:

javascript
// campaigns/multi-keyword-dallas.mjs
const keywords = [
  'junk removal dallas',
  'junk removal near me',
  'dallas junk hauling',
];

export default {
  name: 'Dallas Multi-Keyword Push',
  plugin: 'ctr-engine',
  pluginExport: 'runCTR',
  accountSource: 'store',
  accountFilter: { status: 'active' },
  concurrency: 3,
  scheduleEverySec: 900,
  maxRunsPerAccount: 30,

  getJobConfig(account, runIndex) {
    const keyword = keywords[runIndex % keywords.length];
    return {
      email: account.email,
      keyword,
      targetDomain: 'journeyjunkremoval.com',
      city: 'Dallas',
      state: 'TX',
      dwellTime: runIndex < 5 ? 'short' : 'medium',
    };
  },
};

How to run

bash
node cli.mjs campaign --config campaigns/dallas-junk-removal.json

Or via API:

bash
curl -X POST http://localhost:4700/campaigns/run \
  -H "Content-Type: application/json" \
  -H "x-api-key: ghost-engine-key" \
  -d @campaigns/dallas-junk-removal.json

How it fits with other plugins

  • Reads accounts from account-store
  • Locks and unlocks via account-store routes
  • Calls ctr-engine.runCTR, cascade-runner.runCascade, or other plugin
  • Triggered by scheduler for recurring schedules
  • Reports results to result-store plus analytics