Instagram Post Downloader Skill

Download and save Instagram posts as high-resolution files.

Instagram Post Downloader Skill — The Skill Playground: pick the Executive Update skill, fill in a few notes, hit run, and watch a structured executive… (from the mohitagw15856/pm-claude-skills README)

From the mohitagw15856/pm-claude-skills README — shows the whole collection, not only this skill. · view on GitHub

How to use it

Claude Code
  1. Run the line below. It pulls the whole folder into ~/.claude/skills/instagram-post-downloader, including the files SKILL.md points to.
  2. Describe your job in plain words. Claude Code follows the skill from there.
Claude Code — installs the whole folder, not just SKILL.md
npx degit mohitagw15856/pm-claude-skills/skills/instagram-post-downloader#main ~/.claude/skills/instagram-post-downloader

For one project only, change the path to .claude/skills/instagram-post-downloader. This skill also uses metadata.txt, instagram_downloader.py, r.json — copying SKILL.md alone won't be enough. See the folder on GitHub.

Claude (web or desktop app)
  1. On this page open ⋯ → Download .md.
  2. Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
  3. Pick the file and Save. Claude shows the name and description and runs a security scan.
  4. Check the skill is switched on.
  5. Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
  1. ChatGPT: make a Project and paste it into Instructions.
  2. Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
  • Check which app you pasted it into — the steps above name the right one.
  • Some skills need the paid tier of Claude or ChatGPT.
Step-by-step guide with screenshots · Ask in the forum

Paste into Claude, ChatGPT or Cursor.

Source of Instagram Post Downloader Skill

Show the full text674 lines
namedescription
instagram-post-downloaderDownload and save Instagram posts as high-resolution files. Use when asked to download, save, or archive an Instagram post, reel thumbnail, or carousel. Produces saved high-res images in a named folder, with carousel slides stitched into a single PDF; supports batch downloading of multiple URLs at once.

Instagram Post Downloader Skill

Downloads Instagram posts at full resolution from Instagram's CDN — no screenshots, no compression. Handles single images, carousels (multi-slide posts), and Reel cover images. For carousels, produces individual slide files plus a single stitched PDF. Supports batch URLs in one run.


PREREQUISITE — Domain Allowlist

Before this skill can fetch any media, you must add Instagram's CDN domain to Claude Code's allowlist:

Settings → Capabilities → Domain allowlist → Add:

*.cdninstagram.com

Without this, all CDN fetch calls will be blocked. If you see a permission error when Claude attempts a fetch to cdninstagram.com, this is the fix.


Required Inputs

Claude will ask for these if not provided upfront:

Input Required Notes
Instagram post URL(s) Yes One per line, or comma-separated. https://www.instagram.com/p/XXXX/ or https://www.instagram.com/reel/XXXX/ format
Output directory No Defaults to ./instagram-downloads/ in the current working directory
PDF stitch for carousels No Defaults to yes — produces carousel.pdf alongside individual slides
File naming prefix No Optional prefix added before slide filenames, e.g. brand_ → brand_slide_01.jpg

Batch input example:

https://www.instagram.com/p/ABC123/
https://www.instagram.com/p/DEF456/
https://www.instagram.com/p/GHI789/

Output Structure

For each URL processed, Claude creates a folder named after the post caption (first 40 characters, sanitised — spaces become underscores, special characters stripped). If no caption is available, the folder is named after the post shortcode.

Single image post
instagram-downloads/
└── this_is_the_caption_first_40_chars/
    ├── image.jpg
    └── metadata.txt
Carousel post
instagram-downloads/
└── carousel_caption_first_40_chars/
    ├── slide_01.jpg
    ├── slide_02.jpg
    ├── slide_03.jpg
    ├── slide_04.jpg
    ├── carousel.pdf          ← all slides stitched in order
    └── metadata.txt
Batch run (3 URLs)
instagram-downloads/
├── first_post_caption_sanitised/
│   ├── image.jpg
│   └── metadata.txt
├── second_post_carousel_caption/
│   ├── slide_01.jpg
│   ├── slide_02.jpg
│   ├── carousel.pdf
│   └── metadata.txt
└── third_post_caption_here/
    ├── image.jpg
    └── metadata.txt
metadata.txt format
Post URL:       https://www.instagram.com/p/XXXX/
Shortcode:      XXXX
Type:           carousel | single_image | reel
Slide count:    4  (carousel only)
Caption:        [full caption text]
Username:       @username
Fetched at:     2026-05-27T14:32:00Z
CDN URLs:
  slide_01.jpg  https://scontent.cdninstagram.com/v/...
  slide_02.jpg  https://scontent.cdninstagram.com/v/...
Completion summary (printed to terminal)
Instagram Post Downloader — Batch Complete
==========================================
URLs processed:   3
Posts saved:      3
Total files:      11  (9 images + 2 PDFs)
Skipped:          0
Output dir:       /Users/you/project/instagram-downloads/

Results:
  ✓ this_is_the_caption_first_40_chars/     1 image
  ✓ carousel_caption_first_40_chars/        4 slides → carousel.pdf
  ✓ third_post_caption_here/                1 image

How Claude Should Execute This Skill

Step 1 — Collect and validate inputs
  1. Accept the URL(s) from the user. If the user pastes a comma-separated list, split on commas. If they paste one per line, split on newlines.
  2. Validate each URL matches instagram.com/p/, instagram.com/reel/, or instagram.com/tv/. Flag malformed URLs before proceeding.
  3. Confirm the output directory. If none provided, use ./instagram-downloads/ and tell the user.
  4. Ask about PDF stitching preference only if the user hasn't said either way. Default is yes.
Step 2 — For each URL: fetch the post page

Fetch the Instagram post page HTML:

GET https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis

Instagram frequently changes its API surface. Use this fallback chain in order:

Attempt A — JSON endpoint:

https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis

Parse the JSON response. Look for graphql.shortcode_media or data.shortcode_media.

Attempt B — Embed page (most reliable):

https://www.instagram.com/p/{shortcode}/embed/captioned/

Fetch this page's HTML and extract og:image meta tags and any window.__additionalDataLoaded or window.__StaticData JSON blobs embedded in <script> tags.

Attempt C — oEmbed endpoint:

https://api.instagram.com/oembed/?url=https://www.instagram.com/p/{shortcode}/&omitscript=true

