← NetlifyCONTENT HISTORY

Update to Netlify

Snapshot Oct 7, 2026 · 00:02 UTC · version 1.6.0

Collection source: downloaded plugin package. These snapshots do not have a confirmed matching collection source. Differences in file lists alone do not establish changes to the package.

WHAT CHANGED · RULE-BASED ANALYSIS

Payment or plan references changed

Instruction wording changed from “Guide for writing Netlify serverless functions. Use when creating API endpoints, background processing, scheduled tasks, or any server-side logic using Netlify Functions. Covers modern syntax (default export + Config), TypeScript, path r...” to “Write, configure, and deploy Netlify serverless functions in TypeScript, JavaScript, or Go. Use this when adding an API endpoint or backend route, adding a contact form handler, wiring auth or Identity signup/login hooks, building stream...”. 236 additional added or edited lines are in the evidence.

Observed in published text. Live prices and checkout terms have not been verified by this change.

Product description

Before

Guide for writing Netlify serverless functions. Use when creating API endpoints, background processing, scheduled tasks, or any server-side logic using Netlify Functions. Covers modern syntax (default export + Config), TypeScript, path r...

After

Write, configure, and deploy Netlify serverless functions in TypeScript, JavaScript, or Go. Use this when adding an API endpoint or backend route, adding a contact form handler, wiring auth or Identity signup/login hooks, building stream...

Skill instructions

Before

Guide for writing Netlify serverless functions. Use when creating API endpoints, background processing, scheduled tasks, or any server-side logic using Netlify Functions. Covers modern syntax (default export + Config), TypeScript, path r...

After

Write, configure, and deploy Netlify serverless functions in TypeScript, JavaScript, or Go. Use this when adding an API endpoint or backend route, adding a contact form handler, wiring auth or Identity signup/login hooks, building stream...

Supporting files

Before

[{"relative_path":"LICENSE.txt","size_in_bytes":10776},{"relative_path":"agents/openai.yaml","size_in_bytes":352},{"relative_path":"assets/netlify-small.svg","size_in_bytes":1291},{"relative_path":"assets/netlify.png","size_in_bytes":2686}]

After

[]

Compare saved observations

Download comparison JSON
Full technical diff · 3 changed fields

changed /description

BEFORE
"Guide for writing Netlify serverless functions. Use when creating API endpoints, background processing, scheduled tasks, or any server-side logic using Netlify Functions. Covers modern syntax (default export + Config), TypeScript, path routing, background functions, scheduled functions, streaming, and method routing."
AFTER
"Write, configure, and deploy Netlify serverless functions in TypeScript, JavaScript, or Go. Use this when adding an API endpoint or backend route, adding a contact form handler, wiring auth or Identity signup/login hooks, building streaming or AI-proxy responses, scheduling cron jobs, running long background jobs (batch processing/scraping), reacting to deploy or form events, setting up rate limiting or region/memory config, or reading environment variables and secrets inside a function. Covers file locations, the Request/Context/Response handler shape, path routing, config options, and local testing with netlify dev."

changed /included_files

BEFORE
[
  {
    "relative_path": "LICENSE.txt",
    "size_in_bytes": 10776
  },
  {
    "relative_path": "agents/openai.yaml",
    "size_in_bytes": 352
  },
  {
    "relative_path": "assets/netlify-small.svg",
    "size_in_bytes": 1291
  },
  {
    "relative_path": "assets/netlify.png",
    "size_in_bytes": 2686
  }
]
AFTER
[]

changed /skill_md_contents

