Community Workflows

News Carousel -> Instagram

Composio SearchComposio SearchInstagramInstagram

Generic end-to-end news carousel: user picks the topic on first run, searches and validates 4 real-world images, renders 4 clean 1024x1024 slides with headline text, uploads and publishes as an Instagram carousel.

How To

Run the workflow and tell it your topic (e.g. "climate", "geopolitics", "tech", "sports", "ai"). It will find the top daily story, pull 4 real-world images, render clean headline slides, and post the carousel to Instagram.

Example Results:

Revolutionized Manufacturing
GM Deploys AI For Design

Example Run:

View conversation here

If you are happy with the results, tell your agent to schedule it daily, or few times a day. It will automatically post news carousels on your instagram.

Requires your permissions to post to Instagram. Select "Always allow" once you are happy with the results so schedules can run without interruptions.

Instructions

Workflow: News Carousel → Instagram

Rules

  • Only real-world photography for image queries.
  • If this is a scheduled run, check bound note to avoid repeating stories from previous runs.

Step 0: Get Topic

If this is a scheduled run, check the schedule notes for the topic. When user asks you to create a schedule for this workflow, you should mention it in schedule instructions so this step can be skipped. On regular runs, always ask the user about the topic unless they want it to be hardcoded.

If that line exists, use it as the topic and skip to Step 1.

If it does not exist, ask the user:

"What topic should this carousel cover? (e.g. climate, geopolitics, tech, sports, science…)"


Step 1: Research Latest Developments on TOPIC

Use COMPOSIO_MULTI_EXECUTE_TOOL.

If TOPIC is current events/news-heavy (e.g., geopolitics, sports, tech, climate):
Use COMPOSIO_SEARCH_NEWS.
Query: latest TOPIC news <today's date> most important developments

If TOPIC is more general/evergreen:
Use COMPOSIO_SEARCH_WEB.
Query: latest TOPIC news <today's date> most important developments

Pick the single highest-signal story not in the avoid list. Extract for all 4 slides:

  • Slide 1 — Headline hook: line1 (max 55 chars), line2 (max 55 chars)
  • Slide 2 — Key detail or fact
  • Slide 3 — Real-world impact or context
  • Slide 4 — Takeaway / what's next
  • Caption — Tweet-style, max 2200 chars, max 5 hashtags

Step 2: Search for Real-World Imagery

Use COMPOSIO_SEARCH_IMAGE via COMPOSIO_MULTI_EXECUTE_TOOL. Pass session_id: "pure".

json
{
  "tools": [{
    "tool_slug": "COMPOSIO_SEARCH_IMAGE",
    "arguments": { "num": 15, "query": "<2-3 real-world keywords from the story>" }
  }],
  "sync_response_to_workbench": true
}

Step 3: Validate & Download 4 Distinct Images

Use COMPOSIO_REMOTE_WORKBENCHsession_id: "pure".

python
import json, requests, os

file_data = json.load(open("/home/user/.composio/mex/<filename>.json"))
images = file_data['results'][0]['response']['data']['images_results']

candidates = [img for img in images if img.get('original') and
             img.get('original_width', 0) >= 600 and
             img.get('original_height', 0) >= 400]

seen = set()
unique = []
for img in candidates:
    src = img.get('source', '').lower()
    if src not in seen:
        seen.add(src); unique.append(img)
candidates = unique[:4]

downloaded = []
for i, img in enumerate(candidates, 1):
    url = img['original']
    try:
        r = requests.get(url, stream=True, timeout=10, headers={"User-Agent": "Mozilla/5.0"})
        ct = r.headers.get('Content-Type', '')
        if r.status_code == 200 and 'image' in ct.lower():
            ext = '.jpg' if 'jpeg' in ct else '.png' if 'png' in ct else '.webp'
            path = f"/home/user/slide_{i}{ext}"
            with open(path, 'wb') as f:
                for chunk in r.iter_content(8192): f.write(chunk)
            downloaded.append({"path": path, "url": url})
            print(f"OK slide_{i}: {url}")
        else:
            print(f"FAIL slide_{i}: status={r.status_code} ct={ct}")
    except Exception as e:
        print(f"ERR slide_{i}: {e}")

print(f"\nDownloaded {len(downloaded)}/4 images")

Require at least 4 images. Fail clearly if fewer than 2 downloaded.


Step 4: Install Rendering Dependencies

Use COMPOSIO_REMOTE_WORKBENCHsession_id: "pure".

python
import subprocess, sys
subprocess.run([sys.executable, "-m", "pip", "install", "pyppeteer", "nest_asyncio", "-q"])
try:
    from pyppeteer.chromium_downloader import download_chromium
    download_chromium()
except Exception as e:
    print(f"Chromium: {e}")
print("Ready.")

Step 5: Render All Slides

Use COMPOSIO_REMOTE_WORKBENCHsession_id: "pure". Fill in slide text from Step 1.

  • Title case for all text — no all caps
  • Sublines should be full, descriptive sentences (not fragments)
  • Font size: line1 = 44px bold, line2 = 28px italic at 0.73 opacity
