Skip to main content
This script looks for recent meetings in Supersonic, identifies attendees, and creates follow-up tasks for each one. Run it daily on cron, or trigger it from a webhook after each meeting ends.

How it works

  1. Search the timeline for meetings in the last 24 hours.
  2. For each meeting, extract the attendees.
  3. Create a follow-up task linked to each attendee with a due date 2 days out.
  4. Skip meetings that already have follow-up tasks (tracked via a state file).

The script

#!/usr/bin/env python3
"""
Meeting follow-up task creator.
Finds recent meetings, creates follow-up tasks for attendees.

Env vars: SUPERSONIC_API_KEY
State file: ~/.supersonic_meeting_followups.json
"""

import json
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
import httpx

API_URL = "https://mcp.supersonic.cv/api/developers/mcp/call/"
API_KEY = os.environ["SUPERSONIC_API_KEY"]
STATE_FILE = Path.home() / ".supersonic_meeting_followups.json"
FOLLOWUP_DAYS = 2  # due date = meeting date + N days


def api_call(tool: str, params: dict) -> dict:
    resp = httpx.post(
        API_URL,
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={"tool": tool, "params": params},
        timeout=15.0,
    )
    resp.raise_for_status()
    return resp.json()


def load_processed() -> set:
    """Load IDs of meetings we've already processed."""
    if STATE_FILE.exists():
        data = json.loads(STATE_FILE.read_text())
        return set(data.get("processed", []))
    return set()


def save_processed(ids: set):
    STATE_FILE.write_text(json.dumps({"processed": list(ids)}))


def get_recent_meetings(hours: int = 24) -> list[dict]:
    """Search timeline for meetings in the last N hours."""
    since = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()
    data = api_call("timeline.search", {
        "activity_type": "meeting",
        "since": since,
        "limit": 50,
    })
    return data.get("activities", [])


def create_followup_task(
    title: str,
    description: str,
    due_date: str,
    record_id: str | None = None,
):
    """Create a task in Supersonic."""
    params = {
        "title": title,
        "description": description,
        "due_date": due_date,
    }
    if record_id:
        params["record_id"] = record_id

    return api_call("tasks.create", params)


def main():
    processed = load_processed()
    meetings = get_recent_meetings(hours=24)
    print(f"Found {len(meetings)} recent meetings.")

    new_tasks = 0
    for meeting in meetings:
        meeting_id = meeting.get("id")

        if meeting_id in processed:
            continue

        subject = meeting.get("subject") or meeting.get("title") or "Meeting"
        attendees = meeting.get("attendees", [])
        meeting_date = meeting.get("date") or meeting.get("created_at", "")
        record_id = meeting.get("record_id")

        # Calculate due date
        try:
            if meeting_date:
                mt = datetime.fromisoformat(meeting_date.replace("Z", "+00:00"))
            else:
                mt = datetime.now(timezone.utc)
            due = (mt + timedelta(days=FOLLOWUP_DAYS)).strftime("%Y-%m-%d")
        except (ValueError, TypeError):
            due = (datetime.now(timezone.utc) + timedelta(days=FOLLOWUP_DAYS)).strftime("%Y-%m-%d")

        # Build task description
        if attendees:
            attendee_names = [a.get("name") or a.get("email", "Unknown") for a in attendees]
            attendee_list = ", ".join(attendee_names)
            description = f"Follow up after meeting: {subject}\nAttendees: {attendee_list}"
        else:
            description = f"Follow up after meeting: {subject}"

        task_title = f"Follow up: {subject}"

        print(f"  Creating task: {task_title} (due {due})")
        create_followup_task(
            title=task_title,
            description=description,
            due_date=due,
            record_id=record_id,
        )
        new_tasks += 1
        processed.add(meeting_id)

    save_processed(processed)
    print(f"Created {new_tasks} follow-up task(s).")


if __name__ == "__main__":
    main()

Setup

1

Install dependencies

pip install httpx
2

Set environment variables

export SUPERSONIC_API_KEY="supersonic_live_YOUR_KEY"
3

Test it

Run the script once. If you have recent meetings, it will create tasks:
python meeting_followups.py
Verify tasks were created:
npx supersonic-cli tasks list
4

Schedule with cron

Run daily at 6 PM (after the day’s meetings):
0 18 * * * SUPERSONIC_API_KEY=supersonic_live_YOUR_KEY /usr/bin/python3 /path/to/meeting_followups.py >> /var/log/meeting_followups.log 2>&1
Or every 2 hours for faster follow-ups:
0 */2 * * * SUPERSONIC_API_KEY=supersonic_live_YOUR_KEY /usr/bin/python3 /path/to/meeting_followups.py >> /var/log/meeting_followups.log 2>&1

Customizing follow-up rules

Different due dates by meeting type

def get_due_days(subject: str) -> int:
    """Return follow-up days based on meeting type."""
    subject_lower = subject.lower()
    if "demo" in subject_lower:
        return 1  # follow up quickly after demos
    if "review" in subject_lower or "check-in" in subject_lower:
        return 7  # weekly reviews don't need urgent follow-up
    return 2  # default

Create tasks for specific attendees only

Filter attendees to external contacts (skip your own team):
TEAM_DOMAINS = {"yourcompany.com", "supersonic.cv"}

def is_external(attendee: dict) -> bool:
    email = attendee.get("email", "")
    domain = email.split("@")[-1] if "@" in email else ""
    return domain not in TEAM_DOMAINS

# In main(), replace the attendees line:
attendees = [a for a in meeting.get("attendees", []) if is_external(a)]

Add Slack notification when tasks are created

def notify_slack(task_title: str, due: str):
    webhook = os.environ.get("SLACK_WEBHOOK_URL")
    if not webhook:
        return
    httpx.post(webhook, json={
        "text": f"Follow-up task created: *{task_title}* (due {due})"
    }, timeout=10.0)
The state file prevents duplicate tasks. If you delete the state file, the script will reprocess the last 24 hours of meetings and may create duplicate tasks. Keep the state file backed up if this matters.
To change the lookback window, adjust the hours parameter in get_recent_meetings(). If you run the script every 2 hours, set it to hours=3 for some overlap to avoid missing meetings.