BEFORE
"---\nname: netlify-functions\ndescription: Guide for writing Netlify serverless functions. Use when creating API endpoints, background processing, scheduled tasks, or any server-side logic using Netlify Functions. Covers modern syntax (default export + Config), TypeScript, path routing, background functions, scheduled functions, streaming, and method routing.\n---\n\n# Netlify Functions\n\n## Modern Syntax\n\nAlways use the modern default export + Config pattern. Never use the legacy `exports.handler` or named `handler` export.\n\n```typescript\nimport type { Context, Config } from \"@netlify/functions\";\n\nexport default async (req: Request, context: Context) => {\n  return new Response(\"Hello, world!\");\n};\n\nexport const config: Config = {\n  path: \"/api/hello\",\n};\n```\n\nThe handler receives a standard Web API `Request` and returns a `Response`. The second argument is a Netlify `Context` object.\n\n## File Structure\n\nPlace functions in `netlify/functions/`:\n\n```\nnetlify/functions/\n  _shared/           # Non-function shared code (underscore prefix)\n    auth.ts\n    db.ts\n  items.ts           # -> /.netlify/functions/items (or custom path via config)\n  users/index.ts     # -> /.netlify/functions/users\n```\n\nUse `.ts` or `.mts` extensions. If both `.ts` and `.js` exist with the same name, the `.js` file takes precedence.\n\n## Path Routing\n\nDefine custom paths via the `config` export:\n\n```typescript\nexport const config: Config = {\n  path: \"/api/items\",                    // Static path\n  // path: \"/api/items/:id\",            // Path parameter\n  // path: [\"/api/items\", \"/api/items/:id\"], // Multiple paths\n  // excludedPath: \"/api/items/special\", // Excluded paths\n  // preferStatic: true,                // Don't override static files\n};\n```\n\nWithout a `path` config, functions are available at `/.netlify/functions/{name}`. Setting a `path` makes the function available **only** at that path.\n\nAccess path parameters via `context.params`:\n\n```typescript\n// config: { path: \"/api/items/:id\" }\nexport default async (req: Request, context: Context) => {\n  const { id } = context.params;\n  // ...\n};\n```\n\n## Method Routing\n\n```typescript\nexport default async (req: Request, context: Context) => {\n  switch (req.method) {\n    case \"GET\":    return handleGet(context.params.id);\n    case \"POST\":   return handlePost(await req.json());\n    case \"DELETE\": return handleDelete(context.params.id);\n    default:       return new Response(\"Method not allowed\", { status: 405 });\n  }\n};\n\nexport const config: Config = {\n  path: \"/api/items/:id\",\n  method: [\"GET\", \"POST\", \"DELETE\"],\n};\n```\n\n## Background Functions\n\nFor long-running tasks (up to 15 minutes). The client receives an immediate `202` response; return values are ignored.\n\nName the file with a `-background` suffix:\n\n```\nnetlify/functions/process-background.ts\n```\n\nStore results externally (Netlify Blobs, database) for later retrieval.\n\n## Scheduled Functions\n\nRun on a cron schedule (UTC timezone):\n\n```typescript\nexport default async (req: Request) => {\n  const { next_run } = await req.json();\n  console.log(\"Next invocation at:\", next_run);\n};\n\nexport const config: Config = {\n  schedule: \"@hourly\", // or cron: \"0 * * * *\"\n};\n```\n\nShortcuts: `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`. Scheduled functions have a **30-second timeout** and only run on published deploys.\n\n## Streaming Responses\n\nReturn a `ReadableStream` body for streamed responses (up to 20 MB):\n\n```typescript\nexport default async (req: Request) => {\n  const stream = new ReadableStream({ /* ... */ });\n  return new Response(stream, {\n    headers: { \"Content-Type\": \"text/event-stream\" },\n  });\n};\n```\n\n## Context Object\n\n| Property | Description |\n|---|---|\n| `context.params` | Path parameters from config |\n| `context.geo` | `{ city, country: {code, name}, latitude, longitude, subdivision, timezone, postalCode }` |\n| `context.ip` | Client IP address |\n| `context.cookies` | `.get()`, `.set()`, `.delete()` |\n| `context.deploy` | `{ context, id, published }` |\n| `context.site` | `{ id, name, url }` |\n| `context.account.id` | Team account ID |\n| `context.requestId` | Unique request ID |\n| `context.waitUntil(promise)` | Extend execution after response is sent |\n\n## Environment Variables\n\nUse `Netlify.env` (not `process.env`) inside functions:\n\n```typescript\nconst apiKey = Netlify.env.get(\"API_KEY\");\n```\n\n## Resource Limits\n\n| Resource | Limit |\n|---|---|\n| Synchronous timeout | 60 seconds |\n| Background timeout | 15 minutes |\n| Scheduled timeout | 30 seconds |\n| Memory | 1024 MB |\n| Buffered payload | 6 MB |\n| Streamed payload | 20 MB |\n\n## Framework Considerations\n\nFrameworks with server-side capabilities (Astro, Next.js, Nuxt, SvelteKit, TanStack Start) typically generate their own serverless functions via adapters. You usually do not write raw Netlify Functions in these projects — the framework adapter handles server-side rendering and API routes. Write Netlify Functions directly when:\n\n- Using a client-side-only framework (Vite + React SPA, vanilla JS)\n- Adding background or scheduled tasks to any project\n- Building standalone API endpoints outside the framework's routing\n\nSee the **netlify-frameworks** skill for adapter setup.\n"
AFTER
"---\nname: netlify-functions\ndescription: Write, configure, and deploy Netlify serverless functions in TypeScript, JavaScript, or Go. Use this when adding an API endpoint or backend route, adding a contact form handler, wiring auth or Identity signup/login hooks, building streaming or AI-proxy responses, scheduling cron jobs, running long background jobs (batch processing/scraping), reacting to deploy or form events, setting up rate limiting or region/memory config, or reading environment variables and secrets inside a function. Covers file locations, the Request/Context/Response handler shape, path routing, config options, and local testing with netlify dev.\n---\n\n# Netlify Functions\n\nReach for the modern default-handler API (`.mts` TypeScript). Export a default async handler taking a web `Request` and a Netlify `Context`, returning a web `Response`. Avoid the legacy AWS Lambda handler shape unless writing Go or migrating old code (see Legacy at the end).\n\n## File locations\n\n- Default directory: `netlify/functions/` (relative to base directory). Keep it **outside** your publish directory or source files ship as static assets.\n- A function is one file or a subdirectory whose entry file is named `index` or matches the subdirectory name. All of these create a function `hello`:\n  - `netlify/functions/hello.mts`\n  - `netlify/functions/hello/hello.mts`\n  - `netlify/functions/hello/index.mts`\n- Use `.mts` (TS) / `.mjs` (JS) for ES modules. `.cts`/`.cjs` force CommonJS; `.ts`/`.js` follow the nearest `package.json` `\"type\"`.\n\n## Minimal function\n\nNo `config` export. Serves at `/.netlify/functions/hello`.\n\n```ts title=\"netlify/functions/hello.mts\"\nimport type { Context } from \"@netlify/functions\"\n\nexport default async (req: Request, context: Context) => {\n  return new Response(\"Hello, world!\")\n}\n```\n\nInstall types: `npm install @netlify/functions` (required for TS types; optional for JS).\n\nRead env vars and secrets with `Netlify.env.get()`:\n\n```ts\nconst apiKey = Netlify.env.get(\"STRIPE_SECRET_KEY\")\n```\n\nNever hardcode secrets. For the variable to exist at runtime its scope must include **Functions**. Variables set in `netlify.toml` are NOT available to functions. Values are frozen per deploy — change them and redeploy to apply.\n\n**Response headers are set in code** on the returned `Response`. `[[headers]]` in `netlify.toml`, `_headers`, and redirect header rules apply ONLY to static CDN responses, not function responses. Do not add CORS headers unless explicitly requested.\n\n## Custom path routing\n\nSet `config.path` to route to custom URLs. When set, the function serves ONLY at that path — not at `/.netlify/functions/<name>`.\n\n```ts title=\"netlify/functions/travel.mts\"\nimport type { Config, Context } from \"@netlify/functions\"\n\nexport default async (req: Request, context: Context) => {\n  const { city, country } = context.params\n  return new Response(`You're visiting ${city} in ${country}!`)\n}\n\nexport const config: Config = {\n  path: \"/travel-guide/:city/:country\",\n}\n```\n\n- Multiple paths: `path: [\"/cats\", \"/dogs\"]`.\n- Patterns: `path` supports [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) syntax — `path: [\"/sale/*\", \"/item/:sku\"]`. Named groups land on `context.params`. For the query string use `req.url`.\n- `excludedPath`: carve exceptions, e.g. `excludedPath: [\"/product/*.css\"]` with `path: \"/product/*\"`.\n- `preferStatic: true`: let a real static file at the URL win.\n- `method`: restrict methods, e.g. `method: [\"GET\", \"POST\"]`.\n\n## Fetchable module shape (alternative)\n\nEquivalent to the bare handler; carries `config` inline and lets you add event handlers.\n\n```ts\nimport type { NetlifyFunction } from \"@netlify/functions\"\n\nexport default {\n  fetch: (req, context) => new Response(\"Hello, world!\"),\n  config: { path: \"/hello\" },\n} satisfies NetlifyFunction\n```\n\n## Context object\n\nSecond handler argument (or `getContext()` from `@netlify/functions` when out of handler scope — throws outside a request; wrap in try/catch).\n\n- `context.params` — named path params.\n- `context.geo` — `city`, `country.code/name`, `latitude`, `longitude`, `subdivision`, `timezone`, `postalCode`.\n- `context.ip` — client IP string.\n- `context.cookies` — `get(name)` / `set(options)` / `delete(name|options)`. Cross-subdomain cookies need a custom domain (`netlify.app` is on the Public Suffix List).\n- `context.site` — `id`, `name`, `url`. `context.deploy` — `context`, `id`, `published`, `skewProtectionToken`. `context.account.id`. `context.server.region`. `context.requestId`.\n- `context.waitUntil(promise)` — run work after the response is sent (analytics, logs) without blocking. Billing/log duration counts until the promise settles. Available for functions deployed on/after 2025-03-20.\n\n⚠️ Under `netlify dev`, `context.geo` and `context.ip` are **mocked** — placeholder values that never change. Don't conclude geo code is broken locally. Exercise branches with `netlify dev --geo=mock --country=DE` and verify on a real deploy.\n\n## Config object\n\nExport `const config` (or the `config` property of a Fetchable module):\n\n- `path` / `excludedPath` — `string | string[]`, must start with `/`.\n- `method` — one method or array.\n- `preferStatic` — `boolean`.\n- `background` — `boolean` (see Background).\n- `schedule` — cron string (see Scheduled). Mutually exclusive with `path`/`excludedPath`.\n- `rateLimit` — `{ action: 'rate_limit'|'rewrite', aggregateBy: 'domain'|'ip'|[...], to?, windowSize, windowLimit }`.\n- `memory` / `vcpu` — see below; mutually exclusive.\n- `region` — airport code; see below.\n\n## Integrations\n\n```ts title=\"netlify/functions/users.mts\"\nimport type { Config } from \"@netlify/functions\"\nimport { getDatabase } from \"@netlify/database\"\n\nconst db = getDatabase()\n\nexport default async (req: Request) => {\n  const users = await db.sql`SELECT id, email FROM users LIMIT 10`\n  return Response.json({ users })\n}\n\nexport const config: Config = { path: \"/users\" }\n```\n\nBlobs: `import { getStore } from \"@netlify/blobs\"`; `getStore(\"uploads\").set(key, await req.blob())`.\n\n`purgeCache()` from `@netlify/functions` invalidates the edge cache from inside a function:\n\n```ts\nimport { purgeCache } from \"@netlify/functions\"\n\nexport default async () => {\n  await purgeCache({ tags: [\"products\"] }) // omit tags to purge all\n  return new Response(\"Purged!\", { status: 202 })\n}\n```\n\n## Streaming responses\n\nReturn a `ReadableStream` as the `Response` body. Limits: **60s execution, 20 MB response**.\n\n```ts\nexport default async (req: Request) => {\n  const res = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${Netlify.env.get(\"OPENAI_API_KEY\")}`,\n    },\n    body: JSON.stringify({ model: \"gpt-4o-mini\", stream: true, messages: [/* ... */] }),\n  })\n  return new Response(res.body, { headers: { \"content-type\": \"text/event-stream\" } })\n}\n```\n\nTo build a stream manually, `new ReadableStream({ start(controller) { controller.enqueue(...); controller.close() } })`.\n\n## Background functions (long-running)\n\n`config.background: true`. Client gets an immediate `202`; the return value is discarded; runs up to **15 minutes**. No streaming. Retries: on invocation error, retry after 1 min, then again 2 min later. Send results somewhere other than the client.\n\n```ts title=\"netlify/functions/process.mts\"\nimport type { Config } from \"@netlify/functions\"\n\nexport default async (req: Request) => {\n  // Long-running work. Client already has its 202.\n}\n\nexport const config: Config = { background: true, path: \"/process\" }\n```\n\nLimits: background payload **256 KB**. Legacy `-background` filename suffix still works but prefer `config.background`.\n\n## Scheduled functions (cron)\n\n`config.schedule` with a cron expression, executed in **UTC**. The request body is JSON with `next_run` (ISO-8601). Inline config is TS/JS only — Go must use `netlify.toml`.\n\nAlways compute the UTC time for the target local hour. E.g. 9 AM ET → `\"0 13 * * *\"` UTC (note this shifts by an hour across DST; pick the UTC offset you need). Prefer explicit cron over `@daily`/`@hourly` shortcuts, which can't target a specific local hour.\n\n```ts title=\"netlify/functions/daily-digest.mts\"\nimport type { Config } from \"@netlify/functions\"\n\nexport default async (req: Request) => {\n  const { next_run } = await req.json()\n  console.log(\"Next invocation at:\", next_run)\n}\n\nexport const config: Config = {\n  schedule: \"0 13 * * *\", // 9 AM ET (EST); UTC\n}\n```\n\nVia `netlify.toml` (all languages):\n\n```toml\n[functions.\"daily-digest\"]\n  schedule = \"0 13 * * *\"\n```\n\nConstraints: **30s limit** (use background for longer); only fire on **published deploys** (not Deploy Previews/branch deploys — invoke manually with **Run now**); no URL invocation; no streaming; no request payloads/POST data; incompatible with Split Testing. All extensions supported **except** `@reboot` and `@annually`.\n\n## Platform-event functions\n\nExport a default object with handlers named after events. They always run in the background — no response to a client. Combine with `fetch` in the same function. Every handler is fully typed; import event types from `@netlify/functions`.\n\n```ts title=\"netlify/functions/on-deploy.mts\"\nimport type { DeploySucceededEvent, DeployFailedEvent } from \"@netlify/functions\"\n\nexport default {\n  deploySucceeded(event: DeploySucceededEvent) {\n    console.log(`Deploy ${event.deploy.id} succeeded for ${event.site.name}`)\n  },\n  deployFailed(event: DeployFailedEvent) {\n    console.log(`Deploy ${event.deploy.id} failed: ${event.deploy.errorMessage}`)\n  },\n}\n```\n\n**Deploy events** (`event.deploy`, `event.site`; return `void`): `deployBuilding`, `deploySucceeded`, `deployFailed`, `deployDeleted`, `deployLocked`, `deployUnlocked`.\n\n**Identity events** (`event.user`, only `id` guaranteed):\n\n| Handler | Can deny? | Can mutate? |\n|---|---|---|\n| `userValidate` | Yes | Yes |\n| `userSignup` | Yes | Yes |\n| `userLogin` | Yes | Yes |\n| `userModified` | Yes | Yes |\n| `userDeleted` | No | No |\n\n- Deny: call `event.deny()` inside the handler → end user gets `401`. First function to deny aborts the chain.\n- Mutate: return `{ user: {...} }` to persist changes; return `undefined` to pass through.\n\n**Form events**: `formSubmitted` → `event.data` (object keyed by field name). Return `void`.\n\nMultiple functions can handle the same event (all run). Netlify signs each event (JWS) and verifies before invoking, blocking external requests. Legacy filename convention (file named after the event, payload via `await req.json()` → `payload`) still works but prefer typed handlers.\n\n## Region\n\n⚠️ Do NOT override `config.region` unless the user states a specific reason (co-located DB/backend, data residency, regional audience). The default `cmh` (US East, Ohio) is deliberate.\n\nWhen justified — e.g. an EU-resident database:\n\n```ts\nexport const config: Config = { path: \"/eu-data\", region: \"dub\" }\n```\n\nAirport codes (self-serve): `cmh`, `dub`, `fra`, `gru`, `iad`, `lhr`, `nrt`, `pdx`, `sfo`, `sin`, `syd`, `yul`. Support-assisted: `cdg`, `mxp`. Each function runs in exactly one region (no multi-region geo-routing). Region selection needs Pro/Enterprise. Framework-adapter-generated functions can't take `export const config` — set region at project level in the UI under **Cloud compute > Functions > Region**. After changing region, **redeploy**. Function-level region beats the site-level UI setting.\n\n## Memory / vCPU\n\n⚠️ Do NOT set `config.memory` or `config.vcpu` speculatively — billing scales linearly with size. Raise them only for known memory/compute-intensive work (AI inference, image/PDF, large JSON/CSV) or observed OOM/timeouts caused by the function's own work.\n\nWhen justified (e.g. observed OOM processing large PDFs):\n\n```ts\nexport const config: Config = { path: \"/heavy\", memory: \"2gb\" } // or memory: 2048\n```\n\n- `memory`: 1024–4096 MB. `vcpu`: 0.5–2.0 (0.5 → 1024 MB, 2.0 → 4096 MB). Mutually exclusive; Netlify sizes the other automatically. Needs Credit-based Pro/Enterprise. Via `netlify.toml`: `[functions.heavy]\\n  memory = \"2gb\"`.\n\n## Bundling & files on disk\n\n⚠️ Files read from disk at runtime (`fs.readFile` on templates, JSON, WASM) are **not bundled**: works under `netlify dev`, ENOENT in production. Prefer importing static data as a module. Otherwise declare it in `netlify.toml`:\n\n```toml\n[functions]\n  included_files = [\"files/*.md\"]\n  external_node_modules = [\"package-1\"]\n```\n\n⚠️ The combined env-var limit is **~4 KB** for ALL functions (they run on AWS Lambda) — no Netlify setting raises it. Keep large payloads (service-account JSON, PEM keys) out of env vars; use a bundled file, Blobs, or a runtime fetch.\n\nJS-only esbuild: `[functions]\\n  node_bundler = \"esbuild\"`.\n\n## Limits (not configurable)\n\n- Synchronous execution: **60s**. Scheduled: **30s**. Background: **15 min**.\n- Buffered request/response payload: **6 MB** (binary is Base64-encoded, ~30% overhead → effective **4.5 MB** binary limit).\n- Streamed response: **20 MB**. Background payload: **256 KB**.\n\n## Local testing & deploy\n\n- Most frameworks emulate functions in their dev server. Vite frameworks (Astro, Nuxt, TanStack Start, React Router): install `@netlify/vite-plugin` and run the dev server. Next.js and anything else: use the [Netlify CLI](https://docs.netlify.com/api-and-cli-guides/cli-guides/local-development/) (`netlify dev`).\n- Scheduled functions don't fire on a schedule locally — invoke once with `netlify functions:invoke <name>`.\n- Deploy: push to Git for continuous deployment, or use the Netlify CLI/API.\n- Logs & metrics live in the Netlify UI; stream with the CLI. All deployed function versions appear under the **Functions** tab; use the search field at the top of the list to filter functions by name, and the separate filter to select a branch or enter a Deploy Preview number.\n\n## Node runtime version\n\nRuntime follows the build's Node.js version (fallback: Node.js 24). Override by setting env var `AWS_LAMBDA_JS_RUNTIME` (e.g. `nodejs24.x`) via UI/CLI/API — **not** `netlify.toml` — then redeploy. ES modules: `__dirname`/`__filename` unavailable, use `import.meta.url`; named imports of CommonJS packages fail, use a default import.\n\n## Legacy / Go (avoid unless needed)\n\nGo must use the [Lambda-compatible API](https://docs.netlify.com/build/functions/lambda-compatibility/?fn-language=go); Go routing/region/memory are set in `netlify.toml`. For migrating Lambda-style JS/TS, `@netlify/aws-lambda-compat` wraps an AWS handler:\n\n```ts\nimport { withLambda } from \"@netlify/aws-lambda-compat\"\nimport type { HandlerContext, HandlerEvent, HandlerResponse } from \"@netlify/aws-lambda-compat\"\n\nexport default withLambda(async (event: HandlerEvent, context: HandlerContext): Promise<HandlerResponse> => {\n  const name = event.queryStringParameters?.name ?? \"World\"\n  return { statusCode: 200, headers: { \"content-type\": \"application/json\" }, body: JSON.stringify({ name }) }\n})\n```\n\nLambda-compat mode enforces the 4 KB env-var limit; [upgrade to modern functions](https://developers.netlify.com/guides/migrating-to-the-modern-netlify-functions/) to remove it.\n\n<!-- system: agent-context/functions/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (functions)\n\nThese are org conventions, not docs facts — they are merged into the rendered\nskill by ctx-gen and are never generated. Extracted from the previous\nhand-written netlify-functions skill; owned by the skills maintainer.\n\n1. Use TypeScript (`.mts`) when possible.\n2. Access environment variables via `Netlify.env.get()` (prefer it over\n   `process.env` for consistency).\n3. Never add CORS headers unless explicitly requested.\n4. Store secrets in environment variables, never in code.\n5. `context.geo` and `context.ip` are mocked under `netlify dev` — placeholder\n   values, not the real location or client IP. Don't conclude geo code is\n   broken because local values never change; exercise branches with\n   `netlify dev --geo=mock --country=DE` and verify on a deploy.\n6. Do NOT set `config.memory` or `config.vcpu` speculatively. Raise them only\n   for known memory/compute-intensive work or observed OOM/timeouts caused by\n   the function's own work — billing scales linearly with size.\n7. Do NOT override `config.region` unless the user has stated a specific\n   reason (co-located database/backend, data residency, regional audience).\n   The `cmh` default is a deliberate choice.\n8. Files read from disk at runtime (`fs.readFile` on templates, JSON, WASM)\n   are not bundled: works under `netlify dev`, ENOENT in production. Prefer\n   importing static data as a module; otherwise declare the file with a\n   scoped `included_files` entry in `netlify.toml`.\n9. The ~4 KB combined environment-variable limit applies to ALL functions\n   (they run on AWS Lambda), not just Lambda-compat mode. Keep large payloads\n   (service-account JSON, PEM keys) out of env vars — use a bundled file,\n   Blobs, or a runtime fetch. No Netlify setting raises this cap.\n10. The body's FIRST function example must be the minimal default: no\n    `config` export at all, stating the function serves at\n    `/.netlify/functions/<name>`. Custom `path` routing appears only in a\n    later example — agents imitate the first example they see.\n11. Never demonstrate `memory`, `vcpu`, or `region` in a generic example —\n    show them only attached to an explicit stated reason (observed OOM,\n    co-located backend, data residency).\n12. Scheduled-function examples use a real cron expression with the UTC\n    conversion spelled out (e.g. 9 AM ET → `\"0 13 * * *\"` UTC, noting DST) —\n    never only `@hourly`/`@daily` shortcuts, which can't target a specific\n    local hour.\n13. The body must state that `[[headers]]` in netlify.toml, `_headers`, and\n    redirect header rules apply ONLY to static CDN responses — response\n    headers for a function are set in code on the returned `Response`.\n14. When asked to build a function that performs specific work (generate a\n    report, process an upload, send a digest), implement the work — pick a\n    real library where one is needed and write the operation end to end.\n    Never deliver the core task as a `not implemented` stub behind finished\n    plumbing: a function whose central branch throws is not a working\n    answer, however complete its config and routing.\n15. `config.background: true` is the documented, currently-supported way to\n    make a function background (the `-background` filename suffix also still\n    works). If a locally installed bundler doesn't recognize the flag,\n    suspect version skew first: check and upgrade the local tooling, and\n    keep local-compatibility findings separate from claims about platform\n    support — never remove the docs-recommended flag from an answer based\n    solely on an older installed schema.\n"

