• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
paulkinlan

paulkinlan

email-agent

Public
Like
email-agent
Home
Code
6
db
2
lib
3
CLAUDE.md
E
email-handler.ts
H
main.ts
C
task-runner.ts
Connections
Environment variables
1
Branches
1
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
CLAUDE.md
Code
/
CLAUDE.md
Search
…
Viewing readonly version of main branch: v28
View latest version
CLAUDE.md

Email Agent System

An email-based personal AI agent built on Val Town. Users interact entirely via email — sending messages, tasks, ideas, and memories to memory-do@valtown.email. The agent classifies, stores, executes, and replies.

Architecture

email-agent/
  CLAUDE.md             — This file. System instructions and conventions.
  main.ts               — HTTP endpoint for status/admin/debugging (type: http)
  email-handler.ts      — Email ingestion point (type: email, address: memory-do@valtown.email)
  task-runner.ts        — Interval that processes pending/recurring tasks (type: interval)
  lib/
    classifier.ts       — Claude-powered email classification
    scheduler.ts        — Natural language → ISO datetime conversion
    agent.ts            — Task execution engine with full user context
  db/
    schema.ts           — SQLite schema (FTS5 for full-text search)
    queries.ts          — All CRUD operations, FTS sync, user context builder

How It Works

Email Flow (email-handler.ts)

  1. Email arrives at memory-do@valtown.email
  2. Sender is identified/created as a user
  3. Interaction is logged (inbound)
  4. Email is classified by Claude into: memories, instant tasks, future tasks, recurring tasks, ideas
  5. All extracted data is stored in SQLite with FTS indexes synced
  6. Instant tasks execute immediately via the agent; results are emailed back
  7. Future/recurring tasks are queued with computed next_run_at timestamps
  8. Acknowledgment email is sent summarizing what was captured

Task Runner (task-runner.ts)

  • Runs on an interval (every 15 minutes)
  • Finds tasks where next_run_at <= now and status = 'pending'
  • Executes each via the agent with full user context
  • Emails results to the user
  • Recurring tasks get rescheduled to their next occurrence
  • Failed tasks still get rescheduled if recurring

HTTP Admin (main.ts)

  • GET / or GET /stats — system-wide counts (users, memories, tasks, ideas, interactions)
  • GET /user/:email — full user profile with all associated data

Database

Uses project-scoped SQLite (std/sqlite/main.ts). Do NOT change to std/sqlite or std/sqlite/global.ts — this project uses its own database, not the user's global one.

Tables

  • users — unique sender emails, optional name
  • memories — long-term facts/preferences per user (category: preference, fact, context, general)
  • tasks — instant/future/recurring with status lifecycle (pending → in_progress → completed/failed/cancelled)
  • ideas — freeform captured ideas with tags
  • interactions — raw log of every inbound/outbound email

FTS5 Virtual Tables

Every content table has a corresponding *_fts table. FTS indexes must be synced manually on every insert — this is done in queries.ts. If you add a new content table, add its FTS counterpart in schema.ts and sync inserts in queries.ts.

Indexes

  • idx_memories_user — memories by user
  • idx_tasks_user_status — tasks by user + status
  • idx_tasks_pending_run — partial index for due task lookup
  • idx_ideas_user — ideas by user
  • idx_interactions_user — interactions by user

Environment Variables

  • ANTHROPIC_API_KEY — required. Used by all Claude API calls (classifier, scheduler, agent). Accessed implicitly by the Anthropic SDK.

Dependencies

  • npm:@anthropic-ai/sdk@0.39.0 — Claude API client (pin the version)
  • https://esm.town/v/std/sqlite/main.ts — project-scoped SQLite
  • https://esm.town/v/std/email — Val Town email sending

Conventions

