workflow-combo-loop — PLAN → TAG → LAUNCH → READ → RECODE → UPDATE
The 10-05 Dawn combo map (output/dawn-combo-map-2026-10-05/MAP.md) was a one-off. This skill makes it a loop every creative pipeline runs through: concepts are planned as one-variable combos off a paying parent, tagged at birth so the read never has to guess, and the map refreshes itself weekly from Meta (read-only) so the next plan starts from the newest winners, not last week's.
1. Read first
knowledge/ad-formats/_FORMAT-CORE.md(§1 doctrine, §6 launch + read). Binding.knowledge/ad-formats/_MODEL-REGISTRY.md(Sonnet = coding role).output/combo-loop/dawn/latest/MAP.md: the current map, DIFF vs the last snapshot, frequency/decay per family, recipes.scripts/combo_loop/codebook_dawn.md: the codebook (dimensions + allowed values). Same codebook for tags and for coding.references/procedure.md: commands, file layout, the ad-name ↔ tag mapping, how recipes/confidence are computed, an ILLUSTRATIVE run.references/sources.md: where this came from and the receipts.- Stake bank:
output/creative-strategy-cooper-2026-10-05/dawn-stakes.md.
2. When to use / when not
- Use: before any new Dawn concept in any pipeline (Step 0 of every format-/workflow- pack spec); when Fish asks what to test next, what's winning by dimension, or whether a format's frequency/decay holds; weekly refresh; tagging a pack/job before launch.
- Not: judging a single ad set (that is
media-buyer: 7-day rule, never pause); writing copy (format beat sheet → copy routing in core §1); the hub atlas avatar × angle labels (atlas_label.py, still used by dawn-native-brief; this loop codes 12 dimensions, the atlas 2). - Curvelle: config exists in
scripts/combo_loop/brands.jsonbut refresh refuses until it has its own codebook + breakeven ROAS (media-buyer owns the number).
3. Inputs
output/combo-loop/<brand>/latest/recipes.jsonfrom the last refresh (plan step).- The format skill name (
--format format-song,dawn-statics,dawn-native, …) and the brand. - For refresh:
META_SYSTEM_TOKENin the workspace.env(GET only), theclaudeCLI for Sonnet coding. - For tags: the pack (
output/concept-packs/<slug>.md), the ugc-omni/yapperjob.json, the ui-mockupspec.json, or a hubads.jsonentry withmeta_ad_name.
4. Outputs
output/combo-loop/<brand>/<date>/:MAP.md(DIFF, frequency + decay, recipes, coded-this-run, uncoded, invalid tags),recipes.json(per family: winning value per dimension with spend/ROAS/n/W/L/confidence, runner-up, avoid list, top/weak pairs, paying parents),units.json,agg.json,family.json,ads.json,weekly.json.latest→ newest date.output/combo-loop/<brand>/codes.json: cumulative codes (baseline 10-05 + Sonnet + tags never overwritten).output/combo-loop/<brand>/plans/<date>-<format>.md: the plan block a pack pastes.output/combo-loop/<brand>/tagged/<prefix>.md: registered tags for work with no pack file.
5. Process
- PLAN (free, no network).
python3 scripts/combo_loop/plan.py --format <skill> --brand dawn [--n 4]→ 3-5 combos, each = a named paying parent (spend, ROAS, CPA) + ONE changed dimension: exploit (move toward the recipe winner), stake swap (a stake-bank row that lands on her), explore (a winner from another family never tried here). Thin families (video today) plan from the song recipe (story formats) or the static recipe (callouts) and say so. Pick with Fish; never stack two variables. - TAG (free). Paste the plan's yaml block into the pack front matter / job or spec JSON (
"combo": {...}); setcombo_ad_prefixto the exact string the launched Meta ad names will start with. No file (natives, statics) →python3 scripts/combo_loop/tags.py register --prefix N1006-A combo_avatar=... .... Gate:python3 scripts/combo_loop/tags.py check <file>PASS.tags.py keyslists allowed values. - LAUNCH. Normal launch path (core §6): own ad set per concept, preflight, dof guard, Fish's go. Ad names MUST start with
combo_ad_prefix(copies/SCALE dupes keep the prefix; "LP Split · ", "LW · ", "- Copy", "- SCALE" are stripped before matching). - READ + RECODE + UPDATE (free Meta GETs; Sonnet via subscription).
python3 scripts/combo_loop/refresh.py --brand dawn(weekly via launchd, or on demand): pulls lifetime + weekly per-ad insights (≤1 req/s), assigns every ad to a unit (tags > existing code by ad base name > existing code by body hash), codes only NEW concepts ≥$100 with the codebook verbatim (cap 40/run, sure/unsure per field), aggregates per family (song / video / static), writes the dated snapshot + DIFF vs the previous one.--no-codeskips Sonnet;--offlinere-aggregates saved pulls. - Review (Fish). Read the DIFF: recipe flips, cells that moved ≥0.05 ROAS, new winners/losers, frequency/decay per family. Check
unsurefields on new codes before trusting a recipe that rests on them; correct a code by editingcodes.json(set"c": "sure"with your value). - Next plan starts from the new
latest/(step 1). The loop closes.
6. Tools & models
- Built 10-06:
scripts/combo_loop/refresh.py,scripts/combo_loop/plan.py,scripts/combo_loop/tags.py,scripts/combo_loop/common.py,scripts/combo_loop/brands.json,scripts/combo_loop/codebook_dawn.md. - Coding role = Sonnet via
scripts/claude_cli.pyrun_sonnet(OAuth subscription; ~1-2 min per 8 concepts). No paid API. - Meta: GET only (insights, creatives). The system user cannot read video
length(#10 permission): video concepts are coded from the script (hubmeta_ad_namescripts, 10-05 song lyrics) or primary text, and say so. - Schedule:
output/combo-loop/com.fish.combo-loop.plist(weekly Monday 06:15, read-only, NOT installed; Fish installs). - Built 10-06 late: hub page for the map —
scripts/build_pipeline_hub.pycombo_map_md()→f/combo-map.html(nav link + foundations card on the pipeline hub), readsoutput/combo-loop/<brand>/latest/MAP.mdper brand (dawn, curvelle). Discord DIFF poster —scripts/combo_loop/discord_post.py(default prints only;--postrequired to actually send, viaDISCORD_BOT_TOKEN/DISCORD_PIPELINE_QA_CHANNEL). Curvelle codebook —scripts/combo_loop/codebook_curvelle.md(mirrorscodebook_dawn.md's 14 fields fromknowledge/brands/curvelle/*);brands.jsoncurvelle.codebookpoints at it.breakeven_roasfor Curvelle stays null (media-buyer's number) —refresh.pystill refuses Curvelle until that lands.
7. Gates & QA checklist
- [ ] Plan: each concept names ONE variable and a paying parent with CPA; stake lines come from the bank with their source tag (VOC grep rule, corrections 10-06).
- [ ] Tags:
tags.py checkPASS;combo_ad_prefixequals the launch script's ad-name prefix (read the launch script before launch). - [ ] Refresh: MAP.md written,
latestmoved, Meta GET count printed, no token in logs; "Uncoded" list empty or explained by the cap. - [ ] Before quoting a recipe: confidence ≥ med, or say "signal, not proof" (songs: few spenders).
- [ ] Never act on the map with a Meta write from the loop; launches and budget changes go through media-buyer + Fish.
8. Failure modes
- Prefix drift: ads launched with a name that doesn't start with
combo_ad_prefixfall back to Sonnet coding (lower confidence). Fix the tag's prefix, re-run refresh. - Two variables changed: the read is unattributable; the plan block lists "keep fixed" for that reason.
- Proxy recipes: thin families (video) borrow the song/static recipe; a result there tests the format AND the variable. Read it as a format test first.
- Body-hash drift: a static whose primary text changes becomes a new concept; that's correct (it is a new message), but its history stays under the old key.
- Coding from captions: video concepts without a script are coded from the primary text; Sonnet marks those fields unsure. Add a hub
script+meta_ad_nameor tag the pack. - Small-n songs: 3-5 spenders carry the song recipe; the DIFF shows when it changes. Don't extend a song finding to statics or vice versa (MAP §2 "what changes between formats").
9. Sources
- 10-05 build:
output/dawn-combo-map-2026-10-05/MAP.md+ its scripts (promoted, not moved), plan~/.claude/plans/dawn-combo-map-2026-10-05.md. - Fish 10-06: "map stakes / emotions / payoffs / root cause / mechanism and authority/pov and angles with each other … plan like that in the pipelines … and also have feedback loops".
references/sources.mdfor receipts; hub atlas precedentproject_angle_atlas.md(labels at planning time, ad-name prefix).