An email-based personal AI agent built on Val Town. Interact entirely via email — send messages, tasks, ideas, and memories to memory-do@valtown.email and the agent processes everything inside a persistent per-user sandbox.
Send an email. The agent reads it, figures out what you mean, and acts on it.
You → "Remind me Thursday to call the dentist"
→ "I like chickens"
→ "What's the latest news on WebAssembly?"
→ "Every Monday, give me a summary of my active tasks"
Under the hood:
- Receive — Your email arrives and is written to your personal sandbox inbox
- Boot — A Deno sandbox spins up with your persistent volume mounted
- Execute — Claude reads your
CLAUDE.md, checks the inbox, and handles everything using full Linux tooling (Bash, file I/O, git, curl, etc.) - Reply — You get an email back with results, follow-up questions, and any generated file attachments (rendered as HTML with a plain-text fallback)
Emails sent via voice transcription work great. The agent is tuned for misspellings, run-on sentences, and implicit intent — it interprets what you mean, not what you literally typed.
Each user gets their own persistent Deno sandbox volume — a private filesystem that acts as the agent's brain. There's no classifier step and no custom tool-use loop. The agent runs inside the sandbox using the Claude Agent SDK with native Linux tools.
┌───────────────────────────────────────────────────────┐
│ VAL TOWN (orchestrator) │
│ │
│ email-handler-v2.ts → receives email │
│ ├─ Get/create user │
│ ├─ Write email to sandbox inbox │
│ ├─ Boot sandbox → run agent-loop.mjs │
│ ├─ Read response from sandbox outbox │
│ ├─ Process schedule.json → scheduled_tasks │
│ └─ Send reply email (HTML + plain text) │
│ │
│ task-runner-v2.ts → runs every 15 min │
│ ├─ Query due scheduled_tasks │
│ ├─ Boot sandbox for each due task │
│ └─ Email results, reschedule if recurring │
└───────────────────┬───────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────┐
│ DENO SANDBOX (per-user microVM) │
│ │
│ Runs: node /opt/agent/agent-loop.mjs │
│ Uses: Claude Agent SDK with Bash/Read/Write/Edit/ │
│ Glob/Grep tools │
│ Mounts: user's persistent volume at /home/user/ │
│ │
│ /home/user/ │
│ ├── CLAUDE.md ← agent instructions │
│ ├── agents.md ← persona/identity │
│ ├── TODO.md ← persistent task list │
│ ├── memories/ ← knowledge as markdown files │
│ ├── ideas/ ← captured ideas │
│ ├── people/ ← people directory │
│ ├── repos/ ← cloned git repositories │
│ ├── workspace/ ← scratch area │
│ ├── .ssh/ ← SSH keys │
│ └── .mail/ │
│ ├── inbox/ ← tasks arrive here │
│ └── outbox/ ← responses go here │
└───────────────────────────────────────────────────────┘
All user state lives on their persistent sandbox volume as plain files:
| What | Where |
|---|---|
| Memories & facts | memories/*.md (one file per topic, Claude organizes) |
| Tasks & reminders | TODO.md (Claude maintains it directly) |
| Ideas | ideas/*.md (one per idea) |
| People | people/*.md (one per person) |
| Preferences | Embedded in CLAUDE.md (Claude edits it!) |
| Git repos | repos/ (persistent across sessions) |
| SSH keys | .ssh/ (standard Unix location) |
No SQL for content storage. Claude manages files naturally because it has full filesystem access.
The agent uses the Claude Agent SDK's built-in tools — not custom function calls:
| Tool | What It Does |
|---|---|
Bash | Run any command — git, npm, python, curl, etc. |
Read | Read any file on the volume |
Write | Write any file on the volume |
Edit | Edit files with diffs |
Glob | Find files by pattern |
Grep | Search file contents |
This means the agent can do multi-step work: search your memories with grep, fetch a web page with curl, cross-reference with your stored projects, save what it learned as a markdown file, clone a repo, run code — all in one email exchange.
| Type | Behavior | Example |
|---|---|---|
| Instant | Executes immediately, reply in same email | "What's the weather in London?" |
| Future | Scheduled for a specific time | "Remind me Thursday at 10am to call dentist" |
| Recurring | Repeats on a schedule | "Every Monday, summarize my active tasks" |
Future and recurring tasks are processed by a task runner that checks every 15 minutes. The agent writes a schedule.json to its outbox, and the orchestrator picks it up and inserts it into the scheduled_tasks table.
Each user's sandbox has a CLAUDE.md and agents.md that the agent reads at the start of every session. The agent can edit these files directly — so when you say:
- "Be more casual and use emojis"
- "Call me Paul"
- "Always use bullet points"
…the agent updates its own instructions. Changes persist across sessions because the volume is persistent.
Full bidirectional support:
- Inbound — Email attachments (CSV, JSON, text, code files, etc.) are included in the task description sent to the sandbox
- Outbound — The agent can generate files inside the sandbox, which are read from the outbox and sent back as email attachments
email-agent/
README.md ← You are here
CLAUDE.md — Internal dev guide and conventions
main.ts — HTTP admin/status endpoint
email-handler-v2.ts — Email ingestion → sandbox execution
task-runner-v2.ts — Scheduled task processor (every 15 min)
lib/
sandbox-orchestrator.ts — Volume CRUD, sandbox boot, result reading
sandbox-agent-loop.ts — Source for agent-loop.mjs (runs in sandbox)
sandbox-claude-md.ts — Builds CLAUDE.md and agents.md for volumes
scheduler.ts — Natural language → ISO datetime
db/
schema-v2.ts — SQLite schema (users, scheduled_tasks, executions)
schema.ts — Legacy v1 schema (entries, FTS, links)
queries.ts — Legacy v1 queries
admin/
build-snapshot.ts — Builds the toolchain sandbox snapshot
test-orchestrator.ts — Test harness for sandbox orchestration
- Runtime: Val Town (Deno-based serverless)
- Agent: Claude Agent SDK running inside Deno sandboxes
- Sandboxes: Deno Sandbox SDK — per-user persistent volumes with full Linux tooling
- Database: Project-scoped SQLite (thin routing layer only — users, scheduled tasks, execution logs)
- Email: Val Town email sending/receiving
- LLM: Claude via Anthropic SDK (agent loop + scheduler)
SQLite is a thin routing layer — it does not store user content (that lives on sandbox volumes):
| Table | Purpose |
|---|---|
users | Email → user mapping + volume reference |
scheduled_tasks | When to wake up a sandbox (task runner polls this) |
sandbox_executions | Execution log for debugging and cost tracking |
email_dedup | Short-lived deduplication table for email retries |
| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY | Yes | Used by Claude API calls (agent loop, scheduler) |
DENO_DEPLOY_TOKEN | Yes | Used for Deno sandbox and volume management |
The HTTP endpoint (main.ts) provides:
GET /orGET /stats— System-wide stats (entry counts, users, links)GET /user/:email— Full user profile with entries grouped by typeGET /migrate— Run one-time data migration from legacy tables
Note:
main.tscurrently queries the legacy v1 schema tables (entries,entry_links). A future update will switch it to the v2 tables.
- Sandbox boot latency — ~2–5 seconds per execution; not ideal for trivial questions
- Concurrency — Deno Sandbox has a concurrent execution limit during pre-release; may need queuing with multiple simultaneous users
- No admin auth — HTTP endpoints are open
- No rate limiting — No flood protection
- No cost alerting —
sandbox_executionslogs cost but there's no per-user limits or alerts yet - File attachments (outbound) — Stubbed but not yet fully implemented
- SSH key management — Infrastructure exists (
.ssh/dir on volume) but no user-facing flow yet - Snapshot rollout — No automated process for updating the sandbox toolchain across existing users
Built on Val Town. Do what you want with it.