SKILL.md line diff

--- before
+++ after
@@ -1,168 +1,361 @@
 ---
 name: netlify-functions
-description: Guide for writing Netlify serverless functions. Use when creating API endpoints, background processing, scheduled tasks, or any server-side logic using Netlify Functions. Covers modern syntax (default export + Config), TypeScript, path routing, background functions, scheduled functions, streaming, and method routing.
+description: Write, configure, and deploy Netlify serverless functions in TypeScript, JavaScript, or Go. Use this when adding an API endpoint or backend route, adding a contact form handler, wiring auth or Identity signup/login hooks, building streaming or AI-proxy responses, scheduling cron jobs, running long background jobs (batch processing/scraping), reacting to deploy or form events, setting up rate limiting or region/memory config, or reading environment variables and secrets inside a function. Covers file locations, the Request/Context/Response handler shape, path routing, config options, and local testing with netlify dev.
 ---
 
 # Netlify Functions
 
-## Modern Syntax
+Reach for the modern default-handler API (`.mts` TypeScript). Export a default async handler taking a web `Request` and a Netlify `Context`, returning a web `Response`. Avoid the legacy AWS Lambda handler shape unless writing Go or migrating old code (see Legacy at the end).
 
-Always use the modern default export + Config pattern. Never use the legacy `exports.handler` or named `handler` export.
+## File locations
 
