Skip to main content
Technology & EngineeringAutomation Workflow Services267 lines

Val Town

Create serverless functions with Val Town using vals for HTTP handlers, cron jobs, email handlers, and SQLite storage.

Quick Summary23 lines
You are an expert in Val Town, building serverless TypeScript functions (vals) for HTTP endpoints, scheduled jobs, email handlers, and persistent storage. You design vals that leverage Val Town's runtime, built-in SQLite database, and social coding features.

## Key Points

1. **Making vals public when they contain business logic or data access** - Default to private/unlisted. Only make vals public when sharing reusable utilities.
2. **Using blob storage for structured queryable data** - SQLite supports indexes, joins, and aggregations. Reserve blobs for binary files and large text.
3. **Ignoring val execution time limits** - Vals have a 30-second timeout. For long tasks, break work into chunks and use cron vals to process batches.
4. **Importing community vals without reviewing their code** - Public vals can change at any time. Pin imports to specific versions or fork critical dependencies.
- Prototyping webhook receivers, APIs, and integrations without any infrastructure setup
- Building lightweight cron jobs that monitor services, sync data, or send notifications
- Creating email-triggered automations with built-in email handler support
- Sharing reusable TypeScript utility functions with the developer community
- Running serverless functions that need simple persistence without provisioning a database

## Quick Example

```bash
# Val Town URLs follow this pattern:
# HTTP val: https://username-valname.web.val.run
# API: https://api.val.town/v1/run/username.valName
```
skilldb get automation-workflow-services-skills/Val TownFull skill: 267 lines
Paste into your CLAUDE.md or agent config

Val Town Serverless Functions

You are an expert in Val Town, building serverless TypeScript functions (vals) for HTTP endpoints, scheduled jobs, email handlers, and persistent storage. You design vals that leverage Val Town's runtime, built-in SQLite database, and social coding features.

Core Philosophy

Vals as Building Blocks

Each val is an independently deployable unit: an HTTP handler, a cron job, an email processor, or a reusable function. Compose complex systems by importing vals from yourself or the community. Keep each val focused on a single concern.

Instant Deployment, Zero Config

Val Town eliminates infrastructure. Write a function, save it, and it's live with an HTTPS endpoint or cron schedule. Embrace this speed for prototyping, webhooks, and lightweight APIs without Dockerfiles or deployment pipelines.

Built-In Persistence

Val Town provides SQLite (via std/sqlite) for structured data and blob storage for files. Use these instead of external databases for lightweight state. Graduate to external databases only when you outgrow the built-in limits.

Setup

// Val Town API for programmatic management
const VAL_TOWN_API = "https://api.val.town/v1";
const headers = {
  Authorization: `Bearer ${Deno.env.get("valtown")}`,
  "Content-Type": "application/json",
};

// Create a new val
await fetch(`${VAL_TOWN_API}/vals`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    name: "myHttpHandler",
    code: `export default function(req: Request): Response {
      return Response.json({ hello: "world" });
    }`,
    type: "http",
    privacy: "public",
  }),
});
# Val Town URLs follow this pattern:
# HTTP val: https://username-valname.web.val.run
# API: https://api.val.town/v1/run/username.valName

Key Patterns

Type HTTP Handlers Correctly

// Do: Use the standard Request/Response API
export default async function(req: Request): Promise<Response> {
  if (req.method !== "POST") {
    return new Response("Method not allowed", { status: 405 });
  }
  const body = await req.json();
  return Response.json({ received: body }, { status: 200 });
}

// Don't: Use Express-style req/res patterns or forget to return a Response

Use SQLite for Persistent State

// Do: Use Val Town's built-in SQLite
import { sqlite } from "https://esm.town/v/std/sqlite";

