Appearance
campaign-manager
FreshSource: 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
| Field | Required | Default | Description |
|---|---|---|---|
name | yes | - | Human-readable label |
plugin | yes | - | Plugin name (ctr-engine, cascade-runner, etc.) |
pluginExport | yes | - | Function to call (runCTR, runCascade) |
accountSource | no | manual | store or manual |
accounts | if manual | - | Array of emails |
accountFilter | if store | {} | {"status":"active","phase":2} |
concurrency | no | 3 | Simultaneous accounts |
scheduleEverySec | no | 600 | Min seconds between runs per account |
maxFailures | no | 5 | Consecutive failures = dead |
maxRunsPerAccount | no | Infinity | Total per account |
maxClicksPerActivation | no | Infinity | Total across all accounts |
jobConfig | yes | - | 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.jsonOr 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.jsonHow 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
schedulerfor recurring schedules - Reports results to
result-storeplusanalytics