No Description

Cameron Pfiffer c1bc16dfdd Fix View button syntax error and handle undefined messages 3 months ago
cli 7055dc181d Remove redundant api_key from request body 5 months ago
.gitignore e9c84b7c48 Remove deprecated backend logic files 3 months ago
README.md 36fadf8e1f Migrate to Letta's native scheduling API 3 months ago
app.py cb193bb429 Fix Modal deprecation warning: keep_warm → min_containers 3 months ago
dashboard.html c1bc16dfdd Fix View button syntax error and handle undefined messages 3 months ago
pytest.ini 8db4c875ad Add comprehensive test suite with 43 tests 5 months ago
requirements-test.txt 8db4c875ad Add comprehensive test suite with 43 tests 5 months ago
requirements.txt 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
setup_encryption.sh 171ece0b34 Rename project from letta-schedules to letta-switchboard 5 months ago
test-lines.bash 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
test_api.sh 7055dc181d Remove redundant api_key from request body 5 months ago

README.md

Letta Scheduling Dashboard

⚠️ Important Update: Now Using Letta's Native Scheduling

Letta now provides built-in scheduling! This project has been simplified to a web dashboard that calls Letta's API directly.

What Changed?

  • Dashboard Updated - Now calls Letta API directly from your browser
  • Old API Deprecated - Backend scheduling endpoints return 410 Gone
  • ➡️ Migration Required - Use Letta's native scheduling API

Quick Links

🎛️ Web Dashboard: https://letta--switchboard-api.modal.run/dashboard
📖 Letta Scheduling Docs: https://docs.letta.com/guides/agents/scheduling/
💻 GitHub: https://github.com/cpfiffer/letta-switchboard


Migration Guide

For Dashboard Users

The dashboard now calls Letta's API directly! Just continue using it - no changes needed.

  1. Visit https://letta--switchboard-api.modal.run/dashboard
  2. Enter your Letta API key and agent ID
  3. Create schedules as before

For API Users

If you were calling the old Switchboard API, you need to migrate to Letta's native API:

Old Endpoint (Deprecated):

POST https://letta--switchboard-api.modal.run/schedules/one-time
{
  "agent_id": "agent-xxx",
  "execute_at": "2025-11-13T09:00:00Z",
  "message": "Hello",
  "role": "user"
}

New Endpoint (Use This):

POST https://api.letta.com/v1/agents/agent-xxx/schedule
{
  "schedule": {
    "type": "one-time",
    "scheduled_at": 1731499200000
  },
  "messages": [{
    "role": "user",
    "content": "Hello"
  }]
}

Key Changes:

  • execute_at (ISO string) → scheduled_at (Unix milliseconds)
  • message (string) → messages (array of message objects)
  • Cron expressions unchanged (still 5-field format)

Full Documentation: https://docs.letta.com/guides/agents/scheduling/


Quick Start

Use the Web Dashboard

Visit https://letta--switchboard-api.modal.run/dashboard

  1. Enter your Letta API key (stored in browser session only)
  2. Enter your agent ID
  3. View, create, and delete schedules
  4. Dashboard calls Letta's API directly from your browser

Features:

  • Direct Letta API integration - No backend needed
  • Timezone support - Local browser time, UTC, or major timezones
  • Simple interface - Clean, easy-to-use dashboard
  • No installation - Just visit the URL!

Use Letta's API Directly

For programmatic access, use Letta's scheduling API:

Create a one-time schedule:

curl -X POST https://api.letta.com/v1/agents/agent-xxx/schedule \
  -H "Authorization: Bearer YOUR_LETTA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schedule": {
      "type": "one-time",
      "scheduled_at": 1731499200000
    },
    "messages": [{
      "role": "user",
      "content": "Hello!"
    }]
  }'

Create a recurring schedule:

curl -X POST https://api.letta.com/v1/agents/agent-xxx/schedule \
  -H "Authorization: Bearer YOUR_LETTA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schedule": {
      "type": "recurring",
      "cron_expression": "0 9 * * 1-5"
    },
    "messages": [{
      "role": "user",
      "content": "Daily standup"
    }]
  }'

List schedules:

curl https://api.letta.com/v1/agents/agent-xxx/schedule \
  -H "Authorization: Bearer YOUR_LETTA_API_KEY"

Delete a schedule:

curl -X DELETE https://api.letta.com/v1/agents/agent-xxx/schedule/SCHEDULE_ID \
  -H "Authorization: Bearer YOUR_LETTA_API_KEY"

Full Documentation: https://docs.letta.com/guides/agents/scheduling/