python
import asyncio, base64, nest_asyncio, os
nest_asyncio.apply()
from pyppeteer import launch

slides = [
    {"image": "/home/user/slide_1.jpg", "line1": "SLIDE_1_LINE1", "line2": "SLIDE_1_LINE2", "out": "/home/user/carousel_1.png"},
    {"image": "/home/user/slide_2.jpg", "line1": "SLIDE_2_LINE1", "line2": "SLIDE_2_LINE2", "out": "/home/user/carousel_2.png"},
    {"image": "/home/user/slide_3.jpg", "line1": "SLIDE_3_LINE1", "line2": "SLIDE_3_LINE2", "out": "/home/user/carousel_3.png"},
    {"image": "/home/user/slide_4.jpg", "line1": "SLIDE_4_LINE1", "line2": "SLIDE_4_LINE2", "out": "/home/user/carousel_4.png"},
]
slides = [s for s in slides if os.path.exists(s["image"])]

async def render_slide(slide):
    ext = os.path.splitext(slide["image"])[1].lower()
    mime = "image/webp" if ext == ".webp" else "image/jpeg" if ext in [".jpg",".jpeg"] else "image/png"
    with open(slide["image"], "rb") as f:
        b64 = base64.b64encode(f.read()).decode()
    img_src = f"data:{mime};base64,{b64}"
    html = f"""<!DOCTYPE html>
<html><body style="margin:0;padding:0;background-color:#000;">
<div style="width:1024px;height:1024px;background-color:#000;display:flex;flex-direction:column;justify-content:flex-start;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
  <div style="padding:36px 60px 20px 60px;">
    <div style="color:#fff;font-size:44px;font-weight:700;line-height:1.2;letter-spacing:-0.5px;">{slide['line1']}</div>
    <div style="color:#ffffffbb;font-size:28px;font-weight:400;line-height:1.4;font-style:italic;margin-top:10px;">{slide['line2']}</div>
  </div>
  <div style="flex:1;padding:0 40px 40px 40px;box-sizing:border-box;overflow:hidden;">
    <img src="{img_src}" style="width:100%;height:100%;object-fit:cover;border-radius:12px;display:block;">
  </div>
</div>
</body></html>"""
    browser = await launch(args=['--no-sandbox','--disable-setuid-sandbox','--disable-dev-shm-usage'])
    page = await browser.newPage()
    await page.setViewport({'width':1024,'height':1024})
    await page.setContent(html)
    await asyncio.sleep(3)
    await page.screenshot({'path': slide['out'], 'clip':{'x':0,'y':0,'width':1024,'height':1024}})
    await browser.close()
    print(f"Rendered {slide['out']} ({os.path.getsize(slide['out'])} bytes)")

async def main():
    for s in slides: await render_slide(s)

asyncio.get_event_loop().run_until_complete(main())
print("All slides rendered.")

Step 6: Upload Slides to S3

Use COMPOSIO_REMOTE_WORKBENCHsession_id: "pure".

python
import json, os
slide_files = []
for i in range(1, 5):
    path = f"/home/user/carousel_{i}.png"
    if not os.path.exists(path): continue
    result, error = upload_local_file(path)
    if error:
        print(f"slide_{i} error: {error}")
    else:
        s3key = result.get("s3key")
        print(f"slide_{i} s3key: {s3key}")
        slide_files.append({"name": f"carousel_{i}.jpg", "mimetype": "image/jpeg", "s3key": s3key})
print(f"\nUploaded {len(slide_files)} slides")
print(json.dumps(slide_files))

Step 7: Resolve Instagram User ID

Check the bound note for a line that reads:

IG_USER_ID: <value>

If found, use it directly as ig_user_id and skip to Step 7a.

If not found, fetch it via INSTAGRAM_GET_USER_PAGES. Then write it to the bound note (manageNotes update on a68a8c5d-391e-42b1-bfc3-e3a088a9683f) by appending:

IG_USER_ID: <fetched_id>

Use this value as ig_user_id for the rest of this run.

7a: Create individual carousel item containers

Use COMPOSIO_MULTI_EXECUTE_TOOLsession_id: "pure", all 4 in parallel.

7b: Create parent carousel container

Use INSTAGRAM_CREATE_CAROUSEL_CONTAINER with the caption from Step 1 and the 4 child IDs. Store returned id as creation_id.


Step 8: Publish the Carousel

Use INSTAGRAM_POST_IG_USER_MEDIA_PUBLISH with creation_id from Step 7b and max_wait_seconds: 90.


Step 9: Update Bound Note

Use manageNotes update on a68a8c5d-391e-42b1-bfc3-e3a088a9683f:

  • ## Last run: <timestamp>
  • Topic + story covered + media ID
  • Add story to avoid list
  • Recommended next angles

Step 10: Report

Notify user via sendNotification:

  • Topic + story + all 4 slide texts
  • Instagram media ID
  • Any skipped slides

Available Connectors

composio_search, instagram