-```typescript
-import type { Context, Config } from "@netlify/functions";
+- Default directory: `netlify/functions/` (relative to base directory). Keep it **outside** your publish directory or source files ship as static assets.
+- A function is one file or a subdirectory whose entry file is named `index` or matches the subdirectory name. All of these create a function `hello`:
+  - `netlify/functions/hello.mts`
+  - `netlify/functions/hello/hello.mts`
+  - `netlify/functions/hello/index.mts`
+- Use `.mts` (TS) / `.mjs` (JS) for ES modules. `.cts`/`.cjs` force CommonJS; `.ts`/`.js` follow the nearest `package.json` `"type"`.
+
+## Minimal function
+
+No `config` export. Serves at `/.netlify/functions/hello`.
+
+```ts title="netlify/functions/hello.mts"
+import type { Context } from "@netlify/functions"
 
 export default async (req: Request, context: Context) => {
-  return new Response("Hello, world!");
-};
+  return new Response("Hello, world!")
+}
+```
 
-export const config: Config = {
-  path: "/api/hello",
-};
+Install types: `npm install @netlify/functions` (required for TS types; optional for JS).
+
+Read env vars and secrets with `Netlify.env.get()`:
+
+```ts
+const apiKey = Netlify.env.get("STRIPE_SECRET_KEY")
 ```
 
-The handler receives a standard Web API `Request` and returns a `Response`. The second argument is a Netlify `Context` object.
+Never hardcode secrets. For the variable to exist at runtime its scope must include **Functions**. Variables set in `netlify.toml` are NOT available to functions. Values are frozen per deploy — change them and redeploy to apply.
+
+**Response headers are set in code** on the returned `Response`. `[[headers]]` in `netlify.toml`, `_headers`, and redirect header rules apply ONLY to static CDN responses, not function responses. Do not add CORS headers unless explicitly requested.
+
+## Custom path routing
 
-## File Structure
+Set `config.path` to route to custom URLs. When set, the function serves ONLY at that path — not at `/.netlify/functions/<name>`.
 
-Place functions in `netlify/functions/`:
+```ts title="netlify/functions/travel.mts"
+import type { Config, Context } from "@netlify/functions"
 
+export default async (req: Request, context: Context) => {
+  const { city, country } = context.params
+  return new Response(`You're visiting ${city} in ${country}!`)
+}
+
+export const config: Config = {
+  path: "/travel-guide/:city/:country",
+}
 ```
-netlify/functions/
-  _shared/           # Non-function shared code (underscore prefix)
-    auth.ts
-    db.ts
-  items.ts           # -> /.netlify/functions/items (or custom path via config)
-  users/index.ts     # -> /.netlify/functions/users
+
+- Multiple paths: `path: ["/cats", "/dogs"]`.
+- Patterns: `path` supports [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) syntax — `path: ["/sale/*", "/item/:sku"]`. Named groups land on `context.params`. For the query string use `req.url`.
+- `excludedPath`: carve exceptions, e.g. `excludedPath: ["/product/*.css"]` with `path: "/product/*"`.
+- `preferStatic: true`: let a real static file at the URL win.
+- `method`: restrict methods, e.g. `method: ["GET", "POST"]`.
+
+## Fetchable module shape (alternative)
+
+Equivalent to the bare handler; carries `config` inline and lets you add event handlers.
+
+```ts
+import type { NetlifyFunction } from "@netlify/functions"
+
+export default {
+  fetch: (req, context) => new Response("Hello, world!"),
+  config: { path: "/hello" },
+} satisfies NetlifyFunction
 ```
 
-Use `.ts` or `.mts` extensions. If both `.ts` and `.js` exist with the same name, the `.js` file takes precedence.
+## Context object
 
-## Path Routing
+Second handler argument (or `getContext()` from `@netlify/functions` when out of handler scope — throws outside a request; wrap in try/catch).
 
-Define custom paths via the `config` export:
+- `context.params` — named path params.
+- `context.geo` — `city`, `country.code/name`, `latitude`, `longitude`, `subdivision`, `timezone`, `postalCode`.
+- `context.ip` — client IP string.
+- `context.cookies` — `get(name)` / `set(options)` / `delete(name|options)`. Cross-subdomain cookies need a custom domain (`netlify.app` is on the Public Suffix List).
+- `context.site` — `id`, `name`, `url`. `context.deploy` — `context`, `id`, `published`, `skewProtectionToken`. `context.account.id`. `context.server.region`. `context.requestId`.
+- `context.waitUntil(promise)` — run work after the response is sent (analytics, logs) without blocking. Billing/log duration counts until the promise settles. Available for functions deployed on/after 2025-03-20.
 
-```typescript
-export const config: Config = {
-  path: "/api/items",                    // Static path
-  // path: "/api/items/:id",            // Path parameter
-  // path: ["/api/items", "/api/items/:id"], // Multiple paths
-  // excludedPath: "/api/items/special", // Excluded paths
-  // preferStatic: true,                // Don't override static files
-};
+⚠️ Under `netlify dev`, `context.geo` and `context.ip` are **mocked** — placeholder values that never change. Don't conclude geo code is broken locally. Exercise branches with `netlify dev --geo=mock --country=DE` and verify on a real deploy.
+
+## Config object
+
+Export `const config` (or the `config` property of a Fetchable module):
+
+- `path` / `excludedPath` — `string | string[]`, must start with `/`.
+- `method` — one method or array.
+- `preferStatic` — `boolean`.
+- `background` — `boolean` (see Background).
+- `schedule` — cron string (see Scheduled). Mutually exclusive with `path`/`excludedPath`.
+- `rateLimit` — `{ action: 'rate_limit'|'rewrite', aggregateBy: 'domain'|'ip'|[...], to?, windowSize, windowLimit }`.
+- `memory` / `vcpu` — see below; mutually exclusive.
+- `region` — airport code; see below.
+
+## Integrations
+
+```ts title="netlify/functions/users.mts"
+import type { Config } from "@netlify/functions"
+import { getDatabase } from "@netlify/database"
+
+const db = getDatabase()
+
+export default async (req: Request) => {
+  const users = await db.sql`SELECT id, email FROM users LIMIT 10`
+  return Response.json({ users })
+}
+
+export const config: Config = { path: "/users" }
 ```
 
-Without a `path` config, functions are available at `/.netlify/functions/{name}`. Setting a `path` makes the function available **only** at that path.
+Blobs: `import { getStore } from "@netlify/blobs"`; `getStore("uploads").set(key, await req.blob())`.
 