This returns thumbnail_url — useful for single images, but only gives the first frame for carousels.

Headers to include on all requests:

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
Accept-Language: en-US,en;q=0.9
Accept: text/html,application/xhtml+xml,application/json
Step 3 — Extract CDN image URLs

From the fetched data, extract all high-resolution CDN URLs. Instagram CDN URLs follow these patterns:

https://scontent.cdninstagram.com/v/...jpg?...
https://scontent-lax3-1.cdninstagram.com/v/...jpg?...
https://instagram.fXXX1-1.fbcdn.net/v/...jpg?...

For single image posts:

  • Extract the single display_url or the largest display_resources entry (pick the one with the highest config_width).

For carousel posts:

  • Look for edge_sidecar_to_children.edges[] in the JSON. Each edge has its own node.display_url and node.display_resources[].
  • Iterate all edges in order. This determines slide numbering.
  • Pick the highest-resolution variant from each slide's display_resources array.

For Reels:

  • The cover image is extractable the same way as a single image.
  • The video file itself requires a third-party tool (see Bonus section).

If JSON extraction fails, fall back to scraping <meta property="og:image"> tags from the page HTML — this gives at least one image URL (the first slide or only image).

Step 4 — Sanitise folder name

Build the folder name from the post caption:

  1. Take the first 40 characters of the caption.
  2. Strip all characters that are not alphanumeric, spaces, or hyphens.
  3. Replace spaces and hyphens with underscores.
  4. Lowercase the result.
  5. Strip leading/trailing underscores.
  6. If the result is empty (e.g. caption was all emoji), use the post shortcode instead.
import re

def sanitise_folder_name(caption: str, shortcode: str) -> str:
    truncated = caption[:40]
    cleaned = re.sub(r'[^a-zA-Z0-9 \-]', '', truncated)
    underscored = re.sub(r'[\s\-]+', '_', cleaned).strip('_').lower()
    return underscored if underscored else shortcode
Step 5 — Create output folder structure
import os

base_dir = "./instagram-downloads"
folder_name = sanitise_folder_name(caption, shortcode)
post_dir = os.path.join(base_dir, folder_name)
os.makedirs(post_dir, exist_ok=True)

If a folder with that name already exists (e.g. running the same URL twice), append the shortcode to avoid collision: folder_name_SHORTCODE.

Step 6 — Download each image file

For each CDN URL, download the file with a streaming GET request:

import requests

def download_file(url: str, dest_path: str) -> bool:
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
        "Referer": "https://www.instagram.com/",
    }
    response = requests.get(url, headers=headers, stream=True, timeout=30)
    response.raise_for_status()
    with open(dest_path, "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)
    return True

Name files:

  • Single image: image.jpg
  • Carousel slides: slide_01.jpg, slide_02.jpg, ... (zero-padded to 2 digits, or 3 digits if >99 slides)

Detect file format from the Content-Type header or URL extension. Instagram serves JPEG for photos and may serve WebP in some cases — preserve the actual extension.

After all slides are downloaded, stitch them into a single PDF using Pillow:

from PIL import Image

def stitch_to_pdf(image_paths: list[str], output_path: str) -> None:
    """
    Combine a list of image files into a single multi-page PDF.
    Each image becomes one page. Page size matches the image dimensions.
    """
    images = []
    for path in sorted(image_paths):  # sort ensures slide_01, slide_02, ... order
        img = Image.open(path).convert("RGB")
        images.append(img)

    if not images:
        return

    first = images[0]
    rest = images[1:]
    first.save(
        output_path,
        format="PDF",
        save_all=True,
        append_images=rest,
        resolution=150.0,
    )

Save as carousel.pdf in the post folder. If Pillow is not installed, run pip install Pillow first — or instruct the user to do so.

Dependency check at start of skill:

try:
    from PIL import Image
except ImportError:
    print("Pillow not installed. Run: pip install Pillow")
    print("PDF stitching will be skipped. Individual slides will still be downloaded.")
    skip_pdf = True
Step 8 — Write metadata.txt

Write a metadata.txt file into the post folder with all extracted metadata:

from datetime import datetime, timezone

def write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_urls):
    lines = [
        f"Post URL:       {post_url}",
        f"Shortcode:      {shortcode}",
        f"Type:           {post_type}",
    ]
    if post_type == "carousel":
        lines.append(f"Slide count:    {len(cdn_urls)}")
    lines += [
        f"Caption:        {caption}",
        f"Username:       @{username}",
        f"Fetched at:     {datetime.now(timezone.utc).isoformat()}",
        "CDN URLs:",
    ]
    for filename, url in cdn_urls.items():
        lines.append(f"  {filename:<16} {url}")

    with open(os.path.join(post_dir, "metadata.txt"), "w", encoding="utf-8") as f:
        f.write("\n".join(lines) + "\n")
Step 9 — Print completion summary

After processing all URLs, print the summary table to the terminal (format shown in Output Structure section above). Include:

  • Total URLs attempted
  • Posts successfully saved
  • Total files written (images + PDFs separately)
  • Any URLs that were skipped and the reason
Step 10 — Handle errors gracefully
Error scenario Action
URL is not an Instagram URL Skip with message: "Skipped — not an Instagram URL: [url]"
Post is private or requires login Skip with message: "Skipped — post is private or login required: [url]"
CDN fetch returns 403/404 Try alternate CDN URL if available; if none, skip slide and note in metadata
Pillow not installed Skip PDF stitching, save slides only, note in summary
Network timeout Retry once after 5 seconds; if still failing, skip and log
Folder name collision Append shortcode suffix to folder name
Rate limiting (429) Wait 10 seconds and retry; log if retry also fails

Bonus — Downloading Instagram Reels (Video)

This skill covers images and carousel PDFs. For Reels video files, Claude Code cannot download video directly without a third-party tool, because Instagram's video CDN uses signed URLs and additional auth tokens.

Recommended approach for Reels:

Use yt-dlp, a maintained open-source tool:

# Install
pip install yt-dlp

# Download a Reel
yt-dlp "https://www.instagram.com/reel/XXXX/" -o "%(title)s.%(ext)s"

# Download to a specific folder
yt-dlp "https://www.instagram.com/reel/XXXX/" \
  -o "./instagram-downloads/%(uploader)s_%(id)s.%(ext)s"

# Download best quality
yt-dlp -f "bestvideo+bestaudio" "https://www.instagram.com/reel/XXXX/"

