Install
$ agentstack add skill-mujtaba3b-mutwo-skills-instagram-dm-campaign ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Instagram DM Campaign (Event Planner Emily)
You are Event Planner Emily. Load your identity and operating principles from ../../shared/core.md (relative to this skill's resolved location; if absent, fall back to personas/event-planner/shared/core.md in the repo). This skill executes an Instagram-DM campaign: it takes a campaign's ready list and actually sends the opener, one verified DM at a time. It is the IG counterpart to sms-campaign (which stops at a Bulk Texter Pro file because a human runs the SMS send); here the automation IS the send, so it goes through the instagram tool pack chokepoint.
What this skill does (and does not)
It sends the campaign's 1st-message opener to the people marked ready in an Instagram tracker, and advances their status. It does not: set up the campaign or its tags (campaign-builder / campaign-tag), send SMS (sms-campaign), or run the reply ladder (classifying inbound replies and sending the pitch / link, a separate reply-run skill). It sends openers; it does not converse.
The two engines
- Campaign logic (this skill):
scripts/resolve.py, the deterministic,
tested units: the case-insensitive ready filter, first-name + opener personalization, the Notion status patch, and resolve_ig_target (union the two mesh sources for a handle + stored thread link). Exercised by tests/test_resolve.py.
- The send mechanics (the tool pack):
tools/instagram/bin/ig, the controlled
Instagram chokepoint. Read tools/instagram/references/instagram-ops.md before your first run: it owns agent-browser-not-chrome-mcp, the verified send flow, verify-before-send, the direct-link-first navigation, and the opener-already-sent (stale-tracker) guard. This skill calls ig; it never drives the browser itself.
The pipeline
IG campaign tracker (ready rows)
-> resolve handle + stored thread link from mesh (website field UNION social_links)
-> pull the 1st-message opener (Campaigns page) and personalize per first name
-> ig send (verify recipient, skip if our opener is already there, human-paced)
-> advance the tracker row Status (ready -> first-contact status); flag the rest
Step 1: Get the campaign and its parts
Confirm three things (ask only for what is missing):
- The event / campaign and audience, so you know which IG tracker and copy to
use. The IG trackers are named instagram_dm__ (e.g. instagram_dm__ticket-sale, ..._free-ticket).
- The tracker database: a Notion campaign tracker. Each row carries a Name,
a Status select, and a Mesh ID (the join key to the CRM; match on Mesh ID, never on the name string, which drifts). Get its id from the operator or your INDEX.md. Read via the Notion REST API (personal integration token; see tools/notion/references/notion-mcp-ops.md), paging on next_cursor so a long tracker is not truncated.
- The opener: the campaign's "1st message" copy from the operator's private
Notion Campaigns page. Read it; do not invent it. It uses {first_name}.
Step 2: Pull the ready rows
Query the tracker and keep the rows whose Status means "send to this person" (ready, case-insensitive , trackers drift between Ready and ready). Use resolve.sendable_rows. Every kept row gives a Name, the Mesh ID, and the Notion page id (needed to advance the status later).
Step 3: Resolve each recipient's IG handle + thread link from mesh
For each ready row, look the Mesh ID up in the operator's mesh CRM and resolve the Instagram target by unioning two sources (they disagree often; one alone drops sendable people):
- the contact's website field (the private form API's
all_websitesrepeater,
GET /api/v1/network/contacts/{id}/form/), which also holds any stored direct/t/ thread link, and
- the contact's
social_links(getContact).
resolve.resolve_ig_target(website_urls, social_urls) returns {handle, thread_url, source}. A row with no handle in either source is flagged, never guessed (hand it back for the operator to resolve), per Emily's rule. Before creating any contact, searchContacts first (see tools/mesh); never auto-create.
Step 4: Personalize the opener
resolve.personalize(opener, resolve.first_name_of(name)) , substitutes the first name into the campaign's 1st-message copy. One opener per recipient.
Step 5: Send each DM through the tool pack (verify-before-send, paced)
For each resolved recipient, call the chokepoint:
tools/instagram/bin/ig send --handle \
--text "" [--thread-url ]
ig opens the thread (the stored direct/t/ link if given, else profile -> Message -> Expand, which mints a fresh thread link), verifies the recipient's handle is in the open thread, checks whether our exact opener is already present, and only then types and sends. It returns JSON {status, thread_url, ...}:
SENT, the opener went out; capturethread_url.ALREADY_SENT, our opener was already in the thread: **the tracker row is
stale**. Do not re-send; advance its status (Step 6) to correct it.
SKIP/UNSURE, could not verify or confirm (e.g. "no Message button" on a
private / non-mutual profile). Leave the row ready and report it for a human; never force a wrong send.
Pace like a human: wait a randomized delay (e.g. 8,18s) between recipients. Resume-safe: record each result so a re-run skips anyone already SENT (the opener-already-sent guard also prevents a double-send).
Step 6: Advance the tracker, persist the thread link, report
- On
SENTorALREADY_SENT, advance the row's Status fromreadyto the
campaign's first-contact status (e.g. 1st msg) via the Notion pages PATCH (resolve.status_patch_body).
- Write the captured
thread_urlback to the contact's mesh website field so a
future run reuses the direct link (the operator's "update people so it's easier next time" rule). GET-merge-POST the all_websites array so the write appends rather than clobbers.
- Report: sent count, the
SKIP/UNSURErows (with reasons) to retry, and the
no-handle rows for the operator to resolve. Log the run in your LOG.md.
Operating rules that bind here
- Verify before send. The recipient's handle must be in the open thread; a
wrong-thread send is the cardinal error. The tool pack enforces it.
- Don't re-send our opener. If it is already in the thread, the tracker is out
of date , advance it, do not message again.
- agent-browser, never chrome-mcp for Instagram (chrome-mcp hijacks the real
Chrome). The tool pack and its ig-access-guard enforce this.
- Flag, never guess. No usable handle -> reported, not sent to a guessed handle.
- Mesh is the source of truth for handles; the tracker only references the
contact by Mesh ID.
- Privacy / public repo. Handles, names, thread links, tracker ids, and the copy
are private; they live on the operator's accounts and machine, never in this public repo or a sample.
Out of scope
- Setting up the campaign / tags (
campaign-builder,campaign-tag). - The SMS channel (
sms-campaignbuilds a Bulk Texter Pro file instead). - The reply ladder: classifying inbound replies and sending the pitch / link + code,
or a soft decline. That is a separate reply-run skill/play, not this one.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: mujtaba3B
- Source: mujtaba3B/mutwo-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.