Voice AI that runs your team's daily standup. Invite Standy to a calendar event, walk away. Each guest gets a 3-minute 1:1 voice call. The organizer gets the digest emailed when everyone's done.
How it works · Quickstart · Configuration · Architecture · Roadmap
If this saves your team 75 minutes a week, drop a star — that's how I keep building it in the open.
Daily standups cost a team 15 min × 5 days × N people every week. Most of it is everyone listening to status they don't need. The manager wants the summary, not the meeting.
This replaces the meeting with a voice call per person, conducted by an AI agent. Same three questions (yesterday / today / blockers), same accountability, none of the Zoom Tetris.
- Zero new tool to install. You schedule it the way you schedule any other meeting — in Google Calendar, Outlook, or Apple Calendar. The agent receives the
.icsinvite, reads it, and runs the standup at the start time. - No OAuth dance. No "Connect your Google account" step. The agent has its own email address; you just add it as a guest.
- Real voice, not chat. Built on LiveKit Agents + Google Gemini Live for sub-second speech-to-speech latency.
- Bilingual. Each member can be set to English or Arabic; the agent speaks their language end-to-end.
- Self-hosted, two containers. Single Docker Compose file, SQLite on a bind-mounted volume, no managed services required.
┌─────────────────────────┐
│ Manager opens Calendar │
│ → New event, 9 AM │
│ → Adds team + agent │
│ as a guest │
└────────────┬────────────┘
│ (calendar invite by email)
▼
┌─────────────────────────┐ ┌──────────────────┐
│ Agent's mailbox (IMAP) │ ──────► │ Inbox poller │
│ standup@yourdomain │ │ parses .ics │
└─────────────────────────┘ └────────┬─────────┘
▼
┌──────────────────┐
│ CalendarEvent │
│ row in SQLite │
└────────┬─────────┘
│ at start_at, fire one
│ standup per attendee
▼
┌─────────────────────────┐ ┌──────────────────┐
│ Each member opens │ ◄────── │ Per-guest │
│ their join email, │ email │ join link │
│ clicks → 1:1 voice call │ │ (LiveKit room) │
└────────────┬────────────┘ └──────────────────┘
│
▼
┌─────────────────────────┐
│ Agent asks 3 questions │
│yesterday/today/blockers,|
│ digs into vague answers │
│ then says goodbye │
└────────────┬────────────┘
│
│ (after every guest's call wraps)
▼
┌─────────────────────────┐
│ Digest email to the │
│ event organizer │
└─────────────────────────┘
The whole flow is automatic. The dashboard exists for observation (who's done their standup today, what did they say) and light tuning (tone, language, custom instructions for the agent).
You'll need:
- Docker + Docker Compose
- A LiveKit Cloud project (free tier)
- A Google AI Studio API key with Gemini Live access (here)
- A Gmail account with 2-factor auth on (we'll use it for both SMTP outbound and IMAP inbound)
git clone https://github.com/ziadwaelai/standy.git
cd standy
cp .env.example .envFill in .env — at minimum:
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=...
LIVEKIT_API_SECRET=...
GOOGLE_API_KEY=...
SMTP_USER=standup@yourdomain.com
SMTP_PASSWORD=<gmail-app-password> # https://myaccount.google.com/apppasswords
IMAP_USER=standup@yourdomain.com
IMAP_PASSWORD=<gmail-app-password> # same one
INBOUND_EMAIL=standup@yourdomain.com # what managers add as a guest
PUBLIC_APP_URL=http://localhost:8000Then:
docker compose up --build -d
docker compose logs -f apiOpen http://localhost:8000. The dashboard shows the agent's email address with a copy button — paste that as a guest in any calendar event with your team, and watch the logs:
inbox: new event uid=... attendees=4 title='Daily standup'
calendar tick: dispatching 3 standup(s)
event digest sent: uid=... -> manager@yourcompany.com (3 items)
That's the full loop.
| Variable | What it's for |
|---|---|
LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET |
LiveKit Cloud project. Free tier is enough for a small team. |
LIVEKIT_AGENT_NAME |
The worker's registered name. Default standup-agent — keep unless you run multiple agents. |
GOOGLE_API_KEY |
Gemini API key. Must be on a project with Live access. |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD |
Outbound: join links + digest emails. Gmail App Password works. |
IMAP_HOST, IMAP_USER, IMAP_PASSWORD |
Inbound: where calendar invites land. Usually the same Gmail account as SMTP. |
INBOUND_EMAIL |
The address managers add as a guest. Defaults to IMAP_USER if blank. |
PUBLIC_APP_URL |
Public URL of the dashboard. Must be reachable from the email recipients' browsers — for hosted deployments, set this to your https://... domain. |
| Variable | Default | Effect |
|---|---|---|
INBOX_POLL_SECONDS |
30 |
How often the IMAP poller checks for new invites. |
IMAP_MAILBOX |
INBOX |
Mailbox to scan. |
DATABASE_URL |
sqlite:////app/data/standup.db |
Switch to postgresql+psycopg://... for Postgres. |
API_PORT |
8000 |
Host port mapped to the api container. |
| Field | Default | Effect |
|---|---|---|
| Team name | My Team |
Used in email subject lines and the agent's opening line. |
| Tone | friendly-concise |
Free-form prompt input. Try formal, playful, etc. |
| Follow-ups / topic | 2 |
Cap on how many follow-up questions the agent will ask per topic. |
| Max session (sec) | 600 |
Hard cap on call length — agent ends the call if it overruns. |
| Custom instructions | empty | Extra context for the agent. Example: "Ask specifically about the OAuth migration this week." |
- Active toggle — inactive members are skipped when their email shows up in an invite.
- Language —
Englishorالعربية. The agent speaks the chosen language end-to-end (greeting, follow-ups, summary). - Members are auto-created the first time their email appears in a calendar invite. You don't add them by hand.
Two long-running processes, one shared SQLite database:
docker-compose.yml
├─ api → FastAPI + Jinja2 dashboard, APScheduler, IMAP poller, digest mailer
└─ agent → LiveKit Agents worker (registers with LiveKit Cloud, waits for dispatches)
src/standup/
├─ core/ pure domain logic (no FastAPI, no LiveKit)
│ ├─ models.py Member · AgentConfig · StandupSession · CalendarEvent
│ ├─ db.py SQLite engine, WAL pragmas, additive migrations + data backfills
│ ├─ ics.py RFC 5545 parser (RRULE expansion via recurring_ical_events)
│ ├─ inbox.py IMAP poller, UID-cursor based (immune to \Seen flips)
│ ├─ email_util.py SMTP via aiosmtplib + Jinja2 email templates
│ ├─ livekit_util.py token mint + agent dispatch
│ ├─ time_util.py tz coercion + duration humanizers
│ └─ config.py pydantic-settings
│
├─ api/ HTTP surface
│ ├─ main.py FastAPI app, lifespan, static mount
│ ├─ routes.py dashboard, member CRUD, /join/{id}, history
│ └─ scheduler.py APScheduler jobs: inbox poll, calendar tick, event completion
│
├─ agent/ LiveKit worker (separate process)
│ ├─ main.py job entrypoint — wait_for_participant, AEC warm-up, greeting
│ ├─ standup_agent.py the Agent subclass + function tools (mark_topic_done, end_call)
│ ├─ prompts.py bilingual system prompt + summarizer prompt
│ └─ finalize.py shutdown hook — Gemini summarizer, transcript persistence
│
└─ web/ templates + static assets (vanilla JS, no build step)
- Inbox poll (every 30s, configurable) — IMAP
UID SEARCH UID >cursor, parses any.ics, upsertsCalendarEventrows. Tracks UIDs so it survives the manager opening the invite in Gmail (which would flip\Seen). - Calendar tick (every 30s) — for events whose
start_at ∈ [now-15min, now+45s]and not yet processed: dispatches a 1:1 standup per attendee (skipping the agent itself, the organizer, and DECLINED guests). Auto-creates Member rows for new emails. - Event completion (every 60s) — for events past
end_at: flips any pending sessions tomissed, then once every session is terminal, emails the digest to the organizer.
Four tables:
- Member — name, email, role, language (
en/ar), active flag. - AgentConfig — single-row team settings + IMAP UID cursor.
- StandupSession — one per member-call, with status (
scheduled/notified/joined/completed/missed/errored), transcript JSON, summary markdown. - CalendarEvent — parsed from .ics.
last_processed_atclaims an event for dispatch;digest_sent_atclaims it for the digest. Recurring events expand to one row per occurrence over a 60-day horizon.
- Python 3.11 · FastAPI · SQLModel · APScheduler · pydantic-settings
- LiveKit Agents +
livekit-plugins-google(Gemini Live,gemini-2.5-flash-native-audio-latest) - Silero VAD for endpointing
- Gemini 2.5 Flash for the summarizer (separate from the live model)
- icalendar + recurring_ical_events for invite parsing
- aiosmtplib for outbound, stdlib imaplib for inbound
- Jinja2 + vanilla JS — no build step, no framework
No managed dependencies beyond LiveKit Cloud and Gemini.
Things that are deliberately not done so this stays an MVP:
- Bearer-token auth on the dashboard (currently wide open — gate before exposing publicly)
- Multi-tenant (one agent address per team, currently single-team)
- Postmark/Mailgun inbound webhook (faster than IMAP polling at scale)
- Slack / Teams digest delivery in addition to email
- Postgres support for hosted deployments (SQLite is fine for one team)
- OpenAI Realtime as an alternative to Gemini Live (one-line provider swap)
- CI + a real test suite (currently only the prompt-compose unit test)
PRs welcome on any of the above — or open an issue first if you want to discuss the shape.
python -m venv .venv && source .venv/bin/activate
pip install -e .[dev]
pytest # runs the existing tests
# In two terminals for live development:
uvicorn standup.api.main:app --reload --port 8000
python -m standup.agent.main dev # agent worker with hot reloadTests, screenshots, and English-language proofreading are especially welcome contributions. The bilingual prompts in src/standup/agent/prompts.py are the spot for native Arabic speakers to suggest improvements.
MIT — see LICENSE.
If this is useful, a star helps it find the next person who needs it. Thanks for reading.