Claude can run this command via Bash if the user asks. yt-dlp handles the auth token extraction automatically for public Reels.


Full Script Template

Claude should offer to write this as a standalone script (instagram_downloader.py) that the user can run independently:

#!/usr/bin/env python3
"""
Instagram Post Downloader
Fetches high-res images from public Instagram posts and carousels.
Requires: pip install requests Pillow
"""

import os
import re
import sys
import json
import time
import requests
from datetime import datetime, timezone
from pathlib import Path

try:
    from PIL import Image
    PILLOW_AVAILABLE = True
except ImportError:
    PILLOW_AVAILABLE = False
    print("Warning: Pillow not installed. PDF stitching disabled. Run: pip install Pillow")


HEADERS = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Referer": "https://www.instagram.com/",
}


def extract_shortcode(url: str) -> str:
    match = re.search(r"instagram\.com/(?:p|reel|tv)/([A-Za-z0-9_-]+)", url)
    if not match:
        raise ValueError(f"Cannot extract shortcode from URL: {url}")
    return match.group(1)


def fetch_post_data(shortcode: str) -> dict:
    """Try multiple endpoints to get post JSON data."""
    # Attempt A: JSON endpoint
    try:
        url = f"https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis"
        r = requests.get(url, headers=HEADERS, timeout=15)
        if r.status_code == 200:
            data = r.json()
            media = (data.get("graphql", {}).get("shortcode_media") or
                     data.get("data", {}).get("shortcode_media"))
            if media:
                return media
    except Exception:
        pass

    # Attempt B: Embed page
    try:
        url = f"https://www.instagram.com/p/{shortcode}/embed/captioned/"
        r = requests.get(url, headers=HEADERS, timeout=15)
        html = r.text
        # Look for JSON blob in script tags
        matches = re.findall(r'window\.__additionalDataLoaded\([^,]+,(\{.+?\})\);', html)
        for blob in matches:
            try:
                data = json.loads(blob)
                media = (data.get("graphql", {}).get("shortcode_media") or
                         data.get("data", {}).get("shortcode_media"))
                if media:
                    return media
            except json.JSONDecodeError:
                continue
    except Exception:
        pass

    return {}


def get_cdn_urls(media: dict) -> list[tuple[str, str]]:
    """Return list of (filename, cdn_url) tuples."""
    results = []
    media_type = media.get("__typename", "")

    if media_type == "GraphSidecar":
        edges = media.get("edge_sidecar_to_children", {}).get("edges", [])
        for i, edge in enumerate(edges, start=1):
            node = edge.get("node", {})
            resources = node.get("display_resources", [])
            url = (max(resources, key=lambda r: r.get("config_width", 0)).get("src")
                   if resources else node.get("display_url", ""))
            if url:
                ext = "jpg" if "jpg" in url.lower() else "webp"
                filename = f"slide_{i:02d}.{ext}"
                results.append((filename, url))
    else:
        resources = media.get("display_resources", [])
        url = (max(resources, key=lambda r: r.get("config_width", 0)).get("src")
               if resources else media.get("display_url", ""))
        if url:
            ext = "jpg" if "jpg" in url.lower() else "webp"
            results.append((f"image.{ext}", url))

    return results


def sanitise_folder_name(caption: str, shortcode: str) -> str:
    truncated = caption[:40] if caption else ""
    cleaned = re.sub(r"[^a-zA-Z0-9 \-]", "", truncated)
    underscored = re.sub(r"[\s\-]+", "_", cleaned).strip("_").lower()
    return underscored if underscored else shortcode


def download_file(url: str, dest_path: str) -> bool:
    r = requests.get(url, headers=HEADERS, stream=True, timeout=30)
    r.raise_for_status()
    with open(dest_path, "wb") as f:
        for chunk in r.iter_content(chunk_size=8192):
            f.write(chunk)
    return True


def stitch_pdf(image_paths: list[str], output_path: str) -> None:
    if not PILLOW_AVAILABLE:
        return
    images = [Image.open(p).convert("RGB") for p in sorted(image_paths)]
    if images:
        images[0].save(output_path, format="PDF", save_all=True,
                       append_images=images[1:], resolution=150.0)


def process_url(post_url: str, base_dir: str, stitch_pdf_flag: bool) -> dict:
    result = {"url": post_url, "status": "ok", "files": [], "error": None}
    try:
        shortcode = extract_shortcode(post_url)
        media = fetch_post_data(shortcode)

        caption = ""
        username = ""
        if media:
            caption_edges = media.get("edge_media_to_caption", {}).get("edges", [])
            caption = caption_edges[0]["node"]["text"] if caption_edges else ""
            owner = media.get("owner", {})
            username = owner.get("username", "")

        folder_name = sanitise_folder_name(caption, shortcode)
        post_dir = os.path.join(base_dir, folder_name)
        if os.path.exists(post_dir):
            post_dir = f"{post_dir}_{shortcode}"
        os.makedirs(post_dir, exist_ok=True)

        cdn_urls = get_cdn_urls(media) if media else []
        if not cdn_urls:
            # Fallback: oEmbed
            oembed_url = f"https://api.instagram.com/oembed/?url={post_url}&omitscript=true"
            r = requests.get(oembed_url, headers=HEADERS, timeout=10)
            if r.status_code == 200:
                thumb = r.json().get("thumbnail_url", "")
                if thumb:
                    cdn_urls = [("image.jpg", thumb)]
                    username = r.json().get("author_name", "")

        downloaded_paths = []
        cdn_map = {}
        for filename, url in cdn_urls:
            dest = os.path.join(post_dir, filename)
            download_file(url, dest)
            downloaded_paths.append(dest)
            cdn_map[filename] = url
            result["files"].append(filename)

        if stitch_pdf_flag and len(downloaded_paths) > 1 and PILLOW_AVAILABLE:
            pdf_path = os.path.join(post_dir, "carousel.pdf")
            stitch_pdf(downloaded_paths, pdf_path)
            result["files"].append("carousel.pdf")

        post_type = "carousel" if len(cdn_urls) > 1 else "single_image"
        write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_map)
        result["files"].append("metadata.txt")

    except Exception as e:
        result["status"] = "error"
        result["error"] = str(e)

    return result


