> ## Documentation Index
> Fetch the complete documentation index at: https://docs.supersonic.cv/llms.txt
> Use this file to discover all available pages before exploring further.

# Meeting follow-up tasks

> A Python script that checks for recent meetings, extracts attendees, and creates follow-up tasks with due dates.

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

```python theme={null}
#!/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

<Steps>
  <Step title="Install dependencies">
    ```bash theme={null}
    pip install httpx
    ```
  </Step>

  <Step title="Set environment variables">
    ```bash theme={null}
    export SUPERSONIC_API_KEY="supersonic_live_YOUR_KEY"
    ```
  </Step>

  <Step title="Test it">
    Run the script once. If you have recent meetings, it will create tasks:

    ```bash theme={null}
    python meeting_followups.py
    ```

    Verify tasks were created:

    ```bash theme={null}
    npx supersonic-cli tasks list
    ```
  </Step>

  <Step title="Schedule with cron">
    Run daily at 6 PM (after the day's meetings):

    ```cron theme={null}
    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:

    ```cron theme={null}
    0 */2 * * * SUPERSONIC_API_KEY=supersonic_live_YOUR_KEY /usr/bin/python3 /path/to/meeting_followups.py >> /var/log/meeting_followups.log 2>&1
    ```
  </Step>
</Steps>

## Customizing follow-up rules

### Different due dates by meeting type

```python theme={null}
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):

```python theme={null}
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

```python theme={null}
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)
```

<Note>
  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.
</Note>

<Tip>
  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.
</Tip>