Code Style

  • TypeScript everywhere (.ts files only)
  • File and val names in kebab-case (e.g., email-handler.ts, task-runner.ts)
  • Interfaces for all data shapes crossing boundaries (API responses, DB rows, function params)
  • Use Record<string, unknown> for untyped DB rows, cast fields at point of use
  • Always use parameterized queries (args) — never interpolate user input into SQL
  • Emoji prefixes in console.log for easy scanning: 📧 📝 🧠 📊 💾 💡 ⏰ 🔄 ⚡ ✅ ❌ 📤 🔧 📋

Error Handling

  • Every email processing step is wrapped in try/catch
  • Task failures still send a notification email to the user
  • Recurring tasks get rescheduled even on failure
  • FTS search failures are non-fatal (empty tables can cause issues)
  • JSON parse failures in classifier/scheduler fall back to sensible defaults

LLM Calls

  • All Claude calls use claude-sonnet-4-20250514 — keep this consistent
  • System prompts are defined as module-level constants (not inline)
  • Responses are parsed via regex (/\{[\s\S]*\}/) to extract JSON from potential markdown code blocks
  • Always have a fallback if JSON parsing fails

Database Patterns

  • initDatabase() is called at the top of every entry point (HTTP handler, email handler, task runner) — safe to call multiple times (CREATE TABLE IF NOT EXISTS)
  • FTS inserts happen immediately after main table inserts in the same function
  • Use Promise.all for independent parallel queries
  • getUserContext() is the canonical way to build a user context bundle for the agent

Working Style

  • Don't break the email flow — email-handler.ts is the critical path. Changes there should be tested carefully. A broken email handler means users get no response.
  • Idempotent schema — initDatabase() must always be safe to re-run. Use IF NOT EXISTS on everything.
  • Keep classifier prompts tight — The classification prompt directly controls what gets stored. Be precise about what counts as a memory vs. idea vs. task. Over-extraction creates noise.
  • Ideas require explicit intent — The classifier is specifically instructed to only capture ideas the user explicitly asks to store. Don't weaken this — it prevents every email from spawning spurious ideas.
  • Agent has full context — When executing tasks, the agent gets memories, active tasks, recent ideas, recent conversations, and FTS search results. This is intentional — the agent should feel like it knows the user.
  • Two Claude calls per task execution — One for the task response, one for the subject line. This is a known cost trade-off for better email subjects. Consider whether a single call with structured output could replace this.
  • Voice input tolerance — Users may send emails via voice transcription. Expect imperfect input — misspellings, run-on sentences, homophones. Interpret intent rather than demanding precision.

Known Limitations & Future Improvements

  • No real-time data — The agent can't fetch live stock prices, weather, etc. It acknowledges this in responses. Adding tool use (web search, API calls) would be a significant upgrade.
  • No conversation threading — Each email is processed independently. The agent has recent interaction history but doesn't maintain true conversation threads.
  • No email HTML rendering — Replies are plain text only. Adding markdown-to-HTML conversion would improve readability.
  • No auth on HTTP admin — The /stats and /user/:email endpoints are open. Add authentication if this becomes public-facing.
  • FTS content tables aren't content-synced — The FTS tables use content_rowid but aren't configured as content tables (no content= parameter). This means deletes/updates won't sync automatically. Currently not an issue since we only insert, but would matter if edits are added.
  • Single Claude model — All calls use the same model. Could use a lighter model for scheduling/subject lines and a stronger one for complex task execution.
  • No rate limiting — No protection against a user flooding the system with emails.
  • No memory deduplication — Same fact can be stored multiple times across emails. Could use semantic similarity to deduplicate.

Evolving the System

This agent should grow organically based on usage patterns. When you notice recurring needs:

  1. Propose new capabilities — If users frequently ask for something the agent can't do, suggest adding it (e.g., web search tool, calendar integration).
  2. Refine classification — If the classifier is miscategorizing things, tighten the prompt. Log examples of misclassifications.
  3. Add new entity types — If a new category of data emerges (e.g., contacts, projects, bookmarks), propose a new table and update the schema, queries, classifier, and agent context.
  4. Keep this file updated — When the system changes, update CLAUDE.md. This is the source of truth for how the system works.
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.