def write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_map):
    lines = [
        f"Post URL:       {post_url}",
        f"Shortcode:      {shortcode}",
        f"Type:           {post_type}",
    ]
    if post_type == "carousel":
        lines.append(f"Slide count:    {len([k for k in cdn_map if 'slide' in k])}")
    lines += [
        f"Caption:        {caption}",
        f"Username:       @{username}",
        f"Fetched at:     {datetime.now(timezone.utc).isoformat()}",
        "CDN URLs:",
    ]
    for fn, url in cdn_map.items():
        lines.append(f"  {fn:<18} {url}")
    with open(os.path.join(post_dir, "metadata.txt"), "w", encoding="utf-8") as f:
        f.write("\n".join(lines) + "\n")


def main(urls: list[str], base_dir: str = "./instagram-downloads", stitch: bool = True):
    os.makedirs(base_dir, exist_ok=True)
    results = []
    for url in urls:
        url = url.strip()
        if not url:
            continue
        print(f"Processing: {url}")
        r = process_url(url, base_dir, stitch)
        results.append(r)
        time.sleep(1)  # polite delay between requests

    # Summary
    ok = [r for r in results if r["status"] == "ok"]
    err = [r for r in results if r["status"] == "error"]
    total_files = sum(len(r["files"]) for r in ok)
    print("\nInstagram Post Downloader — Batch Complete")
    print("==========================================")
    print(f"URLs processed:   {len(results)}")
    print(f"Posts saved:      {len(ok)}")
    print(f"Total files:      {total_files}")
    print(f"Errors:           {len(err)}")
    print(f"Output dir:       {os.path.abspath(base_dir)}\n")
    for r in results:
        if r["status"] == "ok":
            print(f"  OK  {r['url']}")
        else:
            print(f"  ERR {r['url']}  — {r['error']}")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python instagram_downloader.py <url1> [url2] ...")
        sys.exit(1)
    main(sys.argv[1:])

Quality Checks

Before marking the task complete, verify each item:

  • Domain allowlist confirmed — *.cdninstagram.com is added before any fetch attempts
  • All provided URLs validated as Instagram URLs before processing begins
  • CDN URLs are the highest-resolution variants available (largest config_width selected)
  • Folder name is sanitised — no special characters, no spaces, max 40 chars from caption
  • Folder collision handled — shortcode appended if folder already exists
  • Carousel slides numbered sequentially with zero-padding (slide_01, slide_02, ...)
  • PDF includes all slides in correct order (not alphabetical — by slide index)
  • metadata.txt written to every post folder, including full CDN URLs
  • Pillow dependency checked at startup — graceful fallback if not available
  • Batch completion summary printed with file counts and any errors
  • Private post errors caught and reported — not silently skipped
  • Rate limiting handled — at least 1 second delay between requests
  • No credential or cookie storage — skill operates on public posts only

Anti-Patterns

  • Do not attempt to download private posts or content behind a login wall — this skill is for public posts only
  • Do not ignore 429 rate-limit responses — always implement a backoff wait before retrying
  • Do not save all downloads to a single flat folder when processing multiple accounts — use named subfolders per source
  • Do not skip PDF stitching for carousel posts — individual slides delivered without a combined PDF are incomplete output
  • Do not proceed if Instagram returns a login wall — surface the limitation clearly rather than returning an error silently

Example Trigger Phrases

  • "Download this Instagram post for me: https://www.instagram.com/p/ABC123/"
  • "Save that carousel to my downloads folder"
  • "Can you grab all the slides from this Instagram post and make a PDF?"
  • "Download these 5 Instagram posts" [followed by list of URLs]
  • "Archive this IG post before it gets deleted"
  • "I need the full-res images from this carousel"
  • "Download the images from this Instagram URL and stitch them into a PDF"
  • "Batch download these Instagram posts" [followed by URLs]
  • "Save the slides from this Instagram carousel as individual JPEGs"
  • "Get me the high-res version of this Instagram image"

Notes on Instagram's Anti-Scraping Measures

Instagram actively changes its page structure and API endpoints. If all three fetch attempts fail:

  1. The embed page method (/embed/captioned/) is historically the most stable — start there.
  2. CDN URLs expire. Download immediately after fetching — do not store URLs and download later.
  3. Instagram may return a login wall for some posts even if they're technically public. If this happens, the skill cannot proceed without authentication (which is out of scope).
  4. If Instagram returns a 429, wait 10–30 seconds before retrying. Reduce batch size for large lists.

This skill is designed for public posts only. It does not support login, sessions, or private content.


Originally inspired by a skill from Frank and Diana Dovgopol (Write, Prompt, Scale) — adapted and extended for this library.