-Access path parameters via `context.params`:
+`purgeCache()` from `@netlify/functions` invalidates the edge cache from inside a function:
 
-```typescript
-// config: { path: "/api/items/:id" }
-export default async (req: Request, context: Context) => {
-  const { id } = context.params;
-  // ...
-};
+```ts
+import { purgeCache } from "@netlify/functions"
+
+export default async () => {
+  await purgeCache({ tags: ["products"] }) // omit tags to purge all
+  return new Response("Purged!", { status: 202 })
+}
 ```
 
-## Method Routing
+## Streaming responses
 
-```typescript
-export default async (req: Request, context: Context) => {
-  switch (req.method) {
-    case "GET":    return handleGet(context.params.id);
-    case "POST":   return handlePost(await req.json());
-    case "DELETE": return handleDelete(context.params.id);
-    default:       return new Response("Method not allowed", { status: 405 });
-  }
-};
+Return a `ReadableStream` as the `Response` body. Limits: **60s execution, 20 MB response**.
 
-export const config: Config = {
-  path: "/api/items/:id",
-  method: ["GET", "POST", "DELETE"],
-};
+```ts
+export default async (req: Request) => {
+  const res = await fetch("https://api.openai.com/v1/chat/completions", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+      Authorization: `Bearer ${Netlify.env.get("OPENAI_API_KEY")}`,
+    },
+    body: JSON.stringify({ model: "gpt-4o-mini", stream: true, messages: [/* ... */] }),
+  })
+  return new Response(res.body, { headers: { "content-type": "text/event-stream" } })
+}
 ```
 
-## Background Functions
+To build a stream manually, `new ReadableStream({ start(controller) { controller.enqueue(...); controller.close() } })`.
 
-For long-running tasks (up to 15 minutes). The client receives an immediate `202` response; return values are ignored.
+## Background functions (long-running)
 
-Name the file with a `-background` suffix:
+`config.background: true`. Client gets an immediate `202`; the return value is discarded; runs up to **15 minutes**. No streaming. Retries: on invocation error, retry after 1 min, then again 2 min later. Send results somewhere other than the client.
 
+```ts title="netlify/functions/process.mts"
+import type { Config } from "@netlify/functions"
+
+export default async (req: Request) => {
+  // Long-running work. Client already has its 202.
+}
+
+export const config: Config = { background: true, path: "/process" }
 ```
-netlify/functions/process-background.ts
-```
 
-Store results externally (Netlify Blobs, database) for later retrieval.
+Limits: background payload **256 KB**. Legacy `-background` filename suffix still works but prefer `config.background`.
+
+## Scheduled functions (cron)
+
+`config.schedule` with a cron expression, executed in **UTC**. The request body is JSON with `next_run` (ISO-8601). Inline config is TS/JS only — Go must use `netlify.toml`.
 
-## Scheduled Functions
+Always compute the UTC time for the target local hour. E.g. 9 AM ET → `"0 13 * * *"` UTC (note this shifts by an hour across DST; pick the UTC offset you need). Prefer explicit cron over `@daily`/`@hourly` shortcuts, which can't target a specific local hour.
 
-Run on a cron schedule (UTC timezone):
+```ts title="netlify/functions/daily-digest.mts"
+import type { Config } from "@netlify/functions"
 
-```typescript
 export default async (req: Request) => {
-  const { next_run } = await req.json();
-  console.log("Next invocation at:", next_run);
-};
+  const { next_run } = await req.json()
+  console.log("Next invocation at:", next_run)
+}
 
 export const config: Config = {
-  schedule: "@hourly", // or cron: "0 * * * *"
-};
+  schedule: "0 13 * * *", // 9 AM ET (EST); UTC
+}
 ```
 
-Shortcuts: `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`. Scheduled functions have a **30-second timeout** and only run on published deploys.
+Via `netlify.toml` (all languages):
 
-## Streaming Responses
+```toml
+[functions."daily-digest"]
+  schedule = "0 13 * * *"
+```
 
-Return a `ReadableStream` body for streamed responses (up to 20 MB):
+Constraints: **30s limit** (use background for longer); only fire on **published deploys** (not Deploy Previews/branch deploys — invoke manually with **Run now**); no URL invocation; no streaming; no request payloads/POST data; incompatible with Split Testing. All extensions supported **except** `@reboot` and `@annually`.
 
-```typescript
-export default async (req: Request) => {
-  const stream = new ReadableStream({ /* ... */ });
-  return new Response(stream, {
-    headers: { "Content-Type": "text/event-stream" },
-  });
-};
+## Platform-event functions
+
+Export a default object with handlers named after events. They always run in the background — no response to a client. Combine with `fetch` in the same function. Every handler is fully typed; import event types from `@netlify/functions`.
+
+```ts title="netlify/functions/on-deploy.mts"
+import type { DeploySucceededEvent, DeployFailedEvent } from "@netlify/functions"
+
+export default {
+  deploySucceeded(event: DeploySucceededEvent) {
+    console.log(`Deploy ${event.deploy.id} succeeded for ${event.site.name}`)
+  },
+  deployFailed(event: DeployFailedEvent) {
+    console.log(`Deploy ${event.deploy.id} failed: ${event.deploy.errorMessage}`)
+  },
+}
 ```
 
-## Context Object
+**Deploy events** (`event.deploy`, `event.site`; return `void`): `deployBuilding`, `deploySucceeded`, `deployFailed`, `deployDeleted`, `deployLocked`, `deployUnlocked`.
+
+**Identity events** (`event.user`, only `id` guaranteed):
 
-| Property | Description |
-|---|---|
-| `context.params` | Path parameters from config |
-| `context.geo` | `{ city, country: {code, name}, latitude, longitude, subdivision, timezone, postalCode }` |
-| `context.ip` | Client IP address |
-| `context.cookies` | `.get()`, `.set()`, `.delete()` |
-| `context.deploy` | `{ context, id, published }` |
-| `context.site` | `{ id, name, url }` |
-| `context.account.id` | Team account ID |
-| `context.requestId` | Unique request ID |
-| `context.waitUntil(promise)` | Extend execution after response is sent |
+| Handler | Can deny? | Can mutate? |
+|---|---|---|
+| `userValidate` | Yes | Yes |
+| `userSignup` | Yes | Yes |
+| `userLogin` | Yes | Yes |
+| `userModified` | Yes | Yes |
+| `userDeleted` | No | No |
 
-## Environment Variables
+- Deny: call `event.deny()` inside the handler → end user gets `401`. First function to deny aborts the chain.
+- Mutate: return `{ user: {...} }` to persist changes; return `undefined` to pass through.
 
-Use `Netlify.env` (not `process.env`) inside functions:
+**Form events**: `formSubmitted` → `event.data` (object keyed by field name). Return `void`.
 
-```typescript
-const apiKey = Netlify.env.get("API_KEY");
+Multiple functions can handle the same event (all run). Netlify signs each event (JWS) and verifies before invoking, blocking external requests. Legacy filename convention (file named after the event, payload via `await req.json()` → `payload`) still works but prefer typed handlers.
+
+## Region
+
+⚠️ Do NOT override `config.region` unless the user states a specific reason (co-located DB/backend, data residency, regional audience). The default `cmh` (US East, Ohio) is deliberate.
+
+When justified — e.g. an EU-resident database:
+
+```ts
+export const config: Config = { path: "/eu-data", region: "dub" }
 ```
 
-## Resource Limits
+Airport codes (self-serve): `cmh`, `dub`, `fra`, `gru`, `iad`, `lhr`, `nrt`, `pdx`, `sfo`, `sin`, `syd`, `yul`. Support-assisted: `cdg`, `mxp`. Each function runs in exactly one region (no multi-region geo-routing). Region selection needs Pro/Enterprise. Framework-adapter-generated functions can't take `export const config` — set region at project level in the UI under **Cloud compute > Functions > Region**. After changing region, **redeploy**. Function-level region beats the site-level UI setting.
+
+## Memory / vCPU
+
+⚠️ Do NOT set `config.memory` or `config.vcpu` speculatively — billing scales linearly with size. Raise them only for known memory/compute-intensive work (AI inference, image/PDF, large JSON/CSV) or observed OOM/timeouts caused by the function's own work.
+
+When justified (e.g. observed OOM processing large PDFs):
+
+```ts
+export const config: Config = { path: "/heavy", memory: "2gb" } // or memory: 2048
+```
 
