No Description

Cameron Pfiffer 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
.gitignore 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
README.md 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
app.py 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
crypto_utils.py 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
letta_executor.py 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
models.py 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
requirements.txt 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
scheduler.py 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
setup_encryption.sh 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
test-lines.bash 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
test_api.py 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago
test_api.sh 84f859ee77 Initial commit: Letta Schedules serverless scheduling service 5 months ago

README.md

Letta Schedules

A serverless scheduling service for Letta agents built on Modal. Schedule recurring (cron-based) or one-time messages to be sent to your Letta agents at specified times.

Architecture

  • FastAPI - REST API for managing schedules
  • Modal Cron - Runs every minute to check and execute due schedules
  • Modal Volume - Persistent JSON storage for schedule definitions
  • Letta Client - Executes scheduled messages via Letta API

Features

  • ✅ Schedule recurring messages with cron expressions
  • ✅ Schedule one-time messages with ISO 8601 timestamps
  • ✅ Full CRUD operations for schedules
  • ✅ Timezone support for one-time schedules
  • ✅ Automatic cleanup of executed one-time schedules
  • ✅ Async execution via Modal

Installation

  1. Clone the repository:

    cd letta-schedules
    
  2. Install Modal CLI:

    pip install modal
    
  3. Authenticate with Modal:

    modal setup
    

Deployment

1. Set Encryption Key (Required)

Option A: Automated Setup Script

./setup_encryption.sh

This will:

  • Generate a secure encryption key
  • Create the Modal secret
  • Display the key for safekeeping

Option B: Manual Setup

# Generate a new encryption key
ENCRYPTION_KEY=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")

# Display and save the key
echo "Your encryption key: $ENCRYPTION_KEY"

# Create Modal secret
modal secret create letta-schedules-encryption \
  LETTA_SCHEDULES_ENCRYPTION_KEY="$ENCRYPTION_KEY"

⚠️ CRITICAL: Save the encryption key securely! If lost, all encrypted schedules become unrecoverable.

2. Deploy to Modal

modal deploy app.py

This will:

  • Create the Modal app and volume
  • Set up the scheduler cron job (runs every minute)
  • Deploy the FastAPI endpoints with encryption

3. Get Your API URL

modal app list

Look for letta-schedules and note the API endpoint URL.

Local Development

Run locally with hot reloading:

# Enable dev mode (no encryption, easier debugging)
export LETTA_SCHEDULES_DEV_MODE=true
export LETTA_SCHEDULES_ENCRYPTION_KEY="any-value-ignored-in-dev-mode"

modal serve app.py

This starts a local development server with auto-reload on file changes.

Dev Mode Features:

  • 🔓 Files stored in plaintext JSON (no encryption)
  • Easy to inspect with cat, jq, etc.
  • Clearly logged: DEV MODE: Encryption disabled
  • Perfect for local debugging

Important: Never use dev mode in production! Set LETTA_SCHEDULES_DEV_MODE=false or leave unset for production.

Inspecting files in dev mode:

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

# View an execution result
cat /tmp/letta-schedules-volume/results/abc123/uuid.json | jq

# List all schedules for a user
ls -la /tmp/letta-schedules-volume/schedules/recurring/abc123/

Testing

Two test scripts are provided. Both require environment variables:

Setup Environment Variables

export LETTA_API_KEY="sk-..."              # Required: Your valid Letta API key
export LETTA_AGENT_ID="agent-xxx"          # Optional: Agent to test with
export LETTA_SCHEDULES_URL="https://..."   # Optional: Your Modal app URL

Important: The API key must be valid and will be validated against Letta's API during testing.

Python Test Script

python test_api.py

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

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

API Usage

Base URL: https://your-modal-app.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-schedules

Or watch logs in real-time:

modal app logs letta-schedules --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)

License

MIT