1---
2name: instagram-post-downloader
3description: "Download and save Instagram posts as high-resolution files. Use when asked to download, save, or archive an Instagram post, reel thumbnail, or carousel. Produces saved high-res images in a named folder, with carousel slides stitched into a single PDF; supports batch downloading of multiple URLs at once."
4---
5 
6# Instagram Post Downloader Skill
7 
8Downloads Instagram posts at full resolution from Instagram's CDN — no screenshots, no compression. Handles single images, carousels (multi-slide posts), and Reel cover images. For carousels, produces individual slide files plus a single stitched PDF. Supports batch URLs in one run.
9 
10---
11 
12## PREREQUISITE — Domain Allowlist
13 
14Before this skill can fetch any media, you must add Instagram's CDN domain to Claude Code's allowlist:
15 
16**Settings → Capabilities → Domain allowlist → Add:**
17```
18*.cdninstagram.com
19```
20 
21Without this, all CDN fetch calls will be blocked. If you see a permission error when Claude attempts a fetch to `cdninstagram.com`, this is the fix.
22 
23---
24 
25## Required Inputs
26 
27Claude will ask for these if not provided upfront:
28 
29| Input | Required | Notes |
30|---|---|---|
31| Instagram post URL(s) | Yes | One per line, or comma-separated. `https://www.instagram.com/p/XXXX/` or `https://www.instagram.com/reel/XXXX/` format |
32| Output directory | No | Defaults to `./instagram-downloads/` in the current working directory |
33| PDF stitch for carousels | No | Defaults to **yes** — produces `carousel.pdf` alongside individual slides |
34| File naming prefix | No | Optional prefix added before slide filenames, e.g. `brand_` → `brand_slide_01.jpg` |
35 
36**Batch input example:**
37```
38https://www.instagram.com/p/ABC123/
39https://www.instagram.com/p/DEF456/
40https://www.instagram.com/p/GHI789/
41```
42 
43---
44 
45## Output Structure
46 
47For each URL processed, Claude creates a folder named after the post caption (first 40 characters, sanitised — spaces become underscores, special characters stripped). If no caption is available, the folder is named after the post shortcode.
48 
49### Single image post
50 
51```
52instagram-downloads/
53└── this_is_the_caption_first_40_chars/
54 ├── image.jpg
55 └── metadata.txt
56```
57 
58### Carousel post
59 
60```
61instagram-downloads/
62└── carousel_caption_first_40_chars/
63 ├── slide_01.jpg
64 ├── slide_02.jpg
65 ├── slide_03.jpg
66 ├── slide_04.jpg
67 ├── carousel.pdf ← all slides stitched in order
68 └── metadata.txt
69```
70 
71### Batch run (3 URLs)
72 
73```
74instagram-downloads/
75├── first_post_caption_sanitised/
76│ ├── image.jpg
77│ └── metadata.txt
78├── second_post_carousel_caption/
79│ ├── slide_01.jpg
80│ ├── slide_02.jpg
81│ ├── carousel.pdf
82│ └── metadata.txt
83└── third_post_caption_here/
84 ├── image.jpg
85 └── metadata.txt
86```
87 
88### metadata.txt format
89 
90```
91Post URL: https://www.instagram.com/p/XXXX/
92Shortcode: XXXX
93Type: carousel | single_image | reel
94Slide count: 4 (carousel only)
95Caption: [full caption text]
96Username: @username
97Fetched at: 2026-05-27T14:32:00Z
98CDN URLs:
99 slide_01.jpg https://scontent.cdninstagram.com/v/...
100 slide_02.jpg https://scontent.cdninstagram.com/v/...
101```
102 
103### Completion summary (printed to terminal)
104 
105```
106Instagram Post Downloader — Batch Complete
107==========================================
108URLs processed: 3
109Posts saved: 3
110Total files: 11 (9 images + 2 PDFs)
111Skipped: 0
112Output dir: /Users/you/project/instagram-downloads/
113 
114Results:
115 ✓ this_is_the_caption_first_40_chars/ 1 image
116 ✓ carousel_caption_first_40_chars/ 4 slides → carousel.pdf
117 ✓ third_post_caption_here/ 1 image
118```
119 
120---
121 
122## How Claude Should Execute This Skill
123 
124### Step 1 — Collect and validate inputs
125 
1261. Accept the URL(s) from the user. If the user pastes a comma-separated list, split on commas. If they paste one per line, split on newlines.
1272. Validate each URL matches `instagram.com/p/`, `instagram.com/reel/`, or `instagram.com/tv/`. Flag malformed URLs before proceeding.
1283. Confirm the output directory. If none provided, use `./instagram-downloads/` and tell the user.
1294. Ask about PDF stitching preference only if the user hasn't said either way. Default is yes.
130 
131### Step 2 — For each URL: fetch the post page
132 
133Fetch the Instagram post page HTML:
134 
135```
136GET https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis
137```
138 
139Instagram frequently changes its API surface. Use this fallback chain in order:
140 
141**Attempt A — JSON endpoint:**
142```
143https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis
144```
145Parse the JSON response. Look for `graphql.shortcode_media` or `data.shortcode_media`.
146 
147**Attempt B — Embed page (most reliable):**
148```
149https://www.instagram.com/p/{shortcode}/embed/captioned/
150```
151Fetch this page's HTML and extract `og:image` meta tags and any `window.__additionalDataLoaded` or `window.__StaticData` JSON blobs embedded in `<script>` tags.
152 
153**Attempt C — oEmbed endpoint:**
154```
155https://api.instagram.com/oembed/?url=https://www.instagram.com/p/{shortcode}/&omitscript=true
156```
157This returns `thumbnail_url` — useful for single images, but only gives the first frame for carousels.
158 
159**Headers to include on all requests:**
160```
161User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
162Accept-Language: en-US,en;q=0.9
163Accept: text/html,application/xhtml+xml,application/json
164```
165 
166### Step 3 — Extract CDN image URLs
167 
168From the fetched data, extract all high-resolution CDN URLs. Instagram CDN URLs follow these patterns:
169 
170```
171https://scontent.cdninstagram.com/v/...jpg?...
172https://scontent-lax3-1.cdninstagram.com/v/...jpg?...
173https://instagram.fXXX1-1.fbcdn.net/v/...jpg?...
174```
175 
176**For single image posts:**
177- Extract the single `display_url` or the largest `display_resources` entry (pick the one with the highest `config_width`).
178 
179**For carousel posts:**
180- Look for `edge_sidecar_to_children.edges[]` in the JSON. Each edge has its own `node.display_url` and `node.display_resources[]`.
181- Iterate all edges in order. This determines slide numbering.
182- Pick the highest-resolution variant from each slide's `display_resources` array.
183 
184**For Reels:**
185- The cover image is extractable the same way as a single image.
186- The video file itself requires a third-party tool (see Bonus section).
187 
188**If JSON extraction fails**, fall back to scraping `<meta property="og:image">` tags from the page HTML — this gives at least one image URL (the first slide or only image).
189 
190### Step 4 — Sanitise folder name
191 
192Build the folder name from the post caption:
1931. Take the first 40 characters of the caption.
1942. Strip all characters that are not alphanumeric, spaces, or hyphens.
1953. Replace spaces and hyphens with underscores.
1964. Lowercase the result.
1975. Strip leading/trailing underscores.
1986. If the result is empty (e.g. caption was all emoji), use the post shortcode instead.
199 
200```python
201import re
202 
203def sanitise_folder_name(caption: str, shortcode: str) -> str:
204 truncated = caption[:40]
205 cleaned = re.sub(r'[^a-zA-Z0-9 \-]', '', truncated)
206 underscored = re.sub(r'[\s\-]+', '_', cleaned).strip('_').lower()
207 return underscored if underscored else shortcode
208```
209 
210### Step 5 — Create output folder structure
211 
212```python
213import os
214 
215base_dir = "./instagram-downloads"
216folder_name = sanitise_folder_name(caption, shortcode)
217post_dir = os.path.join(base_dir, folder_name)
218os.makedirs(post_dir, exist_ok=True)
219```
220 
221If a folder with that name already exists (e.g. running the same URL twice), append the shortcode to avoid collision: `folder_name_SHORTCODE`.
222 
223### Step 6 — Download each image file
224 
225For each CDN URL, download the file with a streaming GET request:
226 
227```python
228import requests
229 
230def download_file(url: str, dest_path: str) -> bool:
231 headers = {
232 "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
233 "Referer": "https://www.instagram.com/",
234 }
235 response = requests.get(url, headers=headers, stream=True, timeout=30)
236 response.raise_for_status()
237 with open(dest_path, "wb") as f:
238 for chunk in response.iter_content(chunk_size=8192):
239 f.write(chunk)
240 return True
241```
242 
243Name files:
244- Single image: `image.jpg`
245- Carousel slides: `slide_01.jpg`, `slide_02.jpg`, ... (zero-padded to 2 digits, or 3 digits if >99 slides)
246 
247Detect file format from the `Content-Type` header or URL extension. Instagram serves JPEG for photos and may serve WebP in some cases — preserve the actual extension.
248 
249### Step 7 — Stitch carousel PDF (if applicable)
250 
251After all slides are downloaded, stitch them into a single PDF using Pillow:
252 
253```python
254from PIL import Image
255 
256def stitch_to_pdf(image_paths: list[str], output_path: str) -> None:
257 """
258 Combine a list of image files into a single multi-page PDF.
259 Each image becomes one page. Page size matches the image dimensions.
260 """
261 images = []
262 for path in sorted(image_paths): # sort ensures slide_01, slide_02, ... order
263 img = Image.open(path).convert("RGB")
264 images.append(img)
265 
266 if not images:
267 return
268 
269 first = images[0]
270 rest = images[1:]
271 first.save(
272 output_path,
273 format="PDF",
274 save_all=True,
275 append_images=rest,
276 resolution=150.0,
277 )
278```
279 
280Save as `carousel.pdf` in the post folder. If Pillow is not installed, run `pip install Pillow` first — or instruct the user to do so.
281 
282**Dependency check at start of skill:**
283```python
284try:
285 from PIL import Image
286except ImportError:
287 print("Pillow not installed. Run: pip install Pillow")
288 print("PDF stitching will be skipped. Individual slides will still be downloaded.")
289 skip_pdf = True
290```
291 
292### Step 8 — Write metadata.txt
293 
294Write a `metadata.txt` file into the post folder with all extracted metadata:
295 
296```python
297from datetime import datetime, timezone
298 
299def write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_urls):
300 lines = [
301 f"Post URL: {post_url}",
302 f"Shortcode: {shortcode}",
303 f"Type: {post_type}",
304 ]
305 if post_type == "carousel":
306 lines.append(f"Slide count: {len(cdn_urls)}")
307 lines += [
308 f"Caption: {caption}",
309 f"Username: @{username}",
310 f"Fetched at: {datetime.now(timezone.utc).isoformat()}",
311 "CDN URLs:",
312 ]
313 for filename, url in cdn_urls.items():
314 lines.append(f" {filename:<16} {url}")
315 
316 with open(os.path.join(post_dir, "metadata.txt"), "w", encoding="utf-8") as f:
317 f.write("\n".join(lines) + "\n")
318```
319 
320### Step 9 — Print completion summary
321 
322After processing all URLs, print the summary table to the terminal (format shown in Output Structure section above). Include:
323- Total URLs attempted
324- Posts successfully saved
325- Total files written (images + PDFs separately)
326- Any URLs that were skipped and the reason
327 
328### Step 10 — Handle errors gracefully
329 
330| Error scenario | Action |
331|---|---|
332| URL is not an Instagram URL | Skip with message: "Skipped — not an Instagram URL: [url]" |
333| Post is private or requires login | Skip with message: "Skipped — post is private or login required: [url]" |
334| CDN fetch returns 403/404 | Try alternate CDN URL if available; if none, skip slide and note in metadata |
335| Pillow not installed | Skip PDF stitching, save slides only, note in summary |
336| Network timeout | Retry once after 5 seconds; if still failing, skip and log |
337| Folder name collision | Append shortcode suffix to folder name |
338| Rate limiting (429) | Wait 10 seconds and retry; log if retry also fails |
339 
340---
341 
342## Bonus — Downloading Instagram Reels (Video)
343 
344This skill covers images and carousel PDFs. For Reels video files, Claude Code cannot download video directly without a third-party tool, because Instagram's video CDN uses signed URLs and additional auth tokens.
345 
346**Recommended approach for Reels:**
347 
348Use `yt-dlp`, a maintained open-source tool:
349 
350```bash
351# Install
352pip install yt-dlp
353 
354# Download a Reel
355yt-dlp "https://www.instagram.com/reel/XXXX/" -o "%(title)s.%(ext)s"
356 
357# Download to a specific folder
358yt-dlp "https://www.instagram.com/reel/XXXX/" \
359 -o "./instagram-downloads/%(uploader)s_%(id)s.%(ext)s"
360 
361# Download best quality
362yt-dlp -f "bestvideo+bestaudio" "https://www.instagram.com/reel/XXXX/"
363```
364 
365Claude can run this command via Bash if the user asks. `yt-dlp` handles the auth token extraction automatically for public Reels.
366 
367---
368 
369## Full Script Template
370 
371Claude should offer to write this as a standalone script (`instagram_downloader.py`) that the user can run independently:
372 
373```python
374#!/usr/bin/env python3
375"""
376Instagram Post Downloader
377Fetches high-res images from public Instagram posts and carousels.
378Requires: pip install requests Pillow
379"""
380 
381import os
382import re
383import sys
384import json
385import time
386import requests
387from datetime import datetime, timezone
388from pathlib import Path
389 
390try:
391 from PIL import Image
392 PILLOW_AVAILABLE = True
393except ImportError:
394 PILLOW_AVAILABLE = False
395 print("Warning: Pillow not installed. PDF stitching disabled. Run: pip install Pillow")
396 
397 
398HEADERS = {
399 "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
400 "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
401 "Accept-Language": "en-US,en;q=0.9",
402 "Referer": "https://www.instagram.com/",
403}
404 
405 
406def extract_shortcode(url: str) -> str:
407 match = re.search(r"instagram\.com/(?:p|reel|tv)/([A-Za-z0-9_-]+)", url)
408 if not match:
409 raise ValueError(f"Cannot extract shortcode from URL: {url}")
410 return match.group(1)
411 
412 
413def fetch_post_data(shortcode: str) -> dict:
414 """Try multiple endpoints to get post JSON data."""
415 # Attempt A: JSON endpoint
416 try:
417 url = f"https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis"
418 r = requests.get(url, headers=HEADERS, timeout=15)
419 if r.status_code == 200:
420 data = r.json()
421 media = (data.get("graphql", {}).get("shortcode_media") or
422 data.get("data", {}).get("shortcode_media"))
423 if media:
424 return media
425 except Exception:
426 pass
427 
428 # Attempt B: Embed page
429 try:
430 url = f"https://www.instagram.com/p/{shortcode}/embed/captioned/"
431 r = requests.get(url, headers=HEADERS, timeout=15)
432 html = r.text
433 # Look for JSON blob in script tags
434 matches = re.findall(r'window\.__additionalDataLoaded\([^,]+,(\{.+?\})\);', html)
435 for blob in matches:
436 try:
437 data = json.loads(blob)
438 media = (data.get("graphql", {}).get("shortcode_media") or
439 data.get("data", {}).get("shortcode_media"))
440 if media:
441 return media
442 except json.JSONDecodeError:
443 continue
444 except Exception:
445 pass
446 
447 return {}
448 
449 
450def get_cdn_urls(media: dict) -> list[tuple[str, str]]:
451 """Return list of (filename, cdn_url) tuples."""
452 results = []
453 media_type = media.get("__typename", "")
454 
455 if media_type == "GraphSidecar":
456 edges = media.get("edge_sidecar_to_children", {}).get("edges", [])
457 for i, edge in enumerate(edges, start=1):
458 node = edge.get("node", {})
459 resources = node.get("display_resources", [])
460 url = (max(resources, key=lambda r: r.get("config_width", 0)).get("src")
461 if resources else node.get("display_url", ""))
462 if url:
463 ext = "jpg" if "jpg" in url.lower() else "webp"
464 filename = f"slide_{i:02d}.{ext}"
465 results.append((filename, url))
466 else:
467 resources = media.get("display_resources", [])
468 url = (max(resources, key=lambda r: r.get("config_width", 0)).get("src")
469 if resources else media.get("display_url", ""))
470 if url:
471 ext = "jpg" if "jpg" in url.lower() else "webp"
472 results.append((f"image.{ext}", url))
473 
474 return results
475 
476 
477def sanitise_folder_name(caption: str, shortcode: str) -> str:
478 truncated = caption[:40] if caption else ""
479 cleaned = re.sub(r"[^a-zA-Z0-9 \-]", "", truncated)
480 underscored = re.sub(r"[\s\-]+", "_", cleaned).strip("_").lower()
481 return underscored if underscored else shortcode
482 
483 
484def download_file(url: str, dest_path: str) -> bool:
485 r = requests.get(url, headers=HEADERS, stream=True, timeout=30)
486 r.raise_for_status()
487 with open(dest_path, "wb") as f:
488 for chunk in r.iter_content(chunk_size=8192):
489 f.write(chunk)
490 return True
491 
492 
493def stitch_pdf(image_paths: list[str], output_path: str) -> None:
494 if not PILLOW_AVAILABLE:
495 return
496 images = [Image.open(p).convert("RGB") for p in sorted(image_paths)]
497 if images:
498 images[0].save(output_path, format="PDF", save_all=True,
499 append_images=images[1:], resolution=150.0)
500 
501 
502def process_url(post_url: str, base_dir: str, stitch_pdf_flag: bool) -> dict:
503 result = {"url": post_url, "status": "ok", "files": [], "error": None}
504 try:
505 shortcode = extract_shortcode(post_url)
506 media = fetch_post_data(shortcode)
507 
508 caption = ""
509 username = ""
510 if media:
511 caption_edges = media.get("edge_media_to_caption", {}).get("edges", [])
512 caption = caption_edges[0]["node"]["text"] if caption_edges else ""
513 owner = media.get("owner", {})
514 username = owner.get("username", "")
515 
516 folder_name = sanitise_folder_name(caption, shortcode)
517 post_dir = os.path.join(base_dir, folder_name)
518 if os.path.exists(post_dir):
519 post_dir = f"{post_dir}_{shortcode}"
520 os.makedirs(post_dir, exist_ok=True)
521 
522 cdn_urls = get_cdn_urls(media) if media else []
523 if not cdn_urls:
524 # Fallback: oEmbed
525 oembed_url = f"https://api.instagram.com/oembed/?url={post_url}&omitscript=true"
526 r = requests.get(oembed_url, headers=HEADERS, timeout=10)
527 if r.status_code == 200:
528 thumb = r.json().get("thumbnail_url", "")
529 if thumb:
530 cdn_urls = [("image.jpg", thumb)]
531 username = r.json().get("author_name", "")
532 
533 downloaded_paths = []
534 cdn_map = {}
535 for filename, url in cdn_urls:
536 dest = os.path.join(post_dir, filename)
537 download_file(url, dest)
538 downloaded_paths.append(dest)
539 cdn_map[filename] = url
540 result["files"].append(filename)
541 
542 if stitch_pdf_flag and len(downloaded_paths) > 1 and PILLOW_AVAILABLE:
543 pdf_path = os.path.join(post_dir, "carousel.pdf")
544 stitch_pdf(downloaded_paths, pdf_path)
545 result["files"].append("carousel.pdf")
546 
547 post_type = "carousel" if len(cdn_urls) > 1 else "single_image"
548 write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_map)
549 result["files"].append("metadata.txt")
550 
551 except Exception as e:
552 result["status"] = "error"
553 result["error"] = str(e)
554 
555 return result
556 
557 
558def write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_map):
559 lines = [
560 f"Post URL: {post_url}",
561 f"Shortcode: {shortcode}",
562 f"Type: {post_type}",
563 ]
564 if post_type == "carousel":
565 lines.append(f"Slide count: {len([k for k in cdn_map if 'slide' in k])}")
566 lines += [
567 f"Caption: {caption}",
568 f"Username: @{username}",
569 f"Fetched at: {datetime.now(timezone.utc).isoformat()}",
570 "CDN URLs:",
571 ]
572 for fn, url in cdn_map.items():
573 lines.append(f" {fn:<18} {url}")
574 with open(os.path.join(post_dir, "metadata.txt"), "w", encoding="utf-8") as f:
575 f.write("\n".join(lines) + "\n")
576 
577 
578def main(urls: list[str], base_dir: str = "./instagram-downloads", stitch: bool = True):
579 os.makedirs(base_dir, exist_ok=True)
580 results = []
581 for url in urls:
582 url = url.strip()
583 if not url:
584 continue
585 print(f"Processing: {url}")
586 r = process_url(url, base_dir, stitch)
587 results.append(r)
588 time.sleep(1) # polite delay between requests
589 
590 # Summary
591 ok = [r for r in results if r["status"] == "ok"]
592 err = [r for r in results if r["status"] == "error"]
593 total_files = sum(len(r["files"]) for r in ok)
594 print("\nInstagram Post Downloader — Batch Complete")
595 print("==========================================")
596 print(f"URLs processed: {len(results)}")
597 print(f"Posts saved: {len(ok)}")
598 print(f"Total files: {total_files}")
599 print(f"Errors: {len(err)}")
600 print(f"Output dir: {os.path.abspath(base_dir)}\n")
601 for r in results:
602 if r["status"] == "ok":
603 print(f" OK {r['url']}")
604 else:
605 print(f" ERR {r['url']} — {r['error']}")
606 
607 
608if __name__ == "__main__":
609 if len(sys.argv) < 2:
610 print("Usage: python instagram_downloader.py <url1> [url2] ...")
611 sys.exit(1)
612 main(sys.argv[1:])
613```
614 
615---
616 
617## Quality Checks
618 
619Before marking the task complete, verify each item:
620 
621- [ ] Domain allowlist confirmed — `*.cdninstagram.com` is added before any fetch attempts
622- [ ] All provided URLs validated as Instagram URLs before processing begins
623- [ ] CDN URLs are the highest-resolution variants available (largest `config_width` selected)
624- [ ] Folder name is sanitised — no special characters, no spaces, max 40 chars from caption
625- [ ] Folder collision handled — shortcode appended if folder already exists
626- [ ] Carousel slides numbered sequentially with zero-padding (`slide_01`, `slide_02`, ...)
627- [ ] PDF includes all slides in correct order (not alphabetical — by slide index)
628- [ ] metadata.txt written to every post folder, including full CDN URLs
629- [ ] Pillow dependency checked at startup — graceful fallback if not available
630- [ ] Batch completion summary printed with file counts and any errors
631- [ ] Private post errors caught and reported — not silently skipped
632- [ ] Rate limiting handled — at least 1 second delay between requests
633- [ ] No credential or cookie storage — skill operates on public posts only
634 
635---
636 
637## Anti-Patterns
638 
639- [ ] Do not attempt to download private posts or content behind a login wall — this skill is for public posts only
640- [ ] Do not ignore 429 rate-limit responses — always implement a backoff wait before retrying
641- [ ] Do not save all downloads to a single flat folder when processing multiple accounts — use named subfolders per source
642- [ ] Do not skip PDF stitching for carousel posts — individual slides delivered without a combined PDF are incomplete output
643- [ ] Do not proceed if Instagram returns a login wall — surface the limitation clearly rather than returning an error silently
644 
645## Example Trigger Phrases
646 
647- "Download this Instagram post for me: https://www.instagram.com/p/ABC123/"
648- "Save that carousel to my downloads folder"
649- "Can you grab all the slides from this Instagram post and make a PDF?"
650- "Download these 5 Instagram posts" [followed by list of URLs]
651- "Archive this IG post before it gets deleted"
652- "I need the full-res images from this carousel"
653- "Download the images from this Instagram URL and stitch them into a PDF"
654- "Batch download these Instagram posts" [followed by URLs]
655- "Save the slides from this Instagram carousel as individual JPEGs"
656- "Get me the high-res version of this Instagram image"
657 
658---
659 
660## Notes on Instagram's Anti-Scraping Measures
661 
662Instagram actively changes its page structure and API endpoints. If all three fetch attempts fail:
663 
6641. The embed page method (`/embed/captioned/`) is historically the most stable — start there.
6652. CDN URLs expire. Download immediately after fetching — do not store URLs and download later.
6663. Instagram may return a login wall for some posts even if they're technically public. If this happens, the skill cannot proceed without authentication (which is out of scope).
6674. If Instagram returns a 429, wait 10–30 seconds before retrying. Reduce batch size for large lists.
668 
669This skill is designed for public posts only. It does not support login, sessions, or private content.
670 
671---
672 
673*Originally inspired by a skill from Frank and Diana Dovgopol (Write, Prompt, Scale) — adapted and extended for this library.*
674 