-| Resource | Limit |
-|---|---|
-| Synchronous timeout | 60 seconds |
-| Background timeout | 15 minutes |
-| Scheduled timeout | 30 seconds |
-| Memory | 1024 MB |
-| Buffered payload | 6 MB |
-| Streamed payload | 20 MB |
+- `memory`: 1024–4096 MB. `vcpu`: 0.5–2.0 (0.5 → 1024 MB, 2.0 → 4096 MB). Mutually exclusive; Netlify sizes the other automatically. Needs Credit-based Pro/Enterprise. Via `netlify.toml`: `[functions.heavy]\n  memory = "2gb"`.
+
+## Bundling & files on disk
+
+⚠️ Files read from disk at runtime (`fs.readFile` on templates, JSON, WASM) are **not bundled**: works under `netlify dev`, ENOENT in production. Prefer importing static data as a module. Otherwise declare it in `netlify.toml`:
+
+```toml
+[functions]
+  included_files = ["files/*.md"]
+  external_node_modules = ["package-1"]
+```
+
+⚠️ The combined env-var limit is **~4 KB** for ALL functions (they run on AWS Lambda) — no Netlify setting raises it. Keep large payloads (service-account JSON, PEM keys) out of env vars; use a bundled file, Blobs, or a runtime fetch.
+
+JS-only esbuild: `[functions]\n  node_bundler = "esbuild"`.
+
+## Limits (not configurable)
+
+- Synchronous execution: **60s**. Scheduled: **30s**. Background: **15 min**.
+- Buffered request/response payload: **6 MB** (binary is Base64-encoded, ~30% overhead → effective **4.5 MB** binary limit).
+- Streamed response: **20 MB**. Background payload: **256 KB**.
+
+## Local testing & deploy
+
+- Most frameworks emulate functions in their dev server. Vite frameworks (Astro, Nuxt, TanStack Start, React Router): install `@netlify/vite-plugin` and run the dev server. Next.js and anything else: use the [Netlify CLI](https://docs.netlify.com/api-and-cli-guides/cli-guides/local-development/) (`netlify dev`).
+- Scheduled functions don't fire on a schedule locally — invoke once with `netlify functions:invoke <name>`.
+- Deploy: push to Git for continuous deployment, or use the Netlify CLI/API.
+- Logs & metrics live in the Netlify UI; stream with the CLI. All deployed function versions appear under the **Functions** tab; use the search field at the top of the list to filter functions by name, and the separate filter to select a branch or enter a Deploy Preview number.
+
+## Node runtime version
+
+Runtime follows the build's Node.js version (fallback: Node.js 24). Override by setting env var `AWS_LAMBDA_JS_RUNTIME` (e.g. `nodejs24.x`) via UI/CLI/API — **not** `netlify.toml` — then redeploy. ES modules: `__dirname`/`__filename` unavailable, use `import.meta.url`; named imports of CommonJS packages fail, use a default import.
+
+## Legacy / Go (avoid unless needed)
+
+Go must use the [Lambda-compatible API](https://docs.netlify.com/build/functions/lambda-compatibility/?fn-language=go); Go routing/region/memory are set in `netlify.toml`. For migrating Lambda-style JS/TS, `@netlify/aws-lambda-compat` wraps an AWS handler:
+
+```ts
+import { withLambda } from "@netlify/aws-lambda-compat"
+import type { HandlerContext, HandlerEvent, HandlerResponse } from "@netlify/aws-lambda-compat"
+
+export default withLambda(async (event: HandlerEvent, context: HandlerContext): Promise<HandlerResponse> => {
+  const name = event.queryStringParameters?.name ?? "World"
+  return { statusCode: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ name }) }
+})
+```
 
-## Framework Considerations
+Lambda-compat mode enforces the 4 KB env-var limit; [upgrade to modern functions](https://developers.netlify.com/guides/migrating-to-the-modern-netlify-functions/) to remove it.
 
-Frameworks with server-side capabilities (Astro, Next.js, Nuxt, SvelteKit, TanStack Start) typically generate their own serverless functions via adapters. You usually do not write raw Netlify Functions in these projects — the framework adapter handles server-side rendering and API routes. Write Netlify Functions directly when:
+<!-- system: agent-context/functions/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->
+# Netlify house rules (functions)
 
-- Using a client-side-only framework (Vite + React SPA, vanilla JS)
-- Adding background or scheduled tasks to any project
-- Building standalone API endpoints outside the framework's routing
+These are org conventions, not docs facts — they are merged into the rendered
+skill by ctx-gen and are never generated. Extracted from the previous
+hand-written netlify-functions skill; owned by the skills maintainer.
 
-See the **netlify-frameworks** skill for adapter setup.
+1. Use TypeScript (`.mts`) when possible.
+2. Access environment variables via `Netlify.env.get()` (prefer it over
+   `process.env` for consistency).
+3. Never add CORS headers unless explicitly requested.
+4. Store secrets in environment variables, never in code.
+5. `context.geo` and `context.ip` are mocked under `netlify dev` — placeholder
+   values, not the real location or client IP. Don't conclude geo code is
+   broken because local values never change; exercise branches with
+   `netlify dev --geo=mock --country=DE` and verify on a deploy.
+6. Do NOT set `config.memory` or `config.vcpu` speculatively. Raise them only
+   for known memory/compute-intensive work or observed OOM/timeouts caused by
+   the function's own work — billing scales linearly with size.
+7. Do NOT override `config.region` unless the user has stated a specific
+   reason (co-located database/backend, data residency, regional audience).
+   The `cmh` default is a deliberate choice.
+8. Files read from disk at runtime (`fs.readFile` on templates, JSON, WASM)
+   are not bundled: works under `netlify dev`, ENOENT in production. Prefer
+   importing static data as a module; otherwise declare the file with a
+   scoped `included_files` entry in `netlify.toml`.
+9. The ~4 KB combined environment-variable limit applies to ALL functions
+   (they run on AWS Lambda), not just Lambda-compat mode. Keep large payloads
+   (service-account JSON, PEM keys) out of env vars — use a bundled file,
+   Blobs, or a runtime fetch. No Netlify setting raises this cap.
+10. The body's FIRST function example must be the minimal default: no
+    `config` export at all, stating the function serves at
+    `/.netlify/functions/<name>`. Custom `path` routing appears only in a
+    later example — agents imitate the first example they see.
+11. Never demonstrate `memory`, `vcpu`, or `region` in a generic example —
+    show them only attached to an explicit stated reason (observed OOM,
+    co-located backend, data residency).
+12. Scheduled-function examples use a real cron expression with the UTC
+    conversion spelled out (e.g. 9 AM ET → `"0 13 * * *"` UTC, noting DST) —
+    never only `@hourly`/`@daily` shortcuts, which can't target a specific
+    local hour.
+13. The body must state that `[[headers]]` in netlify.toml, `_headers`, and
+    redirect header rules apply ONLY to static CDN responses — response
+    headers for a function are set in code on the returned `Response`.
+14. When asked to build a function that performs specific work (generate a
+    report, process an upload, send a digest), implement the work — pick a
+    real library where one is needed and write the operation end to end.
+    Never deliver the core task as a `not implemented` stub behind finished
+    plumbing: a function whose central branch throws is not a working
+    answer, however complete its config and routing.
+15. `config.background: true` is the documented, currently-supported way to
+    make a function background (the `-background` filename suffix also still
+    works). If a locally installed bundler doesn't recognize the flag,
+    suspect version skew first: check and upgrade the local tooling, and
+    keep local-compatibility findings separate from claims about platform
+    support — never remove the docs-recommended flag from an answer based
+    solely on an older installed schema.
Full snapshot data
{
  "description": "Write, configure, and deploy Netlify serverless functions in TypeScript, JavaScript, or Go. Use this when adding an API endpoint or backend route, adding a contact form handler, wiring auth or Identity signup/login hooks, building streaming or AI-proxy responses, scheduling cron jobs, running long background jobs (batch processing/scraping), reacting to deploy or form events, setting up rate limiting or region/memory config, or reading environment variables and secrets inside a function. Covers file locations, the Request/Context/Response handler shape, path routing, config options, and local testing with netlify dev.",
  "included_files": [],
  "name": "netlify-functions",
  "skill_md_contents": "---\nname: netlify-functions\ndescription: Write, configure, and deploy Netlify serverless functions in TypeScript, JavaScript, or Go. Use this when adding an API endpoint or backend route, adding a contact form handler, wiring auth or Identity signup/login hooks, building streaming or AI-proxy responses, scheduling cron jobs, running long background jobs (batch processing/scraping), reacting to deploy or form events, setting up rate limiting or region/memory config, or reading environment variables and secrets inside a function. Covers file locations, the Request/Context/Response handler shape, path routing, config options, and local testing with netlify dev.\n---\n\n# Netlify Functions\n\nReach for the modern default-handler API (`.mts` TypeScript). Export a default async handler taking a web `Request` and a Netlify `Context`, returning a web `Response`. Avoid the legacy AWS Lambda handler shape unless writing Go or migrating old code (see Legacy at the end).\n\n## File locations\n\n- Default directory: `netlify/functions/` (relative to base directory). Keep it **outside** your publish directory or source files ship as static assets.\n- A function is one file or a subdirectory whose entry file is named `index` or matches the subdirectory name. All of these create a function `hello`:\n  - `netlify/functions/hello.mts`\n  - `netlify/functions/hello/hello.mts`\n  - `netlify/functions/hello/index.mts`\n- Use `.mts` (TS) / `.mjs` (JS) for ES modules. `.cts`/`.cjs` force CommonJS; `.ts`/`.js` follow the nearest `package.json` `\"type\"`.\n\n## Minimal function\n\nNo `config` export. Serves at `/.netlify/functions/hello`.\n\n```ts title=\"netlify/functions/hello.mts\"\nimport type { Context } from \"@netlify/functions\"\n\nexport default async (req: Request, context: Context) => {\n  return new Response(\"Hello, world!\")\n}\n```\n\nInstall types: `npm install @netlify/functions` (required for TS types; optional for JS).\n\nRead env vars and secrets with `Netlify.env.get()`:\n\n```ts\nconst apiKey = Netlify.env.get(\"STRIPE_SECRET_KEY\")\n```\n\nNever hardcode secrets. For the variable to exist at runtime its scope must include **Functions**. Variables set in `netlify.toml` are NOT available to functions. Values are frozen per deploy — change them and redeploy to apply.\n\n**Response headers are set in code** on the returned `Response`. `[[headers]]` in `netlify.toml`, `_headers`, and redirect header rules apply ONLY to static CDN responses, not function responses. Do not add CORS headers unless explicitly requested.\n\n## Custom path routing\n\nSet `config.path` to route to custom URLs. When set, the function serves ONLY at that path — not at `/.netlify/functions/<name>`.\n\n```ts title=\"netlify/functions/travel.mts\"\nimport type { Config, Context } from \"@netlify/functions\"\n\nexport default async (req: Request, context: Context) => {\n  const { city, country } = context.params\n  return new Response(`You're visiting ${city} in ${country}!`)\n}\n\nexport const config: Config = {\n  path: \"/travel-guide/:city/:country\",\n}\n```\n\n- Multiple paths: `path: [\"/cats\", \"/dogs\"]`.\n- Patterns: `path` supports [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) syntax — `path: [\"/sale/*\", \"/item/:sku\"]`. Named groups land on `context.params`. For the query string use `req.url`.\n- `excludedPath`: carve exceptions, e.g. `excludedPath: [\"/product/*.css\"]` with `path: \"/product/*\"`.\n- `preferStatic: true`: let a real static file at the URL win.\n- `method`: restrict methods, e.g. `method: [\"GET\", \"POST\"]`.\n\n## Fetchable module shape (alternative)\n\nEquivalent to the bare handler; carries `config` inline and lets you add event handlers.\n\n```ts\nimport type { NetlifyFunction } from \"@netlify/functions\"\n\nexport default {\n  fetch: (req, context) => new Response(\"Hello, world!\"),\n  config: { path: \"/hello\" },\n} satisfies NetlifyFunction\n```\n\n## Context object\n\nSecond handler argument (or `getContext()` from `@netlify/functions` when out of handler scope — throws outside a request; wrap in try/catch).\n\n- `context.params` — named path params.\n- `context.geo` — `city`, `country.code/name`, `latitude`, `longitude`, `subdivision`, `timezone`, `postalCode`.\n- `context.ip` — client IP string.\n- `context.cookies` — `get(name)` / `set(options)` / `delete(name|options)`. Cross-subdomain cookies need a custom domain (`netlify.app` is on the Public Suffix List).\n- `context.site` — `id`, `name`, `url`. `context.deploy` — `context`, `id`, `published`, `skewProtectionToken`. `context.account.id`. `context.server.region`. `context.requestId`.\n- `context.waitUntil(promise)` — run work after the response is sent (analytics, logs) without blocking. Billing/log duration counts until the promise settles. Available for functions deployed on/after 2025-03-20.\n\n⚠️ Under `netlify dev`, `context.geo` and `context.ip` are **mocked** — placeholder values that never change. Don't conclude geo code is broken locally. Exercise branches with `netlify dev --geo=mock --country=DE` and verify on a real deploy.\n\n## Config object\n\nExport `const config` (or the `config` property of a Fetchable module):\n\n- `path` / `excludedPath` — `string | string[]`, must start with `/`.\n- `method` — one method or array.\n- `preferStatic` — `boolean`.\n- `background` — `boolean` (see Background).\n- `schedule` — cron string (see Scheduled). Mutually exclusive with `path`/`excludedPath`.\n- `rateLimit` — `{ action: 'rate_limit'|'rewrite', aggregateBy: 'domain'|'ip'|[...], to?, windowSize, windowLimit }`.\n- `memory` / `vcpu` — see below; mutually exclusive.\n- `region` — airport code; see below.\n\n## Integrations\n\n```ts title=\"netlify/functions/users.mts\"\nimport type { Config } from \"@netlify/functions\"\nimport { getDatabase } from \"@netlify/database\"\n\nconst db = getDatabase()\n\nexport default async (req: Request) => {\n  const users = await db.sql`SELECT id, email FROM users LIMIT 10`\n  return Response.json({ users })\n}\n\nexport const config: Config = { path: \"/users\" }\n```\n\nBlobs: `import { getStore } from \"@netlify/blobs\"`; `getStore(\"uploads\").set(key, await req.blob())`.\n\n`purgeCache()` from `@netlify/functions` invalidates the edge cache from inside a function:\n\n```ts\nimport { purgeCache } from \"@netlify/functions\"\n\nexport default async () => {\n  await purgeCache({ tags: [\"products\"] }) // omit tags to purge all\n  return new Response(\"Purged!\", { status: 202 })\n}\n```\n\n## Streaming responses\n\nReturn a `ReadableStream` as the `Response` body. Limits: **60s execution, 20 MB response**.\n\n```ts\nexport default async (req: Request) => {\n  const res = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${Netlify.env.get(\"OPENAI_API_KEY\")}`,\n    },\n    body: JSON.stringify({ model: \"gpt-4o-mini\", stream: true, messages: [/* ... */] }),\n  })\n  return new Response(res.body, { headers: { \"content-type\": \"text/event-stream\" } })\n}\n```\n\nTo build a stream manually, `new ReadableStream({ start(controller) { controller.enqueue(...); controller.close() } })`.\n\n## Background functions (long-running)\n\n`config.background: true`. Client gets an immediate `202`; the return value is discarded; runs up to **15 minutes**. No streaming. Retries: on invocation error, retry after 1 min, then again 2 min later. Send results somewhere other than the client.\n\n```ts title=\"netlify/functions/process.mts\"\nimport type { Config } from \"@netlify/functions\"\n\nexport default async (req: Request) => {\n  // Long-running work. Client already has its 202.\n}\n\nexport const config: Config = { background: true, path: \"/process\" }\n```\n\nLimits: background payload **256 KB**. Legacy `-background` filename suffix still works but prefer `config.background`.\n\n## Scheduled functions (cron)\n\n`config.schedule` with a cron expression, executed in **UTC**. The request body is JSON with `next_run` (ISO-8601). Inline config is TS/JS only — Go must use `netlify.toml`.\n\nAlways compute the UTC time for the target local hour. E.g. 9 AM ET → `\"0 13 * * *\"` UTC (note this shifts by an hour across DST; pick the UTC offset you need). Prefer explicit cron over `@daily`/`@hourly` shortcuts, which can't target a specific local hour.\n\n```ts title=\"netlify/functions/daily-digest.mts\"\nimport type { Config } from \"@netlify/functions\"\n\nexport default async (req: Request) => {\n  const { next_run } = await req.json()\n  console.log(\"Next invocation at:\", next_run)\n}\n\nexport const config: Config = {\n  schedule: \"0 13 * * *\", // 9 AM ET (EST); UTC\n}\n```\n\nVia `netlify.toml` (all languages):\n\n```toml\n[functions.\"daily-digest\"]\n  schedule = \"0 13 * * *\"\n```\n\nConstraints: **30s limit** (use background for longer); only fire on **published deploys** (not Deploy Previews/branch deploys — invoke manually with **Run now**); no URL invocation; no streaming; no request payloads/POST data; incompatible with Split Testing. All extensions supported **except** `@reboot` and `@annually`.\n\n## Platform-event functions\n\nExport a default object with handlers named after events. They always run in the background — no response to a client. Combine with `fetch` in the same function. Every handler is fully typed; import event types from `@netlify/functions`.\n\n```ts title=\"netlify/functions/on-deploy.mts\"\nimport type { DeploySucceededEvent, DeployFailedEvent } from \"@netlify/functions\"\n\nexport default {\n  deploySucceeded(event: DeploySucceededEvent) {\n    console.log(`Deploy ${event.deploy.id} succeeded for ${event.site.name}`)\n  },\n  deployFailed(event: DeployFailedEvent) {\n    console.log(`Deploy ${event.deploy.id} failed: ${event.deploy.errorMessage}`)\n  },\n}\n```\n\n**Deploy events** (`event.deploy`, `event.site`; return `void`): `deployBuilding`, `deploySucceeded`, `deployFailed`, `deployDeleted`, `deployLocked`, `deployUnlocked`.\n\n**Identity events** (`event.user`, only `id` guaranteed):\n\n| Handler | Can deny? | Can mutate? |\n|---|---|---|\n| `userValidate` | Yes | Yes |\n| `userSignup` | Yes | Yes |\n| `userLogin` | Yes | Yes |\n| `userModified` | Yes | Yes |\n| `userDeleted` | No | No |\n\n- Deny: call `event.deny()` inside the handler → end user gets `401`. First function to deny aborts the chain.\n- Mutate: return `{ user: {...} }` to persist changes; return `undefined` to pass through.\n\n**Form events**: `formSubmitted` → `event.data` (object keyed by field name). Return `void`.\n\nMultiple functions can handle the same event (all run). Netlify signs each event (JWS) and verifies before invoking, blocking external requests. Legacy filename convention (file named after the event, payload via `await req.json()` → `payload`) still works but prefer typed handlers.\n\n## Region\n\n⚠️ Do NOT override `config.region` unless the user states a specific reason (co-located DB/backend, data residency, regional audience). The default `cmh` (US East, Ohio) is deliberate.\n\nWhen justified — e.g. an EU-resident database:\n\n```ts\nexport const config: Config = { path: \"/eu-data\", region: \"dub\" }\n```\n\nAirport codes (self-serve): `cmh`, `dub`, `fra`, `gru`, `iad`, `lhr`, `nrt`, `pdx`, `sfo`, `sin`, `syd`, `yul`. Support-assisted: `cdg`, `mxp`. Each function runs in exactly one region (no multi-region geo-routing). Region selection needs Pro/Enterprise. Framework-adapter-generated functions can't take `export const config` — set region at project level in the UI under **Cloud compute > Functions > Region**. After changing region, **redeploy**. Function-level region beats the site-level UI setting.\n\n## Memory / vCPU\n\n⚠️ Do NOT set `config.memory` or `config.vcpu` speculatively — billing scales linearly with size. Raise them only for known memory/compute-intensive work (AI inference, image/PDF, large JSON/CSV) or observed OOM/timeouts caused by the function's own work.\n\nWhen justified (e.g. observed OOM processing large PDFs):\n\n```ts\nexport const config: Config = { path: \"/heavy\", memory: \"2gb\" } // or memory: 2048\n```\n\n- `memory`: 1024–4096 MB. `vcpu`: 0.5–2.0 (0.5 → 1024 MB, 2.0 → 4096 MB). Mutually exclusive; Netlify sizes the other automatically. Needs Credit-based Pro/Enterprise. Via `netlify.toml`: `[functions.heavy]\\n  memory = \"2gb\"`.\n\n## Bundling & files on disk\n\n⚠️ Files read from disk at runtime (`fs.readFile` on templates, JSON, WASM) are **not bundled**: works under `netlify dev`, ENOENT in production. Prefer importing static data as a module. Otherwise declare it in `netlify.toml`:\n\n```toml\n[functions]\n  included_files = [\"files/*.md\"]\n  external_node_modules = [\"package-1\"]\n```\n\n⚠️ The combined env-var limit is **~4 KB** for ALL functions (they run on AWS Lambda) — no Netlify setting raises it. Keep large payloads (service-account JSON, PEM keys) out of env vars; use a bundled file, Blobs, or a runtime fetch.\n\nJS-only esbuild: `[functions]\\n  node_bundler = \"esbuild\"`.\n\n## Limits (not configurable)\n\n- Synchronous execution: **60s**. Scheduled: **30s**. Background: **15 min**.\n- Buffered request/response payload: **6 MB** (binary is Base64-encoded, ~30% overhead → effective **4.5 MB** binary limit).\n- Streamed response: **20 MB**. Background payload: **256 KB**.\n\n## Local testing & deploy\n\n- Most frameworks emulate functions in their dev server. Vite frameworks (Astro, Nuxt, TanStack Start, React Router): install `@netlify/vite-plugin` and run the dev server. Next.js and anything else: use the [Netlify CLI](https://docs.netlify.com/api-and-cli-guides/cli-guides/local-development/) (`netlify dev`).\n- Scheduled functions don't fire on a schedule locally — invoke once with `netlify functions:invoke <name>`.\n- Deploy: push to Git for continuous deployment, or use the Netlify CLI/API.\n- Logs & metrics live in the Netlify UI; stream with the CLI. All deployed function versions appear under the **Functions** tab; use the search field at the top of the list to filter functions by name, and the separate filter to select a branch or enter a Deploy Preview number.\n\n## Node runtime version\n\nRuntime follows the build's Node.js version (fallback: Node.js 24). Override by setting env var `AWS_LAMBDA_JS_RUNTIME` (e.g. `nodejs24.x`) via UI/CLI/API — **not** `netlify.toml` — then redeploy. ES modules: `__dirname`/`__filename` unavailable, use `import.meta.url`; named imports of CommonJS packages fail, use a default import.\n\n## Legacy / Go (avoid unless needed)\n\nGo must use the [Lambda-compatible API](https://docs.netlify.com/build/functions/lambda-compatibility/?fn-language=go); Go routing/region/memory are set in `netlify.toml`. For migrating Lambda-style JS/TS, `@netlify/aws-lambda-compat` wraps an AWS handler:\n\n```ts\nimport { withLambda } from \"@netlify/aws-lambda-compat\"\nimport type { HandlerContext, HandlerEvent, HandlerResponse } from \"@netlify/aws-lambda-compat\"\n\nexport default withLambda(async (event: HandlerEvent, context: HandlerContext): Promise<HandlerResponse> => {\n  const name = event.queryStringParameters?.name ?? \"World\"\n  return { statusCode: 200, headers: { \"content-type\": \"application/json\" }, body: JSON.stringify({ name }) }\n})\n```\n\nLambda-compat mode enforces the 4 KB env-var limit; [upgrade to modern functions](https://developers.netlify.com/guides/migrating-to-the-modern-netlify-functions/) to remove it.\n\n<!-- system: agent-context/functions/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (functions)\n\nThese are org conventions, not docs facts — they are merged into the rendered\nskill by ctx-gen and are never generated. Extracted from the previous\nhand-written netlify-functions skill; owned by the skills maintainer.\n\n1. Use TypeScript (`.mts`) when possible.\n2. Access environment variables via `Netlify.env.get()` (prefer it over\n   `process.env` for consistency).\n3. Never add CORS headers unless explicitly requested.\n4. Store secrets in environment variables, never in code.\n5. `context.geo` and `context.ip` are mocked under `netlify dev` — placeholder\n   values, not the real location or client IP. Don't conclude geo code is\n   broken because local values never change; exercise branches with\n   `netlify dev --geo=mock --country=DE` and verify on a deploy.\n6. Do NOT set `config.memory` or `config.vcpu` speculatively. Raise them only\n   for known memory/compute-intensive work or observed OOM/timeouts caused by\n   the function's own work — billing scales linearly with size.\n7. Do NOT override `config.region` unless the user has stated a specific\n   reason (co-located database/backend, data residency, regional audience).\n   The `cmh` default is a deliberate choice.\n8. Files read from disk at runtime (`fs.readFile` on templates, JSON, WASM)\n   are not bundled: works under `netlify dev`, ENOENT in production. Prefer\n   importing static data as a module; otherwise declare the file with a\n   scoped `included_files` entry in `netlify.toml`.\n9. The ~4 KB combined environment-variable limit applies to ALL functions\n   (they run on AWS Lambda), not just Lambda-compat mode. Keep large payloads\n   (service-account JSON, PEM keys) out of env vars — use a bundled file,\n   Blobs, or a runtime fetch. No Netlify setting raises this cap.\n10. The body's FIRST function example must be the minimal default: no\n    `config` export at all, stating the function serves at\n    `/.netlify/functions/<name>`. Custom `path` routing appears only in a\n    later example — agents imitate the first example they see.\n11. Never demonstrate `memory`, `vcpu`, or `region` in a generic example —\n    show them only attached to an explicit stated reason (observed OOM,\n    co-located backend, data residency).\n12. Scheduled-function examples use a real cron expression with the UTC\n    conversion spelled out (e.g. 9 AM ET → `\"0 13 * * *\"` UTC, noting DST) —\n    never only `@hourly`/`@daily` shortcuts, which can't target a specific\n    local hour.\n13. The body must state that `[[headers]]` in netlify.toml, `_headers`, and\n    redirect header rules apply ONLY to static CDN responses — response\n    headers for a function are set in code on the returned `Response`.\n14. When asked to build a function that performs specific work (generate a\n    report, process an upload, send a digest), implement the work — pick a\n    real library where one is needed and write the operation end to end.\n    Never deliver the core task as a `not implemented` stub behind finished\n    plumbing: a function whose central branch throws is not a working\n    answer, however complete its config and routing.\n15. `config.background: true` is the documented, currently-supported way to\n    make a function background (the `-background` filename suffix also still\n    works). If a locally installed bundler doesn't recognize the flag,\n    suspect version skew first: check and upgrade the local tooling, and\n    keep local-compatibility findings separate from claims about platform\n    support — never remove the docs-recommended flag from an answer based\n    solely on an older installed schema.\n"
}

SHA-256 of public snapshot: 2092ea307e1cacc11eba9a15edf6671af13b7e6d1e882ee6858884603af91964