Netlify Deploys skill for Codex
Redirects that fire and previews that stay isolated. Codex keeps Netlify build behaviour in netlify.toml with per-context environments, orders redirect rules and explains shadowing before reaching for force, and writes functions with the modern Request and Response signature. Slow work moves to background functions, periodic jobs to scheduled functions, and every change is checked on a draft deploy with curl before anyone asks for a production publish.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- netlify-deploys Installs to ~/.agents/skills/netlify-deploys/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install netlify-deploys The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project netlify-deploys Installs to .agents/skills/netlify-deploys/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/netlify-deploys.zipunzip -o netlify-deploys.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) netlify-deploys PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $netlify-deploys, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Configures and debugs Netlify sites - netlify.toml build settings and deploy contexts, _redirects and [[redirects]] rules with shadowing and force, headers, Netlify Functions (modern Request/Response handlers, background and scheduled functions), Edge Functions, Blobs and environment variables - using the netlify CLI. Use when a Netlify build fails, a redirect or SPA fallback does not fire, a function 404s or times out, deploy previews hit production data, or you are setting up draft and production deploys from CI.
Use it when
- A Netlify build fails or differs from the local build
- Redirects, rewrites or the SPA fallback do not behave as expected
- You are adding Netlify Functions, background or scheduled functions
- Deploy previews need separate environment variables from production
Use something else when
- The site runs on Cloudflare Pages instead: use cloudflare-pages
- You are deciding what logic should run at the edge across providers: use edge-functions
How to check the result
netlify build passes locally and a draft deploy URL returns the expected status codes, redirect targets and headers for every new rule under curl -sI.
The mistake it prevents
Expecting a redirect to override a file that exists at the same path; without force = true the static file wins.
A good Netlify setup keeps all build behaviour in netlify.toml (not only in the UI),
uses deploy contexts so previews never touch production secrets or data, and has redirect
rules that are ordered, tested and free of accidental shadowing.
Inspect first
- Read
AGENTS.md,netlify.toml,public/_redirectsorstatic/_redirects,_headers,package.jsonand the lockfile. - Check
.netlify/state.jsonfor a linked site. Linking (netlify link) and anything that reads remote env or deploys needs network and an authenticated CLI orNETLIFY_AUTH_TOKENplusNETLIFY_SITE_ID. - Identify the framework. Next.js, Astro, Remix, SvelteKit and Nuxt deploy through framework adapters that generate functions; do not hand-write functions for routes the adapter already owns.
- UI build settings are overridden by
netlify.toml. When the two disagree, the file wins.
netlify.toml baseline
[build]
base = "apps/web" # monorepo package; omit for single-package repos
command = "npm run build"
publish = "dist" # relative to base
[build.environment]
NODE_VERSION = "22"
[functions]
directory = "netlify/functions"
node_bundler = "esbuild"
[context.production.environment]
API_URL = "https://api.example.com"
[context.deploy-preview.environment]
API_URL = "https://staging-api.example.com"
[context.branch-deploy]
command = "npm run build:staging"
Keep secrets out of the file: set them with netlify env:set scoped to contexts, or in
the UI. Values in netlify.toml are committed to git.
netlify env:list --context production
netlify env:set STRIPE_KEY sk_test_xxx --context deploy-preview branch-deploy
netlify env:set STRIPE_KEY "$PROD_VALUE" --context production --secret
Redirects and rewrites
Rules are evaluated top to bottom; the first match wins. _redirects rules are read
before netlify.toml rules.
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/api/:splat"
status = 200 # rewrite (proxy), URL stays the same
[[redirects]]
from = "/blog/:slug"
to = "/articles/:slug"
status = 301
[[redirects]]
from = "/*"
to = "/index.html"
status = 200 # SPA fallback, keep last
Shadowing: by default a rule does not apply when a file exists at the requested path.
That is what makes the SPA fallback safe. Add force = true only when you really want to
override existing files, for example redirecting a removed page that still ships.
| Need | Rule |
|---|---|
| Permanent move | status = 301 |
| Proxy to another origin | to = "https://api.example.com/:splat", status = 200 |
| Country or language routing | conditions = {Country = ["de"], Language = ["de"]} |
| Custom 404 for a section | from = "/docs/*", to = "/docs/404.html", status = 404 |
| Gate by role (Identity) | conditions = {Role = ["admin"]} with a 401 fallback rule |
Functions
Modern handlers use the web Request and Response and declare their path in config:
// netlify/functions/orders.mts
import type { Config, Context } from "@netlify/functions";
export default async (req: Request, context: Context) => {
if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
const body = await req.json();
const order = await createOrder(body, Netlify.env.get("DATABASE_URL"));
return Response.json(order, { status: 201 });
};
export const config: Config = { path: "/api/orders" };
| Need | Use |
|---|---|
| Request/response under the synchronous limit | Standard function |
| Work that takes minutes (exports, emails, imports) | Background function: file name ends in -background, runs up to 15 min, caller gets 202 immediately |
| Periodic job | Scheduled function: export const config: Config = { schedule: "@hourly" } (cron syntax also works) |
| Per-request logic in front of every page (geo, auth gate, A/B) | Edge Function in netlify/edge-functions (Deno runtime) |
| Small key/value or file storage | Netlify Blobs: getStore("uploads") from @netlify/blobs |
Synchronous functions have a short execution limit (seconds, not minutes); check the current plan docs and move anything slow to a background function plus a status endpoint. Scheduled functions run only on published production deploys.
Local loop
netlify dev # runs the framework dev server plus functions, redirects, env
netlify build # reproduces the CI build, including plugins
netlify functions:invoke orders --payload '{"sku":"A1"}'
netlify deploy --dir=dist # draft deploy, prints a unique URL (needs network)
netlify deploy --prod --dir=dist # production: confirm with the user first
netlify dev pulls remote env vars when the site is linked; offline it uses only local
values, so missing-variable errors locally may not reproduce in the cloud.
Caching headers
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/*.html"
[headers.values]
Cache-Control = "public, max-age=0, must-revalidate"
For function responses, Netlify-CDN-Cache-Control controls the CDN separately from the
browser. Netlify invalidates the CDN on every deploy, so hashed assets plus short HTML
TTLs are the default.
Debug table
| Symptom | Check |
|---|---|
| Build works locally, fails on Netlify | NODE_VERSION, base directory, lockfile present, netlify build locally |
| Redirect ignored | A real file shadows it, an earlier rule matches first, or the rule is in the wrong _redirects (must be in the publish dir) |
| Function 404 | Path in config, directory setting, file extension, built by adapter instead |
| Function timeout | Move work to background function or queue; check slow cold DB connections |
| Previews write to prod DB | Context-scoped env vars missing for deploy-preview |
| Old content after deploy | Browser cache from long HTML max-age, or a service worker |
Verification
-
netlify buildpasses locally with the same Node version. - A draft deploy URL serves the change;
curl -sIconfirms redirects, status codes and headers for each new rule. - Functions respond under
netlify dev; background functions return 202. - Deploy previews read preview-scoped env values.
- Production deploys and rollbacks (publish an earlier deploy in the UI or API) only after explicit user approval.
Gotchas
_redirectsmust end up in the publish directory; frameworks often need it inpublic/orstatic/to be copied.- A 200 rewrite to an external origin is a proxy: cookies and auth headers pass through. Do not proxy to services you do not control.
- Functions bundle only what esbuild can trace. Files read at runtime need
included_files = ["data/**"]under[functions]. - Environment variables set only in
[build.environment]are available at build time, not inside functions at runtime.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Netlify Deploys"
short_description: "Redirects that fire and previews that stay isolated"
default_prompt: "$netlify-deploys My Netlify SPA returns 404 on refresh for nested routes, fix the redirects" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.