// Initialize table (idempotent)
await sqlite.execute(`
  CREATE TABLE IF NOT EXISTS events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    type TEXT NOT NULL,
    payload TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);

// Insert with parameterized queries
await sqlite.execute(
  "INSERT INTO events (type, payload) VALUES (?, ?)",
  ["order.created", JSON.stringify({ orderId: "abc123" })]
);

// Don't: Store structured data in blob storage or global variables

Keep Secrets in Environment Variables

// Do: Access secrets via Deno.env
export default async function(req: Request): Promise<Response> {
  const apiKey = Deno.env.get("STRIPE_SECRET_KEY");
  const resp = await fetch("https://api.stripe.com/v1/charges?limit=5", {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  return Response.json(await resp.json());
}

// Don't: Hardcode secrets in val source code (vals can be public)

Common Patterns

HTTP API with Routing

// Val: httpRouter
export default async function(req: Request): Promise<Response> {
  const url = new URL(req.url);
  const path = url.pathname;

  if (req.method === "GET" && path === "/users") {
    return handleGetUsers();
  }
  if (req.method === "POST" && path === "/users") {
    return handleCreateUser(req);
  }
  if (req.method === "GET" && path.startsWith("/users/")) {
    const id = path.split("/")[2];
    return handleGetUser(id);
  }
  return new Response("Not found", { status: 404 });
}

async function handleGetUsers(): Promise<Response> {
  const { rows } = await sqlite.execute("SELECT * FROM users ORDER BY created_at DESC LIMIT 50");
  return Response.json(rows);
}

async function handleCreateUser(req: Request): Promise<Response> {
  const { name, email } = await req.json();
  if (!name || !email) {
    return Response.json({ error: "name and email required" }, { status: 400 });
  }
  await sqlite.execute("INSERT INTO users (name, email) VALUES (?, ?)", [name, email]);
  return Response.json({ created: true }, { status: 201 });
}

async function handleGetUser(id: string): Promise<Response> {
  const { rows } = await sqlite.execute("SELECT * FROM users WHERE id = ?", [id]);
  if (rows.length === 0) return new Response("Not found", { status: 404 });
  return Response.json(rows[0]);
}

Cron Job with SQLite State

// Val: syncGithubStars (type: cron, schedule: every 1 hour)
import { sqlite } from "https://esm.town/v/std/sqlite";

export default async function() {
  await sqlite.execute(`
    CREATE TABLE IF NOT EXISTS github_stars (
      repo TEXT PRIMARY KEY,
      stars INTEGER,
      updated_at TEXT
    )
  `);

  const repos = ["denoland/deno", "val-town/val-town"];

  for (const repo of repos) {
    const resp = await fetch(`https://api.github.com/repos/${repo}`);
    const data = await resp.json();
    await sqlite.execute(
      `INSERT INTO github_stars (repo, stars, updated_at)
       VALUES (?, ?, datetime('now'))
       ON CONFLICT(repo) DO UPDATE SET stars=?, updated_at=datetime('now')`,
      [repo, data.stargazers_count, data.stargazers_count]
    );
  }

  console.log(`Synced ${repos.length} repos`);
}

Email Handler

// Val: handleInboundEmail (type: email)
// Receives email at: username.handleInboundEmail@valtown.email
import { sqlite } from "https://esm.town/v/std/sqlite";

export default async function(email: {
  from: string;
  to: string[];
  subject: string;
  text: string;
  html: string;
}) {
  await sqlite.execute(`
    CREATE TABLE IF NOT EXISTS inbound_emails (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      sender TEXT, subject TEXT, body TEXT, received_at TEXT
    )
  `);

  await sqlite.execute(
    "INSERT INTO inbound_emails (sender, subject, body, received_at) VALUES (?, ?, ?, datetime('now'))",
    [email.from, email.subject, email.text]
  );

  // Forward important emails
  if (email.subject.toLowerCase().includes("urgent")) {
    await fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
      method: "POST",
      body: JSON.stringify({ text: `Urgent email from ${email.from}: ${email.subject}` }),
    });
  }
}

Webhook Receiver with Signature Verification

// Val: stripeWebhook
import { createHmac } from "node:crypto";

export default async function(req: Request): Promise<Response> {
  const signature = req.headers.get("stripe-signature");
  const body = await req.text();
  const secret = Deno.env.get("STRIPE_WEBHOOK_SECRET")!;

  // Parse Stripe signature header
  const pairs = Object.fromEntries(
    signature!.split(",").map((p) => p.split("=") as [string, string])
  );
  const expected = createHmac("sha256", secret)
    .update(`${pairs.t}.${body}`)
    .digest("hex");

  if (expected !== pairs.v1) {
    return new Response("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(body);
  switch (event.type) {
    case "checkout.session.completed":
      await handleCheckout(event.data.object);
      break;
    case "invoice.payment_failed":
      await handleFailedPayment(event.data.object);
      break;
  }

  return Response.json({ received: true });
}

Anti-Patterns

  1. Making vals public when they contain business logic or data access - Default to private/unlisted. Only make vals public when sharing reusable utilities.
  2. Using blob storage for structured queryable data - SQLite supports indexes, joins, and aggregations. Reserve blobs for binary files and large text.
  3. Ignoring val execution time limits - Vals have a 30-second timeout. For long tasks, break work into chunks and use cron vals to process batches.
  4. Importing community vals without reviewing their code - Public vals can change at any time. Pin imports to specific versions or fork critical dependencies.

When to Use

  • Prototyping webhook receivers, APIs, and integrations without any infrastructure setup
  • Building lightweight cron jobs that monitor services, sync data, or send notifications
  • Creating email-triggered automations with built-in email handler support
  • Sharing reusable TypeScript utility functions with the developer community
  • Running serverless functions that need simple persistence without provisioning a database

Install this skill directly: skilldb add automation-workflow-services-skills

Get CLI access →