Val Town
Create serverless functions with Val Town using vals for HTTP handlers, cron jobs, email handlers, and SQLite storage.
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 linesVal 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
- Making vals public when they contain business logic or data access - Default to private/unlisted. Only make vals public when sharing reusable utilities.
- Using blob storage for structured queryable data - SQLite supports indexes, joins, and aggregations. Reserve blobs for binary files and large text.
- 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.
- 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
Related Skills
Make Integromat
Build and manage Make (formerly Integromat) scenarios using modules, routers, webhooks, and data stores.
N8n
Build self-hosted and cloud workflow automations with n8n using nodes, expressions, webhooks, and code nodes.
Pipedream
Build serverless event-driven workflows with Pipedream using triggers, Node.js/Python steps, and data stores.
Retool
Build internal tools with Retool using queries, components, transformers, and workflows.
Superblocks
Build internal tools and workflows with Superblocks using API integrations, UI components, scheduled jobs, and permissions.
Windmill
Build scripts, flows, and apps with Windmill using TypeScript and Python runtimes.