Discussion

Alternatives

Also in Posting & schedulingSee all 364 in Marketing →
Linkedin comment drafterDraft a LinkedIn comment on someone else's post from its URL, or reshare (repost) it to your feed with optional commentary. Use when the user pastes a post URL and asks to comment, engage, be first commenter, or repost with their thoughts. Produces 1-3 variants in the user's voice, picks a reaction, and publishes via Publora on approval. Not for replying to existing comments (use linkedin-reply-handler).Marketing · MITLinkedin content plannerGenerate a 7-day LinkedIn content plan from a theme, audience, and pillars. Produces per-day post pillar, format, hook type, CTA, posting time, daily comment targets, and a weekly inbound-readiness check. Use when the user wants to plan a week or month of content, not draft a single post (use linkedin-post-writer).Marketing · MITLinkedin employee advocacyStand up and run a LinkedIn employee advocacy program for a marketing or sales team. Covers 14-day launch playbook, brand-guideline governance, per-post time budget, cadence benchmarks, and team ROI (reach, engagement, pipeline). Triggers on "employee advocacy", "get the team posting", "scale LinkedIn across team", "advocacy ROI". Not for planning one person's own calendar (use linkedin-content-planner).Marketing · MITInstagram Audience InsightsRead your Instagram niche and profile from real data via Apify, no login. Scan a hashtag for the posts traveling now (likes, comments, owner) to see the format and hook that works. Pull profile stats for any handle, yours or a competitor's: followers, posts, bio, category. Instagram hides who liked or commented on other accounts, so this is discovery plus profiles, not engagers. Triggers on "what works in my niche", "scan the hashtag", "competitor stats". Not for writing captions (use ig-caption-writer).Marketing · MIT