News Carousel -> Instagram
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:


Example Run:
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_WORKBENCH — session_id: "pure".
pythonimport 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_WORKBENCH — session_id: "pure".
pythonimport 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_WORKBENCH — session_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
pythonimport 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_WORKBENCH — session_id: "pure".
pythonimport 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_TOOL — session_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