Skip to content
Back to skills

Schedule Ads

ASecurity

Schedule paid ads across Meta/TikTok/Snapchat/Pinterest/LinkedIn via the AdManage.ai API, driven by a declarative config file. Launches PAUSED by default — never auto-activates live spend.

  • 6 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 5, 2026
content-marketinggorailsapi

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 5, 2026

npx -y skills add anajuliabit/aeon --skill schedule-ads --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Schedule Ads?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Schedule Ads
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/anajuliabit-schedule-ads/badge)](https://www.skillsdirectory.com/skills/anajuliabit-schedule-ads)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: Schedule Ads
description: Schedule paid ads across Meta/TikTok/Snapchat/Pinterest/LinkedIn via the AdManage.ai API, driven by a declarative config file. Launches PAUSED by default — never auto-activates live spend.
schedule: "0 8 * * *"
commits: true
permissions:
  - contents:write
tags: [growth, ads]
---

Reads `skills/schedule-ads/config.yaml`, picks schedule entries matching today, and queues ad launches via AdManage.ai. The actual API calls happen in `scripts/postprocess-admanage.sh` (outside the sandbox, with full env access) — this skill just builds the launch payloads and drops them in `.pending-admanage/`.

Read `memory/MEMORY.md` for context. Read the last 3 days of `memory/logs/` for recent launch activity.

## Safety defaults

This skill **spends real money on ad platforms**. Guardrails, in priority order:

1. **PAUSED by default.** Every launch request sets the entity to PAUSED. The operator has to resume manually in the AdManage dashboard before spend starts. `launchPaused: false` in config is the explicit opt-out.
2. **Daily spend cap.** Before queueing any launches, postprocess checks `GET /v1/spend/daily` for today. If spend ≥ `dailySpendCap` in the config, all queued launches are skipped and a warning is notified. This is a circuit breaker, not a budget enforcer — platform budgets still apply.
3. **Dry-run mode.** If `DRY_RUN=true` in env or `dryRun: true` in config, the skill builds the payloads, writes them to `.pending-admanage/dryrun/`, notifies what *would* launch, and exits without calling the API.
4. **Config-only.** The skill does not invent campaigns, creative, or targeting. If there's no schedule for today, it exits cleanly with no API calls.
5. **Single source of truth.** All ads/campaigns/targeting live in `config.yaml`. The skill never generates new creative on the fly.

## Sandbox note

AdManage requires `Authorization: Bearer $ADMANAGE_API_KEY` on every endpoint. The sandbox blocks env var expansion in curl headers, so this skill **cannot make the API calls directly**. Instead:

- This skill writes launch intents to `.pending-admanage/launches/*.json` (one file per batch).
- After Claude finishes, the workflow runs `scripts/postprocess-admanage.sh`, which has full env access. That script calls `POST /v1/launch`, polls `GET /v1/batch-status/{id}`, and notifies the result via `./notify`.
- The skill never sees or touches the API key.

If `scripts/postprocess-admanage.sh` is missing, the skill still queues correctly — the payloads just sit in `.pending-admanage/launches/` until the script exists. Log a warning and carry on.

## Steps

1. **Load config.** Read `skills/schedule-ads/config.yaml`. If the file doesn't exist, log `SCHEDULE_ADS_NOT_CONFIGURED` and exit cleanly (no notify, no error). The example template lives next to this file as `config.example.yaml`.

2. **Validate config shape.** Required top-level keys: `defaults` (with `adAccountId`, `workspaceId`, `page`), and `schedules` (array). If either is missing, file an issue in `memory/issues/` per the CLAUDE.md issue tracker convention, notify once, and exit.

3. **Pick today's schedule entries.** For each entry in `schedules`, match against today's date:
   - `when.everyDay: true` → always matches.
   - `when.dayOfWeek: monday` (or any weekday name, lowercase) → matches if today is that weekday (UTC).
   - `when.date: "2026-04-25"` → matches only on that exact date.
   - `when.dates: ["2026-04-25", "2026-05-02"]` → matches if today is in the list.
   - `when.cron: "0 8 * * 1"` → (advanced) matches if today satisfies the cron. Optional — skip if it's too much parsing effort.

   If no entries match today, log `SCHEDULE_ADS_NOTHING_TODAY` and exit cleanly (no notify).

4. **Build launch payloads.** For each matching schedule entry, construct the AdManage `POST /v1/launch` body:
   ```json
   {
     "ads": [
       {
         "adName": "<templated from ad.adName, {date} replaced>",
         "adAccountId": "<from defaults or entry override>",
         "workspaceId": "<from defaults or entry override>",
         "title": "<from ad>",
         "description": "<from ad>",
         "cta": "<from ad or defaults.cta>",
         "link": "<from ad>",
         "page": "<from defaults>",
         "insta": "<from defaults, Meta only>",
         "adSets": [ { "value": "<id>", "label": "<name>" } ],
         "media": [ { "url": "<media url>" } ],
         "status": "PAUSED"
       }
     ]
   }
   ```
   Enforce `status: PAUSED` on every ad unless `defaults.launchPaused` is explicitly `false`. Never strip it silently.

   Template substitutions inside string fields:
   - `{date}` → today's ISO date (YYYY-MM-DD)
   - `{dateHuman}` → "April 21, 2026" style

5. **Pre-flight validation.** For each payload:
   - `media[*].url` must be an absolute `https://` URL. Reject entries with local paths or obviously broken URLs.
   - `adSets[*].value` must be a non-empty string. If missing, skip the entry with a warning in the log.
   - For Meta entries (`adAccountId` starts with `act_`): `page` and `insta` must be set. TikTok/Snapchat/etc. have their own requirements — don't block on Meta-specific fields for other platforms.
   - `title` and `description` must be non-empty.

   Drop invalid entries, keep going. Log which ones were skipped and why.

6. **Handle dry-run.** If `DRY_RUN=true` or `config.dryRun: true`:
   - Write payloads to `.pending-admanage/dryrun/{schedule-name}-{timestamp}.json`.
   - Notify a preview (see step 9) but with `[DRY RUN]` prefix.
   - Skip step 7.
   - This mode exists for the operator to sanity-check before arming real launches.

7. **Queue for postprocess.** Write each launch payload to `.pending-admanage/launches/{schedule-name}-{timestamp}.json`:
   ```json
   {
     "schedule": "<entry name>",
     "queuedAt": "<iso timestamp>",
     "dailySpendCap": <number | null>,
     "payload": { "ads": [ ... ] }
   }
   ```
   `postprocess-admanage.sh` will pick these up after Claude exits, run the API calls with real env, poll batch status, and fire its own notifications.

8. **Write artifact to `.outputs/schedule-ads.md`** so downstream chain consumers can read what was queued. Format:
   ```markdown
   # Schedule Ads — ${today}

   Queued: N launches across M schedules.
   Dry-run: yes|no.

   ## Entries
   - <schedule name>: <ad count> ads, platform=<meta|tiktok|…>, paused=<bool>
     - <adName> — <title>
   ```

9. **Notify** via `./notify`. Keep it tight:
   ```
   *Ads queued — ${today}${dryRunSuffix}*

   <N> launches queued from <M> schedules.

   - <schedule name> → <ad count> ads <platform> <paused|LIVE>
     "<first adName>"
   - ...

   <if dry-run>
   no API calls made — remove DRY_RUN to arm.
   <else>
   postprocess-admanage will call AdManage and report batch results.
   ```
   If nothing was queued (no schedules matched), don't notify at all.

10. **Log to `memory/logs/${today}.md`:**
    ```
    ## Schedule Ads
    - Schedules matching today: <names>
    - Payloads queued: <count> (dry-run: <bool>)
    - Files written: .pending-admanage/launches/*.json
    ```

## Config schema

See `skills/schedule-ads/config.example.yaml` for a filled-in template. Minimum viable config:

```yaml
defaults:
  adAccountId: act_XXXXXXXXXX
  workspaceId: XXXXXXXXXXXX
  page: XXXXXXXXXXXX         # Meta Page ID
  insta: XXXXXXXXXXXX        # Instagram user ID
  cta: LEARN_MORE
  launchPaused: true         # NEVER change this without thought
  dailySpendCap: 50          # USD. Circuit breaker.
  dryRun: false

schedules:
  - name: weekly-promo
    platform: meta
    when: { dayOfWeek: monday }
    adSets:
      - { value: "120xxxxxxxxxxxxx", label: "US Broad 25-55" }
    ads:
      - adName: "Weekly promo — {date}"
        title: "Headline copy here"
        description: "Supporting copy in a sentence or two."
        cta: LEARN_MORE
        link: https://example.com
        media:
          - url: https://media.admanage.ai/your-account/hero.mp4
```

## What it does NOT do

- **Does not create campaigns or ad sets.** Those must pre-exist in AdManage (use the dashboard or `POST /v1/manage/create-campaign` separately). This skill only launches *ads into existing ad sets*.
- **Does not upload creative.** Media URLs must be hosted somewhere accessible (AdManage CDN, your own CDN, Supabase, wherever). If you need upload, add a separate `upload-ad-media` skill that calls `POST /v1/media/upload/url`.
- **Does not generate copy.** Titles/descriptions come from config. If the operator wants AI-written variants, a separate skill can write them into `config.yaml` and commit — keeps the launch path boring and auditable.
- **Does not manage budgets, bids, or targeting.** Everything downstream of launch (scaling, pausing losers, budget shifts) lives in follow-up skills or the dashboard.
- **Does not launch to Google Ads, Axon, or Taboola** in v1. Config schema is deliberately Meta/TikTok/Snapchat/Pinterest/LinkedIn-shaped. Adding Google/Axon later is straightforward but their launch shapes differ enough to need their own validation.

## Environment Variables

- `ADMANAGE_API_KEY` — required for `scripts/postprocess-admanage.sh`. Never read by this skill.
- `DRY_RUN` — optional. If `true`, forces dry-run mode regardless of config.
- Notification channels configured via repo secrets (see CLAUDE.md).

## Output

End with a `## Summary` block: schedules matched today, payload count, dry-run yes/no, files written.

Files in this skill

  • SKILL.md9.3 KB
  • config.example.yaml4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…