Create Campaign
This skill provisions Meta advertising campaigns and ad sets on AdManage.ai by reading a declarative configuration file, determining which entities don't already exist, and queuing creation requests. Use it on-demand before launching ad creatives by manually invoking it to create paused campaigns and ad sets, then reference the returned IDs in your scheduling configuration to activate them later. The skill prioritizes safety by creating all entities in paused status, maintaining idempotency through state tracking, and supporting dry-run mode to preview payloads without making API calls.
git clone --depth 1 https://github.com/aaronjmars/aeon /tmp/create-campaign && cp -r /tmp/create-campaign/skills/create-campaign ~/.claude/skills/create-campaignSKILL.md
Reads `skills/create-campaign/config.yaml`, figures out which campaigns/ad sets don't exist yet, and queues create requests to `.pending-admanage/creates/`. The credentialed API calls happen in `scripts/postprocess-admanage-create.sh` after Claude finishes.
This skill is **on-demand** — no `schedule:` in frontmatter. Invoke it manually when you want to provision new campaigns, then reference the returned IDs in `schedules-ads/config.yaml` to launch creatives into them.
Read `memory/MEMORY.md` for context. Read `.admanage-state/campaigns.json` (if it exists) to see what's already created.
## What this skill provisions
Two entity types only:
1. **Meta campaigns** — name, objective, budget, bid strategy, promoted object.
2. **Meta ad sets** — name, budget, optimization goal, targeting (geo/age/platforms), destination.
Everything else (TikTok/Snapchat/Pinterest/LinkedIn campaigns, advanced Meta fields like valueRuleSetId or Advantage+ catalog) is v2+. The shape below is intentionally minimal.
## Safety defaults
Same posture as schedule-ads:
1. **PAUSED by default.** Every campaign + ad set is created with `status: PAUSED`. No surprise spend.
2. **Idempotent.** The skill tracks created entities in `.admanage-state/campaigns.json`. If a campaign name already exists in state, it's skipped. Run the skill twice → no duplicates.
3. **Dry-run mode.** `DRY_RUN=true` or `config.dryRun: true` → payloads written to `.pending-admanage/dryrun-create/`, notified, no API calls.
4. **Config-only.** No config file → exit silently. No invented campaigns, no autonomous provisioning.
## Sandbox note
Every `/manage/*` endpoint requires `Authorization: Bearer $ADMANAGE_API_KEY`. Sandbox blocks env-var expansion in curl headers, so this skill queues intents only:
- Skill writes: `.pending-admanage/creates/campaigns/<slug>.json` and `.pending-admanage/creates/adsets/<campaign-slug>__<adset-slug>.json`
- After Claude exits, `scripts/postprocess-admanage-create.sh` runs with full env access, makes the API calls in the right order (campaigns first, then ad sets referencing returned campaign IDs), and writes results back to `.admanage-state/campaigns.json`.
If the postprocess script is missing, the skill still queues correctly — the payloads sit in `.pending-admanage/creates/` until the script exists.
## Steps
1. **Load config.** Read `skills/create-campaign/config.yaml`. If it doesn't exist, log `CREATE_CAMPAIGN_NOT_CONFIGURED` and exit cleanly (no notify).
2. **Load state.** Read `.admanage-state/campaigns.json`. If it doesn't exist, treat as empty. Shape:
```json
{
"campaigns": [
{
"configName": "Prospecting — Q2 2026",
"campaignId": "120251616228380456",
"adAccountId": "act_xxx",
"createdAt": "2026-04-21T08:00:00Z",
"adSets": [
{
"configName": "US Broad 25-54",
"adSetId": "120251616242460456",
"createdAt": "2026-04-21T08:00:04Z"
}
]
}
]
}
```
3. **Validate config shape.** Required: `defaults.adAccountId`, `defaults.workspaceId`, `campaigns[]`. Each campaign needs `name` and `objective`. Each ad set needs `name`, and either `optimizationGoal` (explicit) or a compatible parent objective. If validation fails, file an issue in `memory/issues/` and exit.
4. **Compute diff.** For each campaign in config:
- Match against state by exact `name`. If present, mark as `existing`.
- If missing, mark as `new` and queue a campaign create.
- For each ad set under the campaign, match against the parent's `adSets[]` in state by name. If missing, queue an ad-set create (with a `parentCampaignConfigName` reference that postprocess will resolve to a real campaign ID).
If nothing is new, log `CREATE_CAMPAIGN_ALL_EXIST` and exit without notify.
5. **Build campaign create payloads.** Per the AdManage `POST /v1/manage/create-campaign` shape:
```json
{
"businessId": "<adAccountId>",
"workspaceId": "<workspaceId>",
"name": "<campaign.name>",
"objective": "<campaign.objective>",
"status": "PAUSED",
"buyingType": "AUCTION",
"specialAdCategories": [],
"dailyBudget": <number>,
"bidStrategy": "<LOWEST_COST_WITHOUT_CAP | LOWEST_COST_WITH_BID_CAP | COST_CAP | ...>",
"promotedObject": { ... }
}
```
Skip keys that are `null`/absent in config — don't send empty strings. Always force `status: PAUSED` unless `defaults.launchPaused: false` is set explicitly.
6. **Build ad-set create payloads.** Per `POST /v1/manage/create-adset`:
```json
{
"businessId": "<adAccountId>",
"workspaceId": "<workspaceId>",
"campaignId": "__RESOLVE_FROM_PARENT__",
"parentCampaignConfigName": "<campaign.name>",
"name": "<adSet.name>",
"status": "PAUSED",
"dailyBudget": <number>,
"billingEvent": "IMPRESSIONS",
"optimizationGoal": "<LANDING_PAGE_VIEWS | OFFSITE_CONVERSIONS | ...>",
"destinationType": "<WEBSITE | PHONE_CALL | MESSAGING_... | ...>",
"targeting": { ... },
"promotedObject": { ... }
}
```
The `__RESOLVE_FROM_PARENT__` sentinel + `parentCampaignConfigName` tells postprocess to look up the campaign ID after the campaign create succeeds. If the parent campaign was *existing* (already in state), write the real campaign ID directly and drop the sentinel.
7. **Pre-flight validation.**
- `adAccountId` must start with `act_` (this skill is Meta-only in v1).
- `dailyBudget` must be a positive number in dollars (not cents).
- `objective` must be one of the documented Meta objectives: `OUTCOME_TRAFFIC`, `OUTCOME_ENGAGEMENT`, `OUTCOME_LEADS`, `OUTCOME_AWARENESS`, `OUTCOME_SALES`, `OUTCOME_APP_PROMOTION`.
- Targeting `geo_locations.countries` must be a non-empty array.
Drop invalid entries, keep going, log what was skipped and why.
8. **Handle dry-run.** If `DRY_RUN=true` or `config.dryRun: true`: write payloads to `.pending-admanage/dryrun-creaMention/keyword sweep on social platforms for [REPLACE: KEYWORDS] — trends, sentiment, top posts
5 concrete real-life actions, leverage-scored against open loops with specificity and anti-fluff gates
Curated AI-agent tweets, clustered into narratives with insight summaries
Tracker of AI agent substitution signals — which roles, companies, and industries show real headcount displacement. Named roles + real deployments only.
Competitive-intelligence digest on the AI agent framework space — momentum, releases, breaking changes across a curated watchlist
Cross-domain market pulse from AIXBT's free grounding endpoint — crypto, macro, tradfi, geopolitics. Refreshes taxonomy references (clusters, chains) as a bonus.
Pre-batch API provider health check — detects credit exhaustion or auth failure for every configured provider key before the scheduled batch runs, giving the operator a window to act before skills degrade
List a wallet's live ERC-20 token approvals on Base and flag unlimited / risky spender grants. Keyless via Base RPC (eth_getLogs + eth_call) — no explorer key needed.