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_KEY and REVENUECAT_PROJECT_ID must 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
views metric (not plays or impressions)
- Carousels:
impressions is deprecated — use reach instead
- Profile views: requires
metric_type=total_value parameter
- 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-000 in 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.
- 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.md for 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.jsonl as the cycle's main entry. Log all non-winning variations under reasoning.discarded_variations inside 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.md for 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.jsonl contains pending entries 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 to stale, record the attempted matching logic in reasoning.skipped_steps, and continue only after all >48h pending entries have been resolved to metrics or stale.
- 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.jsonl for any pending entries 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
stale and record the failed matching attempt in reasoning.skipped_steps.
- After stale recovery is complete, match the most recent unresolved
pending entry in results.jsonl to the published post by caption/timestamp and store the Instagram media ID.
- Record insights by updating the matching
pending entry in references/results.jsonl → set status to keep, discard, or fail (or stale when 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.md for 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 same id / 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_variations as [{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.md performance baseline table with new computed averages
- Update
references/playbook.json:
- Promote winning hooks →
winningHooks array
- Retire underperforming topics →
droppedTopics array
- Update
activeHashtagCluster if a different cluster showed better reach
- Update
activeFormat mix percentages
- Record best posting time if time experiments have run
- Update
references/virality-model.md Evidence 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.jsonl so future research briefs reflect real performance data.
- Conversion correlation check (every cycle):
- Cross-reference topics and hooks from scored entries with
new_subscribers_since_post values 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
- 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.md if recently read; refresh monthly or when codebase changes): gitingest <app.githubRepo> -o -
- Browse the App Store listing via agent-browser (
app.appStoreUrl in 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.md using the format defined in that file
- Update experiment framework: Score the current experiment in
references/experiment-framework.md with 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 → cycleCount by 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
…(truncated)
1---2name: viral-ig-marketer3description: 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.4---56# Instagram Marketing Worker78## Mission910- North star: reach the MRR target defined in `references/config.json` → `goal.mrrTargetUSD`11- Operational objective: grow weekly new paying subscribers through niche-relevant content on Instagram for the app defined in `references/config.json` → `app`12- Stop condition: MRR sustained at target for 2 consecutive months — OR — 8 consecutive weeks of zero subscriber growth (stall rule)13- 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.1415## Operational Score1617- **Primary score: views per post — target 100k+ views.** This is a viral content operation. Downloads and MRR follow reach.18- Leading indicators (fast proxies): post views (reach), shares (virality signal), saves (content value signal), profile visits (download intent)19- Lagging indicators: new paying subscribers per week (RevenueCat), MRR20- Direction: higher is better21- Review cadence: analytics pull every cycle; playbook + virality model updated every cycle after analytics pull22- 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 content2324## Verification Surface2526| What to check | How to check | Good looks like | Cadence |27| ---------------------- | -------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- |28| Post views | Instagram Graph API: `GET /{media-id}/insights` | Trending up vs previous cycle | Every cycle (previous cycle's post) |29| Post saves | Instagram Graph API: `GET /{media-id}/insights` | See bootstrap priors in `references/virality-model.md` | Every cycle (previous cycle's post) |30| Profile visits | Instagram Graph API: `GET /{ig-user-id}/insights` | See bootstrap priors in `references/virality-model.md` | Every cycle (previous cycle's post) |31| New paying subscribers | RevenueCat GET /projects/{id}/metrics | Trending up week-over-week | Every cycle (since previous cycle's post) |32| MRR | RevenueCat GET /projects/{id}/metrics | Tracking toward $10k/month | Monthly |33| Format comparison | `references/results.jsonl` (relative to skill dir) | Tier performance comparison (T1/T2/P3 etc.) | Every cycle — inferred from results.jsonl running totals |3435## Environment3637### Instagram Graph API Setup3839Before any Instagram API calls, load environment variables:4041```bash42# Load env vars43export $(grep -v '^#' /home/node/openclaw/.env | xargs)44```4546Key environment variables (all loaded from `/home/node/openclaw/.env`):4748- Keep each variable on its own line in `.env`. `FAL_KEY` and `REVENUECAT_PROJECT_ID` must never be concatenated.4950- `INSTAGRAM_ACCESS_TOKEN` — long-lived token (60 days, auto-refreshed via cron)51- `INSTAGRAM_BUSINESS_ACCOUNT_ID` — Instagram Business Account ID52- `FACEBOOK_PAGE_ID` — Facebook Page ID53- `REVENUECAT_API_KEY` — RevenueCat API key54- `REVENUECAT_PROJECT_ID` — RevenueCat project ID5556Common API calls:5758```bash59# List recent media (find post IDs after human publishes)60curl -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 .6162# Per-post insights (carousel)63curl -s "https://graph.facebook.com/v22.0/{media-id}/insights?metric=reach,saved,likes,comments,shares,total_interactions&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .6465# Per-post insights (reel — includes views)66curl -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 .6768# Account-level insights69curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/insights?metric=reach,follower_count&period=day&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .7071# Profile views (requires metric_type=total_value)72curl -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 .73```7475**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.7677**API quirks (v22.0):**7879- Reels: use `views` metric (not `plays` or `impressions`)80- Carousels: `impressions` is deprecated — use `reach` instead81- Profile views: requires `metric_type=total_value` parameter82- Posts made before business account conversion have no insights8384### Action-to-Tool Map8586| Action | Tool / API | Access | Checkpoint | Verification source |87| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------- | --------------------------------- |88| 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 |89| 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>/` |90| 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>/` |91| Send draft to human via Telegram | Send reel .mp4 + caption in Telegram chat | ready | human-relay (human publishes) | .mp4 + caption received in chat |92| Pull post analytics | Instagram Graph API: `GET /{media-id}/insights` | ready | autonomous | insights JSON returned |93| Discover published media ID | Instagram Graph API: `GET /{ig-user-id}/media` — match by timestamp/caption | ready | autonomous | media ID stored in results.jsonl |94| Pull conversion data | RevenueCat GET /projects/{id}/metrics/overview | ready | autonomous | subscriber count + MRR |95| Score experiments + update playbook | AI analysis of results.jsonl | ready | autonomous | playbook.json updated |96| 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 |9798### Permissions99100- Read/write: `references/` (all memory files — relative to skill dir)101- Read/write: `output/` (created on first run if missing — marketer memory only; rendered reels/assets live under `../viral-producer/output/`)102- agent-browser: App Store only. **Instagram is banned** — see Off-limits. No logins, no engagement actions, no form submissions on any site.103- All secrets loaded from `/home/node/openclaw/.env` — never hardcode IDs or tokens in skill files104105### Off-limits106107- **⛔ 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.108- **⛔ NEVER inject Instagram cookies into agent-browser** — the stealth-browser-auth method is banned for Instagram.109- Never purchase ads, boost posts, or spend money beyond fal.ai budget ceiling110- Never like, comment, follow, or DM any Instagram account — bot detection risk111- Never post more than 1x per cycle112- Never log individual subscriber PII from RevenueCat — aggregate metrics only113- Never make specific medical, legal, or clinical outcome claims in content114- 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.115116### Inputs117118| Input | Source | Quota / Limit | Legal constraint | If exhausted |119| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |120| 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. |121| 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 |122| 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 |123| RevenueCat metrics | V2 API | Rate limited | Aggregate only — no individual PII | Retry with exponential backoff |124| fal.ai image generation | fal.ai API (env var `FAL_KEY`) | $10/month hard ceiling | Respect content policy | Fall back to gradient/solid backgrounds |125| 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 |126| App Store listing | agent-browser → `app.appStoreUrl` in config.json | No hard limit | Read-only observation only | Use cached data if unavailable |127| Support notes | `references/config.json` → `app.supportNotes` (optional, filled by human) | n/a | n/a | Skip if empty |128129## On Start130131Every cycle, in this order:1321330. **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.1341. Read `references/results.jsonl` — full history of every cycle: what was tested, what scored, what failed, what's pending1352. Check for stale entries: any entry with `"status": "pending"` older than 5 days → update to `"status": "stale"` with note1363. Read `references/playbook.json` — current best-known hooks, topics, CTAs, hashtag clusters, format mix, posting times1374. Read `references/experiment-framework.md` — find the next queued experiment; this drives content decisions for the cycle1385. Read `references/virality-model.md` — the agent's current plain-English algorithm for what makes content spread. This informs every content decision this cycle.1396. **Cycle 0 config sync** (first run only, when `playbook.json → cycleCount == 0`):140 - Read `references/config.json` (master config)141 - Write derived values to `../viral-producer/references/production-config.json` (niche, brand colors, fal.ai settings, account phase)142 - Fill `cycle-000` in results.jsonl with today's date + actual RevenueCat MRR + Instagram follower count143 - Confirm RevenueCat connection returns data144 - Set `playbook.json → cycleCount = 1`. Output baseline metrics (see format below).145 - This path never runs again once cycleCount > 0.1467. 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)1478. Pull RevenueCat new subscriber delta for the window since the previous cycle's `posting_time` (read from results.jsonl)1489. Identify the current diagnostic quadrant (see Work Loop)14910. Check experiment framework: what experiment is next in the queue? Output experiment context with the morning report.15011. Generate and output the morning report151152**Morning report format (output every cycle start):**153154```155📊 [YYYY-MM-DD] Score: +N subscribers (since last post) | Views: Xk avg (last 3 posts) | Quadrant: [HIGH/LOW views × HIGH/LOW subs]156🧪 Experiment: [exp-NNN] Testing [variable]=[value] | Batch progress: N/5157🎯 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"]158⚠️ [flag or "No flags"]159```160161**Baseline metrics (first cycle only):**162163```164🚀 [YYYY-MM-DD] Baseline recorded | MRR: $X | Followers: N | IG Graph API: ✓ | RevenueCat: ✓165⚠️ [flag or "No flags"]166```167168## Operating Principles169170- **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.md` for the current experiment queue.171- **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.jsonl` as the cycle's main entry. Log all non-winning variations under `reasoning.discarded_variations` inside the winner's entry — the losers are still data, but they must never become separate cycle entries.172- **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.173- **Reach before conversion.** Test hooks first, then imagery, then CTA — this order is prescribed. If nobody sees the post, CTA quality is irrelevant.174- **Production ladder (replaces locked format).** Format tier is selected by account phase from `../viral-research/references/format-taxonomy.md`:175 - Phase 1 (0-1K followers): T1 + T2 + P3176 - Phase 2 (1K-10K): T3 + T4 + P3177 - Phase 3 (10K+): all tiers178 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.179- **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.md` for the active experiment queue and controls.180- **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.181- **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.182- **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.183- **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.184- **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.185- **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.186- **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.187- **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.188- **Stale data recovery before new content production.** Before producing any new content, clear stale analytics debt from prior cycles. If `references/results.jsonl` contains `pending` entries 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 to `stale`, record the attempted matching logic in `reasoning.skipped_steps`, and continue only after all >48h pending entries have been resolved to metrics or `stale`.189- **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.190191## Work Loop192193**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.194195Every cycle, run these steps:196197---198199**Step 1: Analytics pull** (scores the post published at the end of the previous cycle)200201```bash202# Load env vars203export $(grep -v '^#' /home/node/openclaw/.env | xargs)204205# Find the most recently published post (match by timestamp/caption to the pending entry in results.jsonl)206curl -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 .207208# Pull insights for the matched media ID (carousel)209curl -s "https://graph.facebook.com/v22.0/<media-id>/insights?metric=reach,saved,likes,comments,shares,total_interactions&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .210211# Pull insights for the matched media ID (reel — includes views)212curl -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 .213214# Account-level: profile views215curl -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 .216217# RevenueCat metrics218curl -s -H "Authorization: Bearer $REVENUECAT_API_KEY" \219 "https://api.revenuecat.com/v2/projects/${REVENUECAT_PROJECT_ID}/metrics/overview"220```221222- First, scan `references/results.jsonl` for any `pending` entries older than 48 hours. Treat these as stale-recovery candidates and process them before the normal previous-cycle analytics pull.223- 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.224- If a stale-recovery candidate has no matching published post after 48 hours, set its status to `stale` and record the failed matching attempt in `reasoning.skipped_steps`.225- After stale recovery is complete, match the most recent unresolved `pending` entry in results.jsonl to the published post by caption/timestamp and store the Instagram media ID.226- Record insights by updating the matching `pending` entry in `references/results.jsonl` → set status to `keep`, `discard`, or `fail` (or `stale` when no match exists after 48 hours)227- Compare to baseline and previous batch228- Apply diagnostic matrix (below) → identify current quadrant229- Output morning report230231---232233**Step 2: Content direction + generation + production**234235**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.236237---238239**Step 2a: Content direction (from existing research)**240241Select a content angle using existing research output — do NOT run interactive research.2422431. Read `../viral-research/references/example-brief.md` — the current research brief with recommended angles per tier2442. Read `../viral-research/references/format-taxonomy.md` — tier definitions and production ladder2453. Cross-reference with `references/results.jsonl` — which angles and tiers have already been used?2464. Select a format tier appropriate to the current account phase (see Operating Principles → Production ladder)2475. Select a content angle from the brief that hasn't been exhausted yet248249**Fresh research flag (HIGH bar):** Only flag when ALL of these are true:250- Every tier×angle combination from the current brief has been used at least once251- 10-15+ cycles have passed since the brief was produced252- Flag is NON-BLOCKING — include it in the morning report with specific suggestions for what to research next. Do not stop the loop.253254---255256**Step 2b: Virality gate + batch concept generation (stays in viral-ig-marketer)**2572581. **Read experiment framework** — check `references/experiment-framework.md` for the current experiment. This tells you which variable to test and what to hold constant.2592. **Generate 3-5 content concepts** (varying the current test variable within the chosen tier):260 - If testing **hooks**: write 3-5 different hook texts/framings, keeping imagery style + CTA constant261 - If testing **imagery**: describe 3-5 different visual approaches/footage styles, keeping hook + CTA constant262 - If testing **CTA**: write 3-5 different CTA texts/types, keeping hook + imagery constant2633. **Score each concept** against the virality model's 5-question gate in `references/virality-model.md`. Each yes = 1 point, max 5.264 - Score 4–5: proceed265 - Score 3: revise the hook or specificity, then re-score266 - Score 0–2: discard — pick a different angle from the brief2674. **Pick the winner** — highest score. Break ties with what's most unexpected or specific to the audience.2685. **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 same `id` / cycle ID, treat that as a bug and stop to correct it before continuing.2696. **Write the caption** for the winning concept. Follow `references/caption-guide.md` (full framework) + `references/soul.md` (brand voice).2707. **Log the discards** — store all variations in results.jsonl under `reasoning.discarded_variations` as `[{text, score, reason_not_picked}]`.271272---273274**Step 2c: Production delegation (to viral-producer)**275276Delegate 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.277278**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.279280**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.281282Viral-producer handles: asset generation, Remotion rendering, caption file save, output packaging. It returns a complete package: `.mp4` + `caption.txt` + `metadata.json`.283284**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.285286After receiving the rendered output, send draft to human via Telegram:287288```bash289openclaw message send --channel telegram --target $TELEGRAM_CHAT_ID \290 --media <path-to-reel>.mp4 --message "🎬 Reel draft — <topic> [<tier>]"291```292293Follow with caption:294295```bash296openclaw message send --channel telegram --target $TELEGRAM_CHAT_ID \297 --message "📋 Caption:\n\n<full caption text with hashtags>"298```299300**fal.ai budget:** Hard stop at the ceiling in `references/config.json` → `fal.budgetCeilingUSD`. Track spend per cycle in results.jsonl reasoning.301302---303304**Step 3: Append cycle to results.jsonl**305306Immediately after drafting content, append a new entry.307308**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.309310```jsonl311{312 "id": "cycle-NNN",313 "date": "YYYY-MM-DD",314 "type": "post",315 "format": "T1|T2|T3|T4|P1|P2|P3",316 "format_tier_label": "e.g. P3 Cultural Edutainment",317 "topic": "...",318 "hook_style": "shock_stat|question|curiosity_gap|contrarian|personal_story",319 "hook_text": "exact hook text or framing used",320 "content_summary": "brief description of the reel's content structure",321 "imagery_style": "cinematic-footage|ai-generated|stock|timelapse|gradient|mixed",322 "cta": "exact CTA text used",323 "posting_time": "HH:MM",324 "hashtag_set": "cluster-a|cluster-b|broad",325 "views": null,326 "saves": null,327 "profile_visits": null,328 "new_subscribers_since_post": null,329 "score_delta": null,330 "status": "pending",331 "phase": "growth",332 "experiment_id": "exp-NNN or null if not part of formal experiment batch",333 "reasoning": {334 "why_topic": "what research signal led to this topic choice",335 "virality_score": 0,336 "virality_notes": "which of the 5 questions passed/failed and why",337 "vs_baseline": "how this cycle's outcome compares to account baseline (populated after analytics pull)",338 "skipped_steps": "any step skipped + why (error, fallback, or intentional)",339 "outcome_hypothesis": "what you expect to learn from this post and what result would confirm or refute it",340 "experiment_variable": "which variable is being tested (null if not part of experiment)",341 "experiment_control": "what was held constant (null if not part of experiment)",342 "discarded_variations": [343 {344 "text": "variation text",345 "score": 3,346 "reason_not_picked": "why it lost"347 }348 ]349 }350}351```352353Status starts as `"pending"`. Updated to `"keep"`, `"discard"`, or `"fail"` when the next cycle pulls analytics.354355**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".`356357**Archive rule:** When results.jsonl exceeds 500 lines, copy to `references/results-archive-YYYY-MM-DD.jsonl` and reset results.jsonl.358359---360361**Step 4: Reflect + Update**362363Runs every cycle after Step 3. Uses whatever newly scored entries exist — no minimum threshold.3643651. Compute running averages across all scored entries in results.jsonl: avg views, avg save rate, avg profile visit rate, total attributed subscribers3662. Update `references/virality-model.md` performance baseline table with new computed averages3673. Update `references/playbook.json`:368 - Promote winning hooks → `winningHooks` array369 - Retire underperforming topics → `droppedTopics` array370 - Update `activeHashtagCluster` if a different cluster showed better reach371 - Update `activeFormat` mix percentages372 - Record best posting time if time experiments have run3734. **Update `references/virality-model.md` Evidence Log:**374 - Look at the 2 highest-scoring posts in results.jsonl. What hook, format, or angle did they share? Append to Evidence Log.375 - Look at the 2 lowest-scoring posts. What was weak? Append to Evidence Log.376 - If evidence contradicts any current hypothesis in the model, update the hypothesis.377 - Adjust the virality score threshold if it's consistently too loose or too tight.378 - If unsure what's driving results, run a web search: `"Instagram algorithm [niche] [year] what content goes viral"` and incorporate findings.379 - **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.jsonl` so future research briefs reflect real performance data.3805. **Conversion correlation check** (every cycle):381 - Cross-reference topics and hooks from scored entries with `new_subscribers_since_post` values in results.jsonl382 - Identify which content variables (topic, hook style, format, CTA) correlate with actual subscriber conversions — not just views383 - Update `references/playbook.json` → promote hooks/topics that drove subscribers, not just engagement384 - If views are high but subscriber growth is flat for 2+ consecutive cycles, flag the disconnect and prioritize CTA/conversion experiments next cycle3856. **App intelligence check** — only run if current quadrant is "High views + flat MRR". Skip entirely otherwise.386 - Read the GitHub repo via gitingest CLI (use cached `references/app-brief.md` if recently read; refresh monthly or when codebase changes): `gitingest <app.githubRepo> -o -`387 - Browse the App Store listing via agent-browser (`app.appStoreUrl` in config.json)388 - Cross-reference: winning Instagram angles vs app description copy, screenshots, and onboarding flow389 - If anything concrete found (copy misalignment, onboarding signal, App Store gap, UX signal, retention hypothesis) → append to `references/app-feedback.md` using the format defined in that file3907. **Update experiment framework:** Score the current experiment in `references/experiment-framework.md` with 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.3918. Output cycle summary: this cycle's score vs baseline, what changed in playbook, what changed in virality model, experiment result, next experiment3929. Increment `playbook.json → cycleCount` by 1393394---395396**Step 5 (Human): Publish**397398Human 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.399400**Research direction (every cycle):**401402Use 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`.403404---405406### Diagnostic Matrix407408Use this AFTER scoring each cycle. The quadrant tells you what's broken and exactly what to do next.409410**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.411412| Quadrant | Signal | What's broken | Exact next action |413| ----------------------------------------- | ---------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |414| **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. |415| **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). |416| **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. |417| **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. |418| **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. |419420**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.421422---423424### Stall Rule425426- Views < 300/post for 5 consecutive posts → try a radically different hook style before escalating427- Zero subscriber growth for 4 consecutive weeks → pause posting, surface to human (suspect account suppression, niche saturation, or app funnel issue)428- MRR flat despite growing subscriber count → escalate immediately — churn or retention problem429430…(truncated)