A Slack /newintro command that books coffee-chat and buddy intro meetings for new starters β serverless, on two AWS Lambdas.
Features β’ Architecture β’ Quick Start β’ Configuration β’ Usage β’ Contributing
| Feature | What it does | |
|---|---|---|
| β‘ | /newintro slash command |
Opens a Slack modal to book Coffee βοΈ or Buddy π€ intros for a list of emails |
| πͺͺ | Azure AD group sync | Pulls the pool of intro partners from an Azure AD group and prunes departed users |
| βοΈ | Weighted partner picking | DynamoDB tracks how often each partner has been used and favours the least-used |
| π | Calendar-aware booking | Google Calendar FreeBusy search finds a free 15-minute slot (11:00β15:00, weekdays) |
| π | Business-day cadence | Spaces each person's meetings two business days apart, skipping weekends |
| π¬ | Live status updates | Posts booking progress, per-meeting confirmations, and a final summary to the channel |
| β‘ | Serverless | Two AWS Lambdas (ARM64/Graviton) + a shared dependency layer β nothing to host |
| Requirement | Notes |
|---|---|
| Python | 3.13 (matches the Lambda runtime) |
| AWS account | Lambda, Secrets Manager, DynamoDB, CloudWatch |
| Slack app | Bot token, signing secret, and a /newintro slash command |
| Azure AD app | Client credentials with group read access |
| Google service account | Domain-wide delegation for Calendar FreeBusy + event creation |
- Slash command
/newintropointing at the UI Lambda's URL - Bot token scopes:
commands,chat:write
Two Lambdas: a thin UI Lambda that answers Slack within its 3-second window, and a Worker Lambda that does the slow booking work asynchronously.
flowchart LR
U["π€ Slack user"] -->|"/newintro + modal"| UI["ποΈ UI Lambda<br/>src/ui_lambda/ui_entry.py"]
UI -->|"async invoke"| W["βοΈ Worker Lambda<br/>src/worker_lambda/worker_entry.py"]
SM["π Secrets Manager<br/>CONFIG_SECRET"] -.-> UI
SM -.-> W
W --> AAD["πͺͺ Azure AD<br/>group sync"]
W --> DDB["ποΈ DynamoDB<br/>partner weights"]
W --> GC["π
Google Calendar<br/>FreeBusy + events"]
W -->|"status messages"| CH["π¬ Slack channel"]
The worker, per email: syncs the Azure AD group into DynamoDB, picks the least-used available partner, searches Google Calendar for a free 15-minute slot, creates the event with both attendees, bumps the partner's weight, and posts the confirmation to Slack.
git clone https://github.com/CaputoDavide93/New-Starters-Meetup.git
cd New-Starters-MeetupThe dependency layer is not committed β build it locally:
pip install -r Layer/requirements.txt --target Layer/python/./scripts/build.shscripts/build.sh does three things:
- Copies
src/common/*.pyintoLayer/python/intro_common/(the shared code ships inside the layer as theintro_commonpackage) - Copies the two entry files into
deploy/ui-lambda/anddeploy/worker-lambda/staging folders - Zips everything into
dist/:ui-lambda.zip,worker-lambda.zip, andlayer-python313-arm64.zip
# Publish the shared layer
aws lambda publish-layer-version \
--layer-name newstarters-deps \
--compatible-runtimes python3.13 \
--compatible-architectures arm64 \
--zip-file fileb://dist/layer-python313-arm64.zip
# Update the two functions (create them first with your preferred tooling)
aws lambda update-function-code --function-name intro-ui-lambda \
--zip-file fileb://dist/ui-lambda.zip
aws lambda update-function-code --function-name intro-worker-lambda \
--zip-file fileb://dist/worker-lambda.zipHandlers: ui_entry.lambda_handler (UI) and worker_entry.lambda_handler (worker). Both functions need the layer attached and the environment variables below; the worker needs a generous timeout (it stops itself safely near the 15-minute Lambda cap).
| Variable | Lambda | Required | Description |
|---|---|---|---|
CONFIG_SECRET |
both | β | ARN of the AWS Secrets Manager secret holding the JSON config |
WORKER_FUNCTION_NAME |
UI | β | Name of the worker Lambda to invoke asynchronously |
LOG_LEVEL |
worker | β | Python log level (default INFO) |
See .env.example for the full template.
All application config lives in one JSON secret (CONFIG_SECRET), loaded once at cold start by src/common/config.py:
| Key | Required | Description |
|---|---|---|
slack_bot_token |
β | Slack bot OAuth token (xoxb-β¦) |
slack_signing_secret |
β | Slack app signing secret |
slack_trigger_channel |
β | Fallback channel ID for status messages |
azure_tenant_id |
β | Azure AD tenant (coffee intros) |
azure_client_id |
β | Azure AD app client ID |
azure_client_secret |
β | Azure AD app client secret |
azure_group_id |
β | Azure AD group holding coffee-intro partners |
buddy_azure_tenant_id |
β | Buddy-intro override (falls back to the coffee credentials) |
buddy_azure_client_id |
β | Buddy-intro override |
buddy_azure_client_secret |
β | Buddy-intro override |
buddy_azure_group_id |
β | Azure AD group holding buddy-intro partners |
google_service_account_key |
β | Google service-account key, as a JSON string |
google_delegated_user |
β | Workspace user the service account impersonates |
google_calendar_id |
β | Calendar the events are created on |
dynamodb_table_name |
β | Partner-weight table (coffee) |
buddy_dynamodb_table_name |
β | Partner-weight table (buddy) |
meeting_title_template |
β | Event title template, e.g. βοΈ Coffee: {person1} & {person2} |
meeting_description_template |
β | Event description template |
buddy_meeting_title_template |
β | Buddy event title template |
buddy_meeting_description_template |
β | Buddy event description template |
Templates accept {person1}/{person2} (display names) and {email1}/{email2} placeholders; unknown placeholders fall back to a safe default title.
- In Slack, run
/newintroin any channel the bot can post to. - Fill in the modal:
- Which type of intro? β βοΈ Coffee or π€ Buddy
- Participant emails β comma-separated list of new starters
- Start date β first day to search for slots
- Meetings per person β how many intros to book for each email
- Submit. The UI Lambda acknowledges instantly and hands off to the worker, which posts progress to the channel as it books:
β Booking Coffeeβ¦
β
Jane.Doe β John.Smith β 14 Jul 11:00
β
Booking complete! 2 succeeded, 0 failed.
scripts/cleanup_db.py merges duplicate user records caused by email case mismatches:
python scripts/cleanup_db.py --table intro-weights --dry-run # preview
python scripts/cleanup_db.py --table intro-weights --apply # applyNew-Starters-Meetup/
βββ src/
β βββ common/ # π§ shared code, shipped in the layer as `intro_common`
β β βββ config.py # π Secrets Manager loader
β β βββ azure_sync.py # πͺͺ Azure AD group sync
β β βββ calendar_utils.py # π
Google Calendar FreeBusy + events
β β βββ dynamo_utils.py # ποΈ DynamoDB weight management
β βββ ui_lambda/
β β βββ ui_entry.py # ποΈ Slack slash command + modal handler
β βββ worker_lambda/
β βββ worker_entry.py # βοΈ booking engine
βββ Layer/
β βββ requirements.txt # π¦ layer deps (install into Layer/python/, not committed)
βββ scripts/
β βββ build.sh # π§ builds the three deployment ZIPs into dist/
β βββ cleanup_db.py # π§Ή DynamoDB duplicate-user cleanup
βββ .env.example # βοΈ Lambda environment template
deploy/ and dist/ are build staging/output folders created by scripts/build.sh β they are gitignored, never edited by hand.
# Replace with your function names
aws logs tail /aws/lambda/intro-ui-lambda --follow
aws logs tail /aws/lambda/intro-worker-lambda --follow| Issue | Solution |
|---|---|
Slack API not_authed |
Verify your bot token with curl -X POST https://slack.com/api/auth.test -H "Authorization: Bearer $SLACK_BOT_TOKEN" |
| "No partner available" | Check the Azure AD group sync and the DynamoDB table contents |
FreeBusy notFound errors |
The user's Google Calendar is not accessible. Ensure the service account has domain-wide delegation and the user has a Google Workspace account. Users whose calendars error are skipped as partners and logged as warnings |
| "Signature mismatch" | Verify slack_signing_secret in the config secret |
| Timeout warnings in the channel | The worker warns near the 15-minute Lambda cap and stops early β reduce the email list or meetings per person |
| "Permission denied" | Check the Lambdas' IAM roles (Secrets Manager read, DynamoDB read/write, lambda:InvokeFunction for the UI role) |
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'feat: add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please see SECURITY.md for reporting vulnerabilities.
This project is licensed under the MIT License β see the LICENSE file for details.
β If this tool helped you, please give it a star! ββΒ·βMade with β€οΈ by Davide Caputo