Agent skill
viral-ig-marketer
Instagram growth orchestrator for any iOS app. Delegates research to viral-research and production to viral-producer. Owns the growth loop: pull analytics, select content direction from existing research briefs, run the virality gate, delegate rendering, send drafts to human via Telegram, reflect and update strategy. Pulls analytics via Instagram Graph API + RevenueCat every cycle and iterates experiments until MRR reaches the target. Use when running the marketing loop, checking analytics, or updating content strategy. Requires references/config.json to be filled before Cycle 0.
Install this agent skill to your Project
npx add-skill https://github.com/iamhenry/ai-project-starter-kit/tree/main/.opencode/skills/viral-ig-marketer
SKILL.md
Instagram Marketing Worker
Mission
- North star: reach the MRR target defined in
references/config.json→goal.mrrTargetUSD - Operational objective: grow weekly new paying subscribers through niche-relevant content on Instagram for the app defined in
references/config.json→app - Stop condition: MRR sustained at target for 2 consecutive months — OR — 8 consecutive weeks of zero subscriber growth (stall rule)
- Autonomy mode: semi-autonomous — human publishes all posts from Instagram app (bot detection risk). Agent generates content, sends drafts via Telegram, analyzes via Instagram Graph API, and recommends every cycle.
Operational Score
- Primary score: views per post — target 100k+ views. This is a viral content operation. Downloads and MRR follow reach.
- Leading indicators (fast proxies): post views (reach), shares (virality signal), saves (content value signal), profile visits (download intent)
- Lagging indicators: new paying subscribers per week (RevenueCat), MRR
- Direction: higher is better
- Review cadence: analytics pull every cycle; playbook + virality model updated every cycle after analytics pull
- North star check: monthly MRR via RevenueCat — if subscriber count grows but MRR doesn't, the problem is the app (onboarding, paywall, pricing), not the content
Verification Surface
| What to check | How to check | Good looks like | Cadence |
|---|---|---|---|
| Post views | Instagram Graph API: GET /{media-id}/insights |
Trending up vs previous cycle | Every cycle (previous cycle's post) |
| Post saves | Instagram Graph API: GET /{media-id}/insights |
See bootstrap priors in references/virality-model.md |
Every cycle (previous cycle's post) |
| Profile visits | Instagram Graph API: GET /{ig-user-id}/insights |
See bootstrap priors in references/virality-model.md |
Every cycle (previous cycle's post) |
| New paying subscribers | RevenueCat GET /projects/{id}/metrics | Trending up week-over-week | Every cycle (since previous cycle's post) |
| MRR | RevenueCat GET /projects/{id}/metrics | Tracking toward $10k/month | Monthly |
| Format comparison | references/results.jsonl (relative to skill dir) |
Tier performance comparison (T1/T2/P3 etc.) | Every cycle — inferred from results.jsonl running totals |
Environment
Instagram Graph API Setup
Before any Instagram API calls, load environment variables:
# Load env vars
export $(grep -v '^#' /home/node/openclaw/.env | xargs)
Key environment variables (all loaded from /home/node/openclaw/.env):
-
Keep each variable on its own line in
.env.FAL_KEYandREVENUECAT_PROJECT_IDmust never be concatenated. -
INSTAGRAM_ACCESS_TOKEN— long-lived token (60 days, auto-refreshed via cron) -
INSTAGRAM_BUSINESS_ACCOUNT_ID— Instagram Business Account ID -
FACEBOOK_PAGE_ID— Facebook Page ID -
REVENUECAT_API_KEY— RevenueCat API key -
REVENUECAT_PROJECT_ID— RevenueCat project ID
Common API calls:
# List recent media (find post IDs after human publishes)
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/media?fields=id,caption,media_type,timestamp,like_count,comments_count&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
# Per-post insights (carousel)
curl -s "https://graph.facebook.com/v22.0/{media-id}/insights?metric=reach,saved,likes,comments,shares,total_interactions&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
# Per-post insights (reel — includes views)
curl -s "https://graph.facebook.com/v22.0/{media-id}/insights?metric=reach,saved,likes,comments,shares,total_interactions,views&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
# Account-level insights
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/insights?metric=reach,follower_count&period=day&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
# Profile views (requires metric_type=total_value)
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/insights?metric=profile_views&metric_type=total_value&period=day&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
Media ID discovery: After human publishes, call GET /{ig-user-id}/media and match the most recent post by timestamp/caption to find the media ID. Store the media ID in results.jsonl for that cycle's entry.
API quirks (v22.0):
- Reels: use
viewsmetric (notplaysorimpressions) - Carousels:
impressionsis deprecated — usereachinstead - Profile views: requires
metric_type=total_valueparameter - Posts made before business account conversion have no insights
Action-to-Tool Map
| Action | Tool / API | Access | Checkpoint | Verification source |
|---|---|---|---|---|
| Research content angles | Read existing brief from ../viral-research/references/example-brief.md + ../viral-research/references/swipe-file.jsonl + ../viral-research/references/format-taxonomy.md. ⛔ NO instagram.com |
ready | autonomous | Content direction selected |
| Produce rendered Reel | Delegate to viral-producer skill (pass format tier, topic, hook, assets). See ../viral-producer/SKILL.md |
ready | autonomous | package in ../viral-producer/output/reels/<slug>/ |
| Generate images via fal.ai | fal.ai API — model + size from config.json → fal.defaultImageModel / fal.defaultImageSize, key in env var FAL_KEY |
ready | autonomous | producer-owned asset in ../viral-producer/output/assets/<slug>/ |
| Send draft to human via Telegram | Send reel .mp4 + caption in Telegram chat | ready | human-relay (human publishes) | .mp4 + caption received in chat |
| Pull post analytics | Instagram Graph API: GET /{media-id}/insights |
ready | autonomous | insights JSON returned |
| Discover published media ID | Instagram Graph API: GET /{ig-user-id}/media — match by timestamp/caption |
ready | autonomous | media ID stored in results.jsonl |
| Pull conversion data | RevenueCat GET /projects/{id}/metrics/overview | ready | autonomous | subscriber count + MRR |
| Score experiments + update playbook | AI analysis of results.jsonl | ready | autonomous | playbook.json updated |
| Flag fresh research needed | Non-blocking flag when all tier×angle combos exhausted AND 10-15+ cycles passed. Suggest what to research next. | ready | human-relay | Flag in morning report |
Permissions
- Read/write:
references/(all memory files — relative to skill dir) - Read/write:
output/(created on first run if missing — marketer memory only; rendered reels/assets live under../viral-producer/output/) - agent-browser: App Store only. Instagram is banned — see Off-limits. No logins, no engagement actions, no form submissions on any site.
- All secrets loaded from
/home/node/openclaw/.env— never hardcode IDs or tokens in skill files
Off-limits
- ⛔ NEVER use agent-browser to visit instagram.com — not for browsing, research, hashtags, profiles, explore, or anything else. Instagram has flagged the account for automated behavior. Any automated Instagram access risks permanent account disabling. This is a hard ban with zero exceptions.
- ⛔ NEVER inject Instagram cookies into agent-browser — the stealth-browser-auth method is banned for Instagram.
- Never purchase ads, boost posts, or spend money beyond fal.ai budget ceiling
- Never like, comment, follow, or DM any Instagram account — bot detection risk
- Never post more than 1x per cycle
- Never log individual subscriber PII from RevenueCat — aggregate metrics only
- Never make specific medical, legal, or clinical outcome claims in content
- Never modify
SKILL.md,soul.md, or sibling skill SKILL.md files — these are read-only. Flag the human if an urgent change is needed.
Inputs
| Input | Source | Quota / Limit | Legal constraint | If exhausted |
|---|---|---|---|---|
| Topic ideas | Web search: marketing blogs, Reddit, niche communities, newsletters (no Instagram browsing). 3-month recency rule: discard anything older than 90 days. | Unlimited | n/a | Broaden search angles within the defined niche — seasonal trends, adjacent community overlaps for hook inspiration. Never create content targeting a different niche. |
| Source reels for remix | Web search for viral reel URLs → yt-dlp download | No hard limit | Transformative use — new message, new CTA | Alternative format — use when research reveals a high-performing reel worth remixing |
| Niche trend data | Web search, UGC blogs (Later, Hootsuite, Buffer, Sprout Social), creator newsletters, Reddit | No hard limit | n/a | Broaden search terms, try adjacent niches for angle inspiration |
| RevenueCat metrics | V2 API | Rate limited | Aggregate only — no individual PII | Retry with exponential backoff |
| fal.ai image generation | fal.ai API (env var FAL_KEY) |
$10/month hard ceiling | Respect content policy | Fall back to gradient/solid backgrounds |
| App codebase | GitHub repo (app.githubRepo in config.json) read via gitingest CLI → cached in references/app-brief.md |
Unlimited (read-only) | Read-only, no forks or PRs | Use cached app-brief.md if repo unavailable |
| App Store listing | agent-browser → app.appStoreUrl in config.json |
No hard limit | Read-only observation only | Use cached data if unavailable |
| Support notes | references/config.json → app.supportNotes (optional, filled by human) |
n/a | n/a | Skip if empty |
On Start
Every cycle, in this order:
- Bootstrap directories (first-run or new environment): ensure
references/exists. Viral assets and reel packages are owned by../viral-producer/output/assets/and../viral-producer/output/reels/. Do not create duplicate local reel/assets dirs. - Read
references/results.jsonl— full history of every cycle: what was tested, what scored, what failed, what's pending - Check for stale entries: any entry with
"status": "pending"older than 5 days → update to"status": "stale"with note - Read
references/playbook.json— current best-known hooks, topics, CTAs, hashtag clusters, format mix, posting times - Read
references/experiment-framework.md— find the next queued experiment; this drives content decisions for the cycle - Read
references/virality-model.md— the agent's current plain-English algorithm for what makes content spread. This informs every content decision this cycle. - Cycle 0 config sync (first run only, when
playbook.json → cycleCount == 0):- Read
references/config.json(master config) - Write derived values to
../viral-producer/references/production-config.json(niche, brand colors, fal.ai settings, account phase) - Fill
cycle-000in results.jsonl with today's date + actual RevenueCat MRR + Instagram follower count - Confirm RevenueCat connection returns data
- Set
playbook.json → cycleCount = 1. Output baseline metrics (see format below). - This path never runs again once cycleCount > 0.
- Read
- Pull Instagram Graph API analytics for the post from the previous cycle (look up the most recent
"status": "pending"entry in results.jsonl by media ID) - Pull RevenueCat new subscriber delta for the window since the previous cycle's
posting_time(read from results.jsonl) - Identify the current diagnostic quadrant (see Work Loop)
- Check experiment framework: what experiment is next in the queue? Output experiment context with the morning report.
- Generate and output the morning report
Morning report format (output every cycle start):
📊 [YYYY-MM-DD] Score: +N subscribers (since last post) | Views: Xk avg (last 3 posts) | Quadrant: [HIGH/LOW views × HIGH/LOW subs]
🧪 Experiment: [exp-NNN] Testing [variable]=[value] | Batch progress: N/5
🎯 This cycle: [one specific action — e.g. "Post reel using shock_stat hook per exp-001" or "Run research cycle — score dropped 2 consecutive posts"]
⚠️ [flag or "No flags"]
Baseline metrics (first cycle only):
🚀 [YYYY-MM-DD] Baseline recorded | MRR: $X | Followers: N | IG Graph API: ✓ | RevenueCat: ✓
⚠️ [flag or "No flags"]
Operating Principles
- One variable per experiment. Test hook OR imagery OR CTA — never two at once. You cannot attribute results if multiple variables change simultaneously. See
references/experiment-framework.mdfor the current experiment queue. - Generate 3-5, post 1. Every cycle, generate 3-5 content concepts (varying the current test variable). Score each against the virality model's 5-question gate. Delegate ONLY the highest-scoring concept to viral-producer. Persist ONLY that single winning concept to
references/results.jsonlas the cycle's main entry. Log all non-winning variations underreasoning.discarded_variationsinside the winner's entry — the losers are still data, but they must never become separate cycle entries. - Use trends, not single posts. Log every post. React to direction after 2 consecutive posts point the same way. One post is a data point — two is a signal — three is a trend.
- Reach before conversion. Test hooks first, then imagery, then CTA — this order is prescribed. If nobody sees the post, CTA quality is irrelevant.
- Production ladder (replaces locked format). Format tier is selected by account phase from
../viral-research/references/format-taxonomy.md:- Phase 1 (0-1K followers): T1 + T2 + P3
- Phase 2 (1K-10K): T3 + T4 + P3
- Phase 3 (10K+): all tiers P3 Cultural Edutainment (frosted glass + dictionary-style text on cinematic footage) is the recommended starting format — proven viral in niche (Calm: 9.7M views), low production cost, gap in sobriety space.
- Three experiment variables within the chosen tier. The agent tests exactly three things: (1) hook text/framing, (2) imagery style/footage, (3) CTA text/type. Format tier is NOT a variable during a phase — it's selected by the production ladder. See
references/experiment-framework.mdfor the active experiment queue and controls. - Platform-native first. Content that looks like genuine value gets algorithmic reach. Content that looks like an ad gets buried. Follow the niche — match the register, tone, and format style of what's already resonating there.
- Simplicity criterion. If a simpler approach performs equally, prefer it. A great hook with simple visuals outperforms polished production with a weak hook. Less production effort = more cycles = faster learning.
- Trust is the only real asset here. Claims made in content are a promise to the audience. Accuracy matters. When uncertain, qualify ("for most people", "research suggests"). Never sensationalize or overstate.
- Shadow ban detection. If post views drop >60% for 2 consecutive posts with no content change → suspect Instagram suppression. Pause posting immediately. Surface to human with evidence.
- Self-healing accumulation. Every cycle builds on the previous one. When results decline, the agent uses prior data to course-correct — not reset. The playbook and virality model grow richer over time. But no single past result constrains future decisions — the agent follows current evidence, not historical momentum. Two consecutive signals in the same direction warrant action. One data point is noise.
- Niche lock. All content targets the niche defined in
config.json → app.niche. Adjacent niche research is for discovering hooks, angles, and formats that might resonate — but the audience is always the niche audience. If the agent finds itself creating content that a different niche's audience would engage with but the target niche wouldn't, discard it. - All Reels MUST contain motion. No pure static images published as Reels. Minimum motion by tier: T2 Quote Card requires text animation (fade-in, typewriter, or staggered reveal) + slow background motion (Ken Burns, parallax, or particle effect), duration 5-7s. T1/T3/T4/P1-P3 already meet motion requirements by design. Enforce this in the production spec — if delegating a T2 to viral-producer, the spec must describe the animation sequence.
- Self-heal before escalating. When something breaks, try to recover once before involving the human. Match the recovery to the failure type: transient errors (timeouts, rate limits) warrant a retry; blocked resources (auth walls, missing files) warrant a fallback or graceful skip; ambiguous failures warrant stopping and surfacing. Always record what failed and what was attempted in
reasoning.skipped_steps. Never silently skip a step — an unrecorded skip is worse than a flagged failure. - Stale data recovery before new content production. Before producing any new content, clear stale analytics debt from prior cycles. If
references/results.jsonlcontainspendingentries older than 48 hours, Step 1 must try to recover them first by matching each stale entry to a published Instagram post via caption/timestamp, pulling Instagram Graph API insights, and updating the entry before the agent creates anything new. If no matching published post can be found after 48 hours, set that entry's status tostale, record the attempted matching logic inreasoning.skipped_steps, and continue only after all >48h pending entries have been resolved to metrics orstale. - Recovery rule (deterministic). On any failed step, follow this sequence exactly: (1) classify the failure as transient, blocked, or ambiguous; (2) attempt one self-healing action only — retry once for transient failures, use the documented fallback for blocked failures, or stop for ambiguous failures; (3) if the retry/fallback succeeds, continue the cycle and log the recovery in
reasoning.skipped_steps; (4) if it fails again or no valid fallback exists, stop the cycle, preserve the current state, and surface the blocker to the human with the failed step, attempted recovery, and what must change to resume. Do not loop indefinitely, invent new recovery paths, or continue after an unresolved failed dependency.
Work Loop
Before starting any cycle, read references/experiment-framework.md to find the current experiment. The experiment framework prescribes which variable to test and which values to hold constant. The diagnostic matrix (below) still drives recovery when things break, but during normal operation, the experiment queue is the primary driver of content decisions.
Every cycle, run these steps:
Step 1: Analytics pull (scores the post published at the end of the previous cycle)
# Load env vars
export $(grep -v '^#' /home/node/openclaw/.env | xargs)
# Find the most recently published post (match by timestamp/caption to the pending entry in results.jsonl)
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/media?fields=id,caption,media_type,timestamp,like_count,comments_count&limit=5&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
# Pull insights for the matched media ID (carousel)
curl -s "https://graph.facebook.com/v22.0/<media-id>/insights?metric=reach,saved,likes,comments,shares,total_interactions&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
# Pull insights for the matched media ID (reel — includes views)
curl -s "https://graph.facebook.com/v22.0/<media-id>/insights?metric=reach,saved,likes,comments,shares,total_interactions,views&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
# Account-level: profile views
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/insights?metric=profile_views&metric_type=total_value&period=day&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
# RevenueCat metrics
curl -s -H "Authorization: Bearer $REVENUECAT_API_KEY" \
"https://api.revenuecat.com/v2/projects/${REVENUECAT_PROJECT_ID}/metrics/overview"
- First, scan
references/results.jsonlfor anypendingentries older than 48 hours. Treat these as stale-recovery candidates and process them before the normal previous-cycle analytics pull. - For each stale-recovery candidate, match it to a published Instagram post by caption/timestamp, store the Instagram media ID, pull insights via the Instagram Graph API, and update the entry with recovered metrics.
- If a stale-recovery candidate has no matching published post after 48 hours, set its status to
staleand record the failed matching attempt inreasoning.skipped_steps. - After stale recovery is complete, match the most recent unresolved
pendingentry in results.jsonl to the published post by caption/timestamp and store the Instagram media ID. - Record insights by updating the matching
pendingentry inreferences/results.jsonl→ set status tokeep,discard, orfail(orstalewhen no match exists after 48 hours) - Compare to baseline and previous batch
- Apply diagnostic matrix (below) → identify current quadrant
- Output morning report
Step 2: Content direction + generation + production
The diagnostic quadrant from Step 1 directs this step. Do not start from scratch — the quadrant prescribes a specific action (see Diagnostic Matrix). Follow that action as the constraint for content creation this cycle.
Step 2a: Content direction (from existing research)
Select a content angle using existing research output — do NOT run interactive research.
- Read
../viral-research/references/example-brief.md— the current research brief with recommended angles per tier - Read
../viral-research/references/format-taxonomy.md— tier definitions and production ladder - Cross-reference with
references/results.jsonl— which angles and tiers have already been used? - Select a format tier appropriate to the current account phase (see Operating Principles → Production ladder)
- Select a content angle from the brief that hasn't been exhausted yet
Fresh research flag (HIGH bar): Only flag when ALL of these are true:
- Every tier×angle combination from the current brief has been used at least once
- 10-15+ cycles have passed since the brief was produced
- Flag is NON-BLOCKING — include it in the morning report with specific suggestions for what to research next. Do not stop the loop.
Step 2b: Virality gate + batch concept generation (stays in viral-ig-marketer)
- Read experiment framework — check
references/experiment-framework.mdfor the current experiment. This tells you which variable to test and what to hold constant. - Generate 3-5 content concepts (varying the current test variable within the chosen tier):
- If testing hooks: write 3-5 different hook texts/framings, keeping imagery style + CTA constant
- If testing imagery: describe 3-5 different visual approaches/footage styles, keeping hook + CTA constant
- If testing CTA: write 3-5 different CTA texts/types, keeping hook + imagery constant
- Score each concept against the virality model's 5-question gate in
references/virality-model.md. Each yes = 1 point, max 5.- Score 4–5: proceed
- Score 3: revise the hook or specificity, then re-score
- Score 0–2: discard — pick a different angle from the brief
- Pick the winner — highest score. Break ties with what's most unexpected or specific to the audience.
- Enforce the single-output rule — once a winner is selected, that winner is the ONLY concept allowed to move forward in this cycle. Do not render runners-up. Do not append runners-up as separate rows in
references/results.jsonl. If multiple rows share the sameid/ cycle ID, treat that as a bug and stop to correct it before continuing. - Write the caption for the winning concept. Follow
references/caption-guide.md(full framework) +references/soul.md(brand voice). - Log the discards — store all variations in results.jsonl under
reasoning.discarded_variationsas[{text, score, reason_not_picked}].
Step 2c: Production delegation (to viral-producer)
Delegate the winning concept to the viral-producer skill. The production spec is the contract — viral-producer validates it and produces exactly what it describes. No creative decisions happen downstream.
How to invoke: Read ../viral-producer/SKILL.md and follow its workflow, passing a production spec as input. The producer skill is an executor — spec in, content out.
Production spec: Build the spec per references/production-spec.md (required + optional fields, example). All required fields must be populated from Steps 2a and 2b before delegating. Pick an audio_track that matches the content's emotional tone from the read-only audio/ library and include that filename in the spec.
Viral-producer handles: asset generation, Remotion rendering, caption file save, output packaging. It returns a complete package: .mp4 + caption.txt + metadata.json.
Render constraint: render exactly 1 video per cycle — the winning concept only. Internal concept generation can produce multiple candidates, but production output must be a single .mp4 package for the selected winner.
After receiving the rendered output, send draft to human via Telegram:
openclaw message send --channel telegram --target $TELEGRAM_CHAT_ID \
--media <path-to-reel>.mp4 --message "🎬 Reel draft — <topic> [<tier>]"
Follow with caption:
openclaw message send --channel telegram --target $TELEGRAM_CHAT_ID \
--message "📋 Caption:\n\n<full caption text with hashtags>"
fal.ai budget: Hard stop at the ceiling in references/config.json → fal.budgetCeilingUSD. Track spend per cycle in results.jsonl reasoning.
Step 3: Append cycle to results.jsonl
Immediately after drafting content, append a new entry.
Persistence constraint: append exactly 1 new row to references/results.jsonl for the cycle winner. Never append one row per variation. Multiple entries with the same cycle ID mean the cycle is corrupted and must be treated as a bug.
{
"id": "cycle-NNN",
"date": "YYYY-MM-DD",
"type": "post",
"format": "T1|T2|T3|T4|P1|P2|P3",
"format_tier_label": "e.g. P3 Cultural Edutainment",
"topic": "...",
"hook_style": "shock_stat|question|curiosity_gap|contrarian|personal_story",
"hook_text": "exact hook text or framing used",
"content_summary": "brief description of the reel's content structure",
"imagery_style": "cinematic-footage|ai-generated|stock|timelapse|gradient|mixed",
"cta": "exact CTA text used",
"posting_time": "HH:MM",
"hashtag_set": "cluster-a|cluster-b|broad",
"views": null,
"saves": null,
"profile_visits": null,
"new_subscribers_since_post": null,
"score_delta": null,
"status": "pending",
"phase": "growth",
"experiment_id": "exp-NNN or null if not part of formal experiment batch",
"reasoning": {
"why_topic": "what research signal led to this topic choice",
"virality_score": 0,
"virality_notes": "which of the 5 questions passed/failed and why",
"vs_baseline": "how this cycle's outcome compares to account baseline (populated after analytics pull)",
"skipped_steps": "any step skipped + why (error, fallback, or intentional)",
"outcome_hypothesis": "what you expect to learn from this post and what result would confirm or refute it",
"experiment_variable": "which variable is being tested (null if not part of experiment)",
"experiment_control": "what was held constant (null if not part of experiment)",
"discarded_variations": [
{
"text": "variation text",
"score": 3,
"reason_not_picked": "why it lost"
}
]
}
}
Status starts as "pending". Updated to "keep", "discard", or "fail" when the next cycle pulls analytics.
Stale cleanup: At every cycle start, scan results.jsonl for entries where status === "pending" AND date < today - 5 days. Update those entries to status: "stale" with notes: "No analytics data after 5 days — check if post was published".
Archive rule: When results.jsonl exceeds 500 lines, copy to references/results-archive-YYYY-MM-DD.jsonl and reset results.jsonl.
Step 4: Reflect + Update
Runs every cycle after Step 3. Uses whatever newly scored entries exist — no minimum threshold.
- Compute running averages across all scored entries in results.jsonl: avg views, avg save rate, avg profile visit rate, total attributed subscribers
- Update
references/virality-model.mdperformance baseline table with new computed averages - Update
references/playbook.json:- Promote winning hooks →
winningHooksarray - Retire underperforming topics →
droppedTopicsarray - Update
activeHashtagClusterif a different cluster showed better reach - Update
activeFormatmix percentages - Record best posting time if time experiments have run
- Promote winning hooks →
- Update
references/virality-model.mdEvidence Log:- Look at the 2 highest-scoring posts in results.jsonl. What hook, format, or angle did they share? Append to Evidence Log.
- Look at the 2 lowest-scoring posts. What was weak? Append to Evidence Log.
- If evidence contradicts any current hypothesis in the model, update the hypothesis.
- Adjust the virality score threshold if it's consistently too loose or too tight.
- If unsure what's driving results, run a web search:
"Instagram algorithm [niche] [year] what content goes viral"and incorporate findings. - Swipe-file feedback: If a scored post reveals a new pattern (hook type, visual style, or format variation that over/under-performed), append a finding to
../viral-research/references/swipe-file.jsonlso future research briefs reflect real performance data.
- Conversion correlation check (every cycle):
- Cross-reference topics and hooks from scored entries with
new_subscribers_since_postvalues in results.jsonl - Identify which content variables (topic, hook style, format, CTA) correlate with actual subscriber conversions — not just views
- Update
references/playbook.json→ promote hooks/topics that drove subscribers, not just engagement - If views are high but subscriber growth is flat for 2+ consecutive cycles, flag the disconnect and prioritize CTA/conversion experiments next cycle
- Cross-reference topics and hooks from scored entries with
- App intelligence check — only run if current quadrant is "High views + flat MRR". Skip entirely otherwise.
- Read the GitHub repo via gitingest CLI (use cached
references/app-brief.mdif recently read; refresh monthly or when codebase changes):gitingest <app.githubRepo> -o - - Browse the App Store listing via agent-browser (
app.appStoreUrlin config.json) - Cross-reference: winning Instagram angles vs app description copy, screenshots, and onboarding flow
- If anything concrete found (copy misalignment, onboarding signal, App Store gap, UX signal, retention hypothesis) → append to
references/app-feedback.mdusing the format defined in that file
- Read the GitHub repo via gitingest CLI (use cached
- Update experiment framework: Score the current experiment in
references/experiment-framework.mdwith actual metrics. If this completes a batch of 5, run the batch analysis template: rank results, pick the winner, lock it as a control variable, and rebuild the queue for the next variable. - Output cycle summary: this cycle's score vs baseline, what changed in playbook, what changed in virality model, experiment result, next experiment
- Increment
playbook.json → cycleCountby 1
Step 5 (Human): Publish
Human receives reel .mp4 + caption via Telegram → publishes from Instagram app. This closes the loop — the post published here becomes the input to Step 1 of the next cycle. The agent discovers the published post's media ID automatically via GET /{ig-user-id}/media at the start of the next cycle.
Research direction (every cycle):
Use existing research output from ../viral-research/references/example-brief.md and ../viral-research/references/swipe-file.jsonl. ⛔ Do NOT browse instagram.com. Adjacent niche web searches are for hook/angle inspiration only — all content targets the niche defined in config.json → app.niche.
Diagnostic Matrix
Use this AFTER scoring each cycle. The quadrant tells you what's broken and exactly what to do next.
How to read it: "High views" = above your current baseline in virality-model.md (or >500 views if no baseline yet). "High subs" = any new subscriber attributed to the post window.
| Quadrant | Signal | What's broken | Exact next action |
|---|---|---|---|
| Q1: High views + High subs | 🟢 Working | Nothing — scale it | Generate 3-5 variations of the SAME hook type. Keep imagery + CTA constant. Post the best one. You found signal — exploit it before moving on. |
| Q2: High views + Low subs | 🟡 Hook works, funnel broken | CTA or app landing | 1. Skip ahead to CTA testing — generate 3-5 CTA variations with the current hook. 2. Check App Store listing (does it match what the reel promises?). 3. If CTA rotation doesn't fix it after 3 posts, escalate to Henry — problem is likely in-app (onboarding, paywall, pricing). |
| Q3: Low views + High subs | 🟡 Converts but invisible | Hook isn't stopping the scroll | Stay on hook testing. The content converts — the hook just isn't reaching people. Try a radically different hook type (if testing "question" hooks, try "shock stat" or "contrarian"). Don't touch imagery or CTA — they're working. |
| Q4: Low views + Low subs | 🔴 Nothing working | Topic or angle is off | 1. Re-read ../viral-research/references/example-brief.md for unused angles. 2. Pull 3 new topic angles from the brief. 3. Generate 3-5 hooks for each angle, score against virality model, pick the best. 4. If 3 consecutive Q4 results → pause and escalate to Henry. |
| High views + High installs + flat MRR | 🔴 App problem | Content is fine, app isn't converting trials to paid | STOP posting. Escalate to Henry immediately. The problem is onboarding, paywall, trial length, or pricing — not content. Posting more just burns audience goodwill. |
Key rule: The diagnostic matrix overrides the experiment queue. If you're in Q1, don't move to the next experiment — double down on what's working. If you're in Q2, jump to CTA testing regardless of where the queue says you should be. The queue is the default path; the matrix is the override.
Stall Rule
- Views < 300/post for 5 consecutive posts → try a radically different hook style before escalating
- Zero subscriber growth for 4 consecutive weeks → pause posting, surface to human (suspect account suppression, niche saturation, or app funnel issue)
- MRR flat despite growing subscriber count → escalate immediately — churn or retention problem in the app
On End
At the close of every cycle, before exiting:
- Improvement notes — reflect on today's work. If there is anything genuine to record (ambiguous instruction, missing tool, improvised step not covered by the skill, soul.md misalignment) → append to
references/improvement-notes.mdusing the format defined in that file. If nothing to report, skip. Do NOT pad. - Do NOT modify
SKILL.mdorsoul.md. If an urgent change is needed, write a note in improvement-notes.md and flag the human.
Memory
All paths are relative to the skill's own directory (wherever this SKILL.md lives):
- Results log (append-only):
references/results.jsonl - Best-known playbook:
references/playbook.json - Config + API keys:
references/config.json - Virality algorithm (agent-owned, living doc):
references/virality-model.md - Brand voice (read-only):
references/soul.md - App codebase brief (cached, agent-refreshed):
references/app-brief.md - App intelligence feedback (append-only):
references/app-feedback.md - Session improvement notes (append-only):
references/improvement-notes.md - Caption guide:
references/caption-guide.md - Experiment framework (queue + controls):
references/experiment-framework.md - Production spec contract:
references/production-spec.md - Baseline study:
references/baseline-study.md - Audio library (read-only; files may exist only on VPS, still valid spec values):
audio/calm-reflective.mp3— calm, reflective, groundingcontemplative-deep.mp3— deep, serious, introspectivedetermined-empowered.mp3— confident, driven, resilientgentle-morning.mp3— soft, warm, hopeful resetuplifting-hopeful.mp3— optimistic, encouraging, forward-looking
Sibling skill references (read from, write feedback to):
- Research brief:
../viral-research/references/example-brief.md - Swipe file (read + append feedback):
../viral-research/references/swipe-file.jsonl - Format taxonomy:
../viral-research/references/format-taxonomy.md - Production config (written on Cycle 0):
../viral-producer/references/production-config.json - Production skill:
../viral-producer/SKILL.md
Every cycle reads in this order: results.jsonl (full) → playbook.json → experiment-framework.md → virality-model.md → then pull live analytics.
JSONL schema: See Step 3 for the current schema. Single source of truth — do not duplicate here.
Safety
- Hard stops: no ad purchases, no account engagement, no content claiming medical outcomes
- Budget ceiling: fal.ai $10/month — if limit hit, fall back to solid or gradient backgrounds
- Rate limits: Instagram Graph API and RevenueCat API — retry with exponential backoff (1s, 2s, 4s), max 3 retries
- Escalation triggers:
- Views drop >60% for 2 consecutive posts → suspect shadow ban → pause posting → alert human with evidence
- 4 consecutive weeks zero subscriber growth → stall rule → pause → surface full diagnosis
- RevenueCat shows high churn alongside new installs → flag app retention issue to human
- Privacy: never log individual subscriber emails, names, or identifiers from RevenueCat. Aggregate metrics only.
Closed Loop Test
- Observe: Instagram Graph API insights (reach, saves, views, profile visits) + RevenueCat (new subscribers, MRR)
- Act: generate reel via viral-producer (format tier per production ladder); send draft via Telegram.
- Verify: analytics vs baseline every cycle via diagnostic matrix
- Record:
references/results.jsonl— append-only JSONL, read in full at every cycle start, stale entries cleaned automatically - Continue: diagnostic matrix drives next action autonomously; human publishes from Instagram app
Proof of Loop
Every cycle runs the same steps in the same order. The loop is closed by the human publish at the end — the data from that post becomes the input to Step 1 of the next cycle.
CYCLE START (agent)
1. Read memory: results.jsonl → playbook → experiment-framework → virality-model
2. Discover published media ID via Instagram Graph API → pull insights for previous cycle's post
3. Score it → update that results.jsonl entry → identify quadrant → output morning report
4a. Select content direction from existing research brief (../viral-research/)
4b. Virality gate + batch concept generation (3-5 concepts, pick winner)
4c. Delegate winning concept to viral-producer → receive .mp4 → send draft via Telegram
5. Append pending entry to results.jsonl
6. Reflect + Update: recompute baseline, update playbook + virality model, feedback to swipe-file.jsonl
CYCLE END (human)
7. Human publishes from Instagram app ← loop closes here
The pending entry appended in step 5 becomes the target of step 2 in the next cycle.
Baseline recording (runs once on Cycle 1):* If playbook.json → cycleCount == 0, the agent records the baseline state (MRR, follower count), confirms connections, Content generation proceeds normally. Once cycleCount is incremented to 1, this path never runs again.
Proof by Cycle 10: results.jsonl has 10 scored entries, playbook reflects real hook and format data, virality model evidence log has entries — every decision is driven by evidence, not bootstrap assumptions.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
verification-gate
Reusable verification gate for completed work before commit or merge. Use when implementation is done and Claude must prove the task works, verify the main user flow, choose between browser-flow, browser-static, or non-browser validation, and return a PASS/FAIL/BLOCKED verdict with evidence. When browser verification is needed, rely on the agent-browser skill for browser actions, screenshots, and recordings.
osgrep
Semantic code search using natural language queries. Use when users ask "where is X implemented", "how does Y work", "find the logic for Z", or need to locate code by concept rather than exact text. Returns file paths with line numbers and code snippets.
napkin
Maintain a per-repo napkin file that tracks mistakes, corrections, and what works. Activates EVERY session, unconditionally. Read the napkin before doing anything. Write to it continuously as you work — not just at session boundaries. Log your own mistakes, not just user corrections. The napkin lives in the repo at `.opencode/napkin.md`.
dogfood
Systematically explore and test a web application to find bugs, UX issues, and other problems. Use when asked to "dogfood", "QA", "exploratory test", "find issues", "bug hunt", "test this app/site/platform", or review the quality of a web application. Produces a structured report with full reproduction evidence -- step-by-step screenshots, repro videos, and detailed repro steps for every issue -- so findings can be handed directly to the responsible teams.
tmux
Manage concurrent background processes using tmux. Use when spawning dev servers, running long-running tasks, monitoring multiple processes, or capturing output from background commands without blocking the main session.
viral-research
Viral content research and DNA extraction for Instagram Reels. Use this skill whenever you need to research what's performing in a niche on Instagram, find viral faceless Reels, download and analyze them frame-by-frame, identify patterns, and produce a structured research brief. Trigger whenever the user mentions viral content research, Instagram research, Reel analysis, content DNA, swipe file building, niche research, competitor analysis for social content, or wants to understand why certain content performs. Also trigger when the user wants to find content formats to adapt, study what's working in a niche, or build a research-backed content strategy. This skill is research-only — it does not generate or publish content.
Didn't find tool you were looking for?