---
name: bulk-uploader
description: Activates when the user wants to bulk upload Meta ads, push a spreadsheet of ads to Meta Ads Manager, launch a batch of campaigns, or run multiple ad creatives at scale. Reads from a CSV/Google Sheets spreadsheet of ad definitions and creates the campaign/ad set/ad objects in Meta via the Composio Metaads MCP server. Always defaults ads to PAUSED for safety until the user explicitly activates them. Includes UTM auto-population, error handling, and result logging back to the source sheet.
---

# Bulk Uploader Skill

This is the distribution layer of Pillar 2. It takes a spreadsheet of ads (one row per ad) and creates them all in Meta Ads Manager via the Composio Metaads MCP. Replaces three hours of manual Ads Manager clicking with twenty minutes of structured spreadsheet work.

## Activation Rules

Activate when:
- The user asks to "bulk upload ads," "push ads to Meta," "launch a batch of campaigns," or anything similar
- The user runs any of the slash commands defined below
- A previous skill (creative-strategist, static-ad-designer) has produced a creative matrix that's ready for distribution

## Prerequisites

1. **Composio MCP installed and connected to Meta Ads.** Test by running:
   ```
   List my Meta ad accounts via Composio.
   ```
   If you see ad accounts, you're connected. If not, run the Composio setup (see Pillar 2 documentation).

2. **A bulk upload spreadsheet** with the standard 22-column structure (use `bulk-upload-template.csv` as the starting point).

3. **Creatives uploaded to your Meta Creative Library.** This skill references creatives by Asset ID; it doesn't upload images. Upload images to Meta first, then put the Asset IDs in column N of the spreadsheet.

4. **Brand kit installed** (`brand-kit.md` in project root) — used for naming convention validation.

---

## Spreadsheet Schema (22 columns)

The spreadsheet must have these columns in this order. The skill will validate the headers before processing.

| Column | Field | Type | Notes |
|---|---|---|---|
| A | campaign_name | string | Use brand-kit naming convention |
| B | campaign_objective | enum | OUTCOME_SALES, OUTCOME_LEADS, OUTCOME_TRAFFIC, OUTCOME_AWARENESS |
| C | daily_budget_cents | integer | $20/day = 2000 |
| D | adset_name | string | Use brand-kit naming convention |
| E | targeting_age_min | integer | 18-65 |
| F | targeting_age_max | integer | 18-65 |
| G | targeting_genders | integer | 0=all, 1=male, 2=female |
| H | targeting_interests | string | Comma-separated Meta interest IDs |
| I | optimization_goal | enum | CONVERSIONS, LINK_CLICKS, LANDING_PAGE_VIEWS, etc. |
| J | ad_name | string | Use brand-kit naming convention |
| K | headline | string | 40 chars recommended max |
| L | primary_text | string | The main ad copy |
| M | description | string | Optional link description |
| N | creative_asset_id | string | Asset ID from Meta Creative Library |
| O | call_to_action | enum | SHOP_NOW, LEARN_MORE, SIGN_UP, GET_OFFER, BOOK_TRAVEL, etc. |
| P | destination_url | URL | The landing page URL |
| Q | utm_source | string | Default: facebook |
| R | utm_medium | string | Default: paid |
| S | utm_campaign | string | Auto-populated from campaign_name |
| T | utm_content | string | Auto-populated from ad_name |
| U | status | enum | PAUSED or ACTIVE — always default to PAUSED |
| V | upload_result | string | Blank initially; this skill writes results here |

---

## Slash Commands

### `/bulk-upload [spreadsheet-path]`

The main command. Reads the spreadsheet and creates all ads via Composio.

**Usage:**
```
/bulk-upload ./ads-2026-08-01.csv
```

**Or with a Google Sheets URL:**
```
/bulk-upload https://docs.google.com/spreadsheets/d/[ID]/edit
```

**Workflow:**
1. Read the spreadsheet (CSV or Google Sheets URL)
2. Validate column headers match the schema
3. Validate every row:
   - Required fields are present
   - Naming conventions match brand-kit pattern
   - Creative asset IDs look valid (numeric, 15+ digits)
   - URLs are well-formed
4. Show summary before starting:
   - Total ads to create
   - Total campaigns to create (if not already existing)
   - Total ad sets to create
   - Confirmation prompt
5. For each row:
   - Check if campaign exists (by name); create if not
   - Check if ad set exists (by name within campaign); create if not
   - Append UTM parameters to destination URL if not already present
   - Create the ad with creative, headline, primary text, CTA, URL
   - Set status to whatever's in column U (default PAUSED)
   - Write success/error message to column V
6. After every 10 rows, show progress summary
7. At end, return final summary with counts and any errors

**Critical defaults:**
- ALWAYS default to PAUSED status, even if the spreadsheet says ACTIVE on the first run. Show a confirmation that says "Spreadsheet shows N ads as ACTIVE. Override and set all to PAUSED for safety review? (y/n)"
- Never bulk-activate ads without explicit user confirmation

### `/activate-batch [pattern]`

Activate previously-uploaded ads that are currently PAUSED.

**Usage:**
```
/activate-batch vitapure_sales_collagen_q3_*
```

**Workflow:**
1. Query Meta for all ads matching the pattern in PAUSED status
2. Show count and confirmation
3. Activate all matching ads
4. Return summary

### `/dry-run [spreadsheet-path]`

Validate the spreadsheet without actually uploading anything. Useful for catching errors before committing.

**Workflow:**
1. Read and validate the spreadsheet
2. Report any rows with errors
3. Show what WOULD be created (campaign count, ad set count, ad count)
4. Show estimated total monthly spend (sum of daily budgets * 30)
5. Do NOT make any API calls to Meta

### `/upload-and-activate [spreadsheet-path]`

For experienced users only. Skips the PAUSED-by-default safety check and activates immediately.

**Always require explicit double-confirmation before running.** Show:
```
⚠️ WARNING: This will create AND ACTIVATE [N] ads with [$X] in daily budget 
(~$[X*30]/month). 

Are you ABSOLUTELY sure? Type "I confirm" to proceed.
```

### `/refresh-from-matrix`

Pull a creative matrix from the Static Ads Engine and auto-populate a fresh spreadsheet ready for upload.

**Workflow:**
1. Read `./creative-matrix.json` (output of Pillar 1's static-ad-designer skill)
2. Read brand-kit.md for defaults (targeting, budget, objective, UTM defaults)
3. Generate a new spreadsheet at `./bulk-upload-[date].csv`
4. Leave creative_asset_id blank (user must fill in after uploading images to Meta)
5. Set all rows to PAUSED status
6. Open the spreadsheet for the user to review and add asset IDs

This is the primary integration point with Pillar 1. Run this after generating new ad images, fill in the asset IDs, then run `/bulk-upload` on the result.

---

## Error Handling

When an individual row fails to upload, write the error to column V and continue with the next row. Do NOT stop the entire batch on a single failure.

**Common errors and what to write to column V:**

- `ERROR: Invalid creative asset ID — image may not be finished processing in Meta. Wait 5 minutes and re-run this row.`
- `ERROR: Invalid targeting interest ID. Use Meta's targeting search to find correct IDs.`
- `ERROR: Daily budget below Meta's minimum for this objective and country.`
- `ERROR: Headline exceeds Meta's character limit.`
- `ERROR: Destination URL not whitelisted on this Page.`
- `ERROR: Naming convention mismatch — does not match brand-kit pattern.`
- `SUCCESS: Created ad ID [Meta ad ID]`

After the batch completes, show a summary:
```
Bulk upload complete.
Total rows: [N]
Successful: [N]
Failed: [N]

Failed rows: [list of row numbers + error summaries]

All successful ads are PAUSED. To review them in Meta Ads Manager, 
go to: https://business.facebook.com/adsmanager/
```

---

## Naming Convention Enforcement

Every campaign_name, adset_name, and ad_name must match the pattern from brand-kit.md:

```
[brand]_[objective]_[hook]_[persona]_[style]_[version]_[date]
```

Before uploading, validate every name. If any name doesn't match:
```
WARNING: Row [N] has ad_name "[name]" which does not match the brand-kit 
naming convention. This will break Pillar 3's automated analytics.

Fix automatically? (y/n)
```

If the user says yes, attempt to fix by parsing the row's other fields (hook, persona, style, etc.) and reconstructing the name.

---

## Cross-Skill Integration

**Calls into this skill:**
- The user (directly)
- creative-strategist (Pillar 3) when recommending a new batch to launch
- The "weekly ad refresh" workflow (chained skill from Pillar 2)

**This skill calls:**
- brand-kit (read naming convention)
- Composio Metaads MCP (for actual Meta API operations)

**Output flows to:**
- Pillar 3 (Creative Analytics) — once ads have been running for 14+ days
- Spreadsheet column V (success/error logging)

---

## Output Format

After `/bulk-upload`, return:

```
[✓] Bulk upload complete.

Created:
  - [N] campaigns
  - [N] ad sets  
  - [N] ads

Status: All ads are PAUSED.
Total daily budget if activated: $[X]
Estimated monthly spend: $[X*30]

Errors: [N rows]
[List of failed rows with error messages]

Next steps:
1. Review ads in Meta Ads Manager: https://business.facebook.com/adsmanager/
2. Spot-check 3-5 ads for correctness (headline, image, URL)
3. Activate when ready: /activate-batch [campaign_pattern]
4. After 14 days, run /creative-analyzer (Pillar 3) to see what's working
```

---

## Notes

- Composio's Metaads MCP handles OAuth refresh automatically. If you see auth errors, your Composio session may need re-authorization. Run the Composio setup again.
- Meta has rate limits on the Marketing API. For batches over 100 ads, the skill will automatically add 2-second delays between API calls.
- For agencies managing multiple client accounts, switch the Composio connection between accounts before running the skill. The skill uses whatever account is currently connected.
- This skill creates the campaign/ad set/ad structure but does NOT upload images. Upload your images to Meta Creative Library first, copy the Asset IDs, paste them into column N of your spreadsheet, then run this skill.