What is This?

A simple web dashboard for managing Letta agent schedules. The dashboard calls Letta's native scheduling API directly from your browser.

Features

Web Dashboard - Clean interface for managing schedules
Direct Letta Integration - Calls Letta API from browser
No Backend - Pure client-side (except for hosting the HTML)
Timezone Support - Schedule in local time or UTC
Free to Use - Just visit the URL

Why Use This Dashboard?

Easy to Use
Simple interface for creating and managing Letta schedules without writing API calls.

No Installation
Just visit the URL - no setup, no configuration, no CLI to install.

Direct Letta API
Your schedules are stored and executed by Letta Cloud. Check execution history at https://app.letta.com

How It Works

  1. Visit the dashboardhttps://letta--switchboard-api.modal.run/dashboard
  2. Enter credentials → Your Letta API key and agent ID
  3. Create schedules → Dashboard calls Letta API from your browser
  4. Letta executes → Letta Cloud handles scheduling and execution
  5. Check results → View execution history at https://app.letta.com

Architecture:

  • Dashboard is pure client-side JavaScript
  • API calls go directly to Letta (https://api.letta.com)
  • No Switchboard backend involved in scheduling
  • Your API key never leaves your browser (stored in session storage)

Natural Language Examples

One-Time Messages

# Relative time
--execute-at "in 5 minutes"
--execute-at "in 2 hours"
--execute-at "in 3 days"

# Tomorrow
--execute-at "tomorrow at 9am"
--execute-at "tomorrow at 14:30"

# Next weekday
--execute-at "next monday at 3pm"
--execute-at "next friday at 10:00"

# ISO 8601 (still works)
--execute-at "2025-11-12T19:30:00Z"

Recurring Schedules

# Minutes
--cron "every 5 minutes"
--cron "every 30 minutes"

# Hourly/Daily
--cron "every hour"
--cron "daily at 9am"
--cron "daily at 14:30"

# Weekdays
--cron "every monday"
--cron "every friday at 3pm"
--cron "every weekday"     # Mon-Fri at 9am

# Traditional cron (still works)
--cron "*/5 * * * *"       # Every 5 minutes

This will test all endpoints (create, list, get, delete) for both recurring and one-time schedules.

**Features:**
- Validates API key before running
- Shows configuration at startup
- Tests create, list, get, and delete operations
- Pretty prints all responses

### Bash Test Script

```bash
./test_api.sh

Same functionality using curl commands.

Example with inline variables:

LETTA_API_KEY=sk-xxx LETTA_AGENT_ID=agent-yyy python test_api.py

CLI Usage (Recommended)

The easiest way to interact with letta-switchboard is via the CLI:

# Send a message immediately
letta-switchboard send --agent-id agent-xxx --message "Hello!"

# Send a message later
letta-switchboard send --agent-id agent-xxx --message "Reminder" --execute-at "tomorrow at 9am"

# Create recurring schedule
letta-switchboard recurring create --agent-id agent-xxx --message "Daily standup" --cron "every weekday at 9am"

# List schedules
letta-switchboard onetime list
letta-switchboard recurring list

# View results
letta-switchboard results list

See CLI Documentation for installation and full usage guide.

API Usage

Base URL: https://letta--schedules-api.modal.run

Authentication

All endpoints require Bearer token authentication using your Letta API key:

curl -H "Authorization: Bearer your-letta-api-key" https://your-modal-app.modal.run/schedules/recurring

Security Model:

  • API Key Validation: All requests validate your API key against Letta's API (lightweight list agents call with limit=1)
  • Create endpoints: Verify API key is valid before creating schedule
  • List endpoints: Returns only schedules created with your API key
  • Get/Delete endpoints: Returns 403 Forbidden if the schedule wasn't created with your API key
  • Privacy: API keys are never returned in responses, only used for authentication and execution

Error Codes:

  • 401 Unauthorized: Invalid or expired Letta API key
  • 403 Forbidden: Valid API key, but trying to access someone else's schedule
  • 404 Not Found: Schedule doesn't exist

Create Recurring Schedule

Schedule a message to be sent on a cron schedule:

curl -X POST https://your-modal-app.modal.run/schedules/recurring \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agent-123",
    "api_key": "your-letta-api-key",
    "cron": "0 9 * * *",
    "message": "Good morning! Time for your daily check-in.",
    "role": "user"
  }'

Cron Format: minute hour day month day_of_week

  • 0 9 * * * - Every day at 9:00 AM
  • */15 * * * * - Every 15 minutes
  • 0 */2 * * * - Every 2 hours
  • 0 0 * * 0 - Every Sunday at midnight

Create One-Time Schedule

Schedule a message for a specific time:

curl -X POST https://your-modal-app.modal.run/schedules/one-time \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agent-123",
    "api_key": "your-letta-api-key",
    "execute_at": "2025-11-07T14:30:00-05:00",
    "message": "Reminder: Meeting in 30 minutes",
    "role": "user"
  }'

Timestamp Format: ISO 8601 with timezone

  • 2025-11-07T14:30:00-05:00 (EST)
  • 2025-11-07T14:30:00Z (UTC)

List All Schedules

# List your recurring schedules
curl -H "Authorization: Bearer your-letta-api-key" \
  https://your-modal-app.modal.run/schedules/recurring

# List your one-time schedules
curl -H "Authorization: Bearer your-letta-api-key" \
  https://your-modal-app.modal.run/schedules/one-time

Get Specific Schedule

# Get recurring schedule
curl -H "Authorization: Bearer your-letta-api-key" \
  https://your-modal-app.modal.run/schedules/recurring/{schedule_id}

# Get one-time schedule
curl -H "Authorization: Bearer your-letta-api-key" \
  https://your-modal-app.modal.run/schedules/one-time/{schedule_id}

Delete Schedule

# Delete recurring schedule
curl -X DELETE -H "Authorization: Bearer your-letta-api-key" \
  https://your-modal-app.modal.run/schedules/recurring/{schedule_id}

# Delete one-time schedule
curl -X DELETE -H "Authorization: Bearer your-letta-api-key" \
  https://your-modal-app.modal.run/schedules/one-time/{schedule_id}

Get Execution Results

# List all execution results
curl -H "Authorization: Bearer your-letta-api-key" \
  https://your-modal-app.modal.run/results

# Get result for specific schedule
curl -H "Authorization: Bearer your-letta-api-key" \
  https://your-modal-app.modal.run/results/{schedule_id}

Result Format:

{
  "schedule_id": "uuid",
  "schedule_type": "recurring",
  "run_id": "run_abc123",
  "agent_id": "agent-123",
  "message": "The scheduled message",
  "executed_at": "2025-11-07T00:15:00"
}

Note: Results are stored when the message is queued to Letta. To check the actual run status, use the Letta API:

# Get the run_id from results
RESULT=$(curl -H "Authorization: Bearer your-letta-api-key" \
  https://your-modal-app.modal.run/results/{schedule_id})

RUN_ID=$(echo $RESULT | jq -r '.run_id')

# Check run status with Letta
curl -H "Authorization: Bearer your-letta-api-key" \
  https://api.letta.com/v1/runs/$RUN_ID

Response Format

Recurring Schedule Response

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "agent_id": "agent-123",
  "cron": "0 9 * * *",
  "message": "Good morning!",
  "role": "user",
  "created_at": "2025-11-06T10:00:00",
  "last_run": "2025-11-06T09:00:00"
}

Note: API keys are stored securely and never returned in responses.

One-Time Schedule Response

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "agent_id": "agent-123",
  "execute_at": "2025-11-07T14:30:00-05:00",
  "message": "Reminder!",
  "role": "user",
  "created_at": "2025-11-06T10:00:00"
}

Note:

  • API keys are stored securely and never returned in responses
  • Once executed, one-time schedules are deleted from storage (check /results endpoint for execution history)

How It Works

  1. API receives schedule request → Validates and stores as JSON in Modal Volume
  2. Cron job runs every minute → Checks all schedules in Volume
  3. Due schedules identified → Spawns async executor functions
  4. Executor verifies schedule exists → Skips if schedule was deleted after spawn
  5. For one-time schedules → Deletes schedule file immediately (prevents re-execution)
  6. Executor calls Letta API → Sends message to specified agent
  7. Saves execution result → Stores run_id and metadata in results folder
  8. For recurring schedules → Updates last_run timestamp in schedule file

Race Condition Prevention:

  • One-time schedules are deleted before execution (not after)
  • If multiple executors spawn, only first one successfully deletes
  • Second executor finds no file → skips gracefully
  • Filesystem is source of truth: file exists = hasn't run yet

Storage Structure

Schedules and execution results are stored in a hash-based directory structure:

/data/
├── schedules/
│   ├── recurring/
│   │   ├── {api_key_hash}/        # SHA256 hash of API key (first 16 chars)
│   │   │   ├── {uuid-1}.json.enc  # Encrypted schedule files
│   │   │   └── {uuid-2}.json.enc
│   │   └── {another_hash}/
│   │       └── {uuid-3}.json.enc
│   └── one-time/
│       ├── 2025-11-06/             # Date bucket
│       │   ├── 14/                 # Hour bucket (00-23)
│       │   │   ├── {api_key_hash}/
│       │   │   │   └── {uuid}.json.enc
│       │   │   └── {another_hash}/
│       │   │       └── {uuid}.json.enc
│       │   └── 15/
│       └── 2025-11-07/
└── results/
    ├── {api_key_hash}/
    │   ├── {schedule_uuid}.json.enc  # Execution results with run_id
    │   └── {schedule_uuid}.json.enc
    └── {another_hash}/

Security Features:

  • All schedule files are encrypted at rest using Fernet (AES-128-CBC)
  • API keys never stored in plaintext
  • User isolation via hash-based directories
  • Time-based bucketing for efficient queries

Performance Benefits:

  • Recurring schedules: O(user's schedules) instead of O(all schedules)
  • One-time schedules: O(schedules in current hour) instead of O(all schedules)
  • Only checks relevant time buckets during cron execution
  • Automatic cleanup: Empty directories are removed after each cron run

Monitoring

View logs in Modal dashboard:

modal app logs letta-switchboard

Or watch logs in real-time:

modal app logs letta-switchboard --follow

Limitations

  • Minimum granularity: 1 minute (cron runs every minute)
  • Timezone handling: One-time schedules support timezones; recurring schedules run in UTC
  • Authentication: Bearer token authentication with validation against Letta API
  • Encryption key management: Single master key for all schedules (consider key rotation strategy for production)

Future Improvements

  • Encryption key rotation mechanism
  • Execution history/logs API endpoint
  • Rate limiting per user
  • Email/webhook notifications on failures
  • Pagination for list endpoints
  • Timezone support for recurring schedules
  • Schedule validation (max schedules per user)
  • Cleanup of old date buckets (>7 days) to prevent unbounded growth

Costs

Modal pricing (as of 2024):

  • Compute: ~$0.000162/second for basic CPU
  • Volume storage: ~$0.10/GB/month
  • Estimated monthly cost: $5-10 for moderate usage (hundreds of schedules)

Free tier: 30 credits/month (~$30 value)


Self-Hosting

Want to run your own instance? Switchboard is fully self-hostable on Modal.

Prerequisites

  1. Clone the repository:

    git clone https://github.com/cpfiffer/letta-switchboard.git
    cd letta-switchboard
    
  2. Install Modal CLI:

    pip install modal
    modal setup
    

Deploy Your Instance

1. Set up encryption (required):

# Automated setup
./setup_encryption.sh

# Or manually
ENCRYPTION_KEY=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
echo "Save this key: $ENCRYPTION_KEY"
modal secret create letta-switchboard-encryption \
  LETTA_SWITCHBOARD_ENCRYPTION_KEY="$ENCRYPTION_KEY"

2. Deploy to Modal:

modal deploy app.py

3. Get your API URL:

modal app list
# Look for 'switchboard' and note the URL

4. Configure CLI to use your instance:

letta-switchboard config set-url https://your-instance.modal.run

Local Development

Run locally with hot reloading:

# Enable dev mode (no encryption)
export LETTA_SWITCHBOARD_DEV_MODE=true

# Start local server
modal serve app.py

Dev Mode Features:

  • Files stored in plaintext JSON (easy to inspect)
  • No encryption overhead
  • Perfect for debugging
  • Auto-reload on code changes

View files in dev mode:

# View a schedule
cat /tmp/letta-switchboard-volume/schedules/recurring/abc123/uuid.json | jq

# List all schedules
ls -la /tmp/letta-switchboard-volume/schedules/

Testing Your Instance

Set environment variables:

export LETTA_API_KEY="sk-..."
export LETTA_AGENT_ID="agent-xxx"
export LETTA_SWITCHBOARD_URL="https://your-instance.modal.run"

Run tests:

# Python test suite
python test_api.py

# Bash test script
./test_api.sh

# Unit tests
pytest -m "not e2e"

# Full E2E tests (requires modal serve running)
pytest

Cost Estimate

Running your own instance on Modal:

  • Free tier: ~$30/month of free credits
  • API requests: Free (minimal compute)
  • Cron job: Runs every minute (~43,000 times/month)
  • Storage: First 1GB free, then $0.10/GB/month
  • Expected cost: $0-5/month for personal use

The hosted service at letta--switchboard-api.modal.run is free to use!


License

MIT