# Instagram Dm Campaign

> Event Planner Emily EXECUTES an Instagram-DM outreach campaign: she pulls the send-able (ready) rows from the campaign's IG tracker, resolves each recipient's Instagram handle and any stored DM-thread link from the operator's mesh CRM, personalizes the campaign's 1st-message opener with the first name, and sends each DM through the instagram tool pack (agent-browser, verify-the-recipient-before-s…

- **Type:** Skill
- **Install:** `agentstack add skill-mujtaba3b-mutwo-skills-instagram-dm-campaign`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [mujtaba3B](https://agentstack.voostack.com/s/mujtaba3b)
- **Installs:** 0
- **Category:** [Web & Browser](https://agentstack.voostack.com/c/web-and-browser)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [mujtaba3B](https://github.com/mujtaba3B)
- **Source:** https://github.com/mujtaba3B/mutwo-skills/tree/main/personas/event-planner/skills/instagram-dm-campaign

## Install

```sh
agentstack add skill-mujtaba3b-mutwo-skills-instagram-dm-campaign
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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):

1. **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`).
2. **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.
3. **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_websites` repeater,
  `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:

```bash
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; capture `thread_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 `SENT` or `ALREADY_SENT`, advance the row's Status from `ready` to the
  campaign's first-contact status (e.g. `1st msg`) via the Notion `pages` PATCH
  (`resolve.status_patch_body`).
- **Write the captured `thread_url` back 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`/`UNSURE` rows (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-campaign` builds 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](https://github.com/mujtaba3B)
- **Source:** [mujtaba3B/mutwo-skills](https://github.com/mujtaba3B/mutwo-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-mujtaba3b-mutwo-skills-instagram-dm-campaign
- Seller: https://agentstack.voostack.com/s/mujtaba3b
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
