Overview
This skill lets an agent perform Netlify web-hosting operations CLI-first (the netlify CLI) with a REST fallback on Netlify's official API — no SDK. The CLI is the ergonomic common path (create a site, deploy a directory, read status); the REST API is the comprehensive long tail (custom domains, fine-grained reads, DNS), grounded on a bundled OpenAPI via an endpoint index + a $ref-resolver so coverage never loads the spec into context. The agent consumes the caller-injected token, picks the CLI command or REST operation, runs it, and parses the JSON. It drives Netlify's static/JAMstack + serverless surface — not full-stack container hosting.
When to activate
- ✅ Creating a Netlify site, or deploying a build to one (draft/preview or
--prod). - ✅ Setting + verifying a custom domain, or configuring a site's DNS.
- ✅ Reading deploy status (the
statefield) or listing/inspecting sites/deploys.
Do NOT activate when:
- You need full-stack container/app hosting (Render, Fly.io, Railway, Vercel SSR-as-PaaS) — Netlify here is a static/JAMstack + serverless-function host only.
- You only need credential setup — credentials are provided by the caller; this skill does not provision or resolve them, and never runs
netlify login(seereferences/credentials.md).
Workflow
Step 1 — Receive the injected credentials
The caller has already resolved the account and injected the token — consume it; do not look for a record or log in. Netlify has no non-secret record field (the host is fixed; no account id/email). You receive from context the capability (web-hosting) and the token's variable NAME. Resolve that name as the project-level .env value if that file exists and defines the var, else the environment variable of that name (project .env first). The token value is never in context prose — only its variable name. Perform no scope resolution or directory walk; it is project .env, not .envrc.
Bridge the token into NETLIFY_AUTH_TOKEN (the CLI reads it; the REST fallback sends Authorization: Bearer). Never netlify login/logout. Full contract: references/credentials.md.
Step 2 — Pick the common path (CLI) or the long tail (REST)
- CLI (common path): scan
assets/cli-index.mdforsites:create,deploy,status,sites:list, and thenetlify api <operationId> --dataescape hatch. Detail:references/cli-reference.md. - REST (long tail): scan
assets/endpoint-index.md(one line per operation), thenpython3 scripts/endpoint.py <operationId>to resolve params + body + a curl skeleton. Detail:references/rest-api.md. Never load the ~690 KB spec into context.
Step 3 — Run it
The four bundled scripts cover the priority ops:
create-site.sh <name> [account-slug]—netlify sites:create … --json(returns the site; its id is theidfield —--jsonkeys it asid, notsite_id— pass that id as--siteto deploy).deploy.sh <site-id> <dir> [--prod]—netlify deploy --dir … --site … --json(draft by default;--prodpublishes).set-custom-domain.sh <site-id> <domain>— RESTPATCH updateSite(custom_domain).deploy-status.sh <site-id> <deploy-id>—getSiteDeploy(thestatefield).
For anything else: CLI command from cli-index.md, or a curl against the resolved REST op.
Step 4 — Handle the response
- Deploys are async + multi-stage. Pull the deploy id from
deploy.sh's--jsonoutput (thedeploy_id/idfield) to feeddeploy-status.sh. A deploy movesbuilding → uploading → processing → ready(orerror). Polldeploy-status.sh(orgetSiteDeploy) untilstateis terminal; a draft deploy has a unique preview URL,--prodpublishes to the live site. - Custom domain ≠ DNS. Setting
custom_domaindoes not make it resolve — follow with DNS config (configureDNSForSite/dns_zonesrecords). - Errors / limits: non-2xx JSON; rate limits are 500 req/min general and deploys 3/min · 100/day — honor the
X-RateLimit-*headers. A401means a missing/expired token.
Rules
Hard rules (never violate):
- Never
netlify login/netlify logout. Auth is the injected token viaNETLIFY_AUTH_TOKEN(or--auth); the CLI runs headlessly. A missing token is a hard caller error, not a cue to log in. - Never read or print the token value. Reference it only as
$NETLIFY_AUTH_TOKEN(CLI env) orBearer $NETLIFY_AUTH_TOKEN(REST); the subprocess reads it from the environment. The token lives only in.env(gitignored) or the environment. - Resolve before constructing a REST call. Build a body/params from the
$ref-resolved schema (endpoint.py) — never from a guessed field set. - Deploy to a known
site_idwith--site. A fresh checkout has no.netlify/state.json; never rely on link state, never use--allow-anonymous. - CLI-first, REST for the tail. Use the CLI for create/deploy/status; drop to REST for custom domain, DNS, and fine-grained reads.
- This skill never writes credentials. Credentials are provided by the caller.
Preferences (override-able):
- Pass
--json(machine output) and--force(skip prompts) to the CLI for non-interactive use. - For body-carrying REST writes (
updateSite,createSiteDeploy) prefer the purecurlover thenetlify apiescape hatch (the escape-hatch body-key convention is unverified); use the escape hatch for path-params-only ops. - Prefer the CLI
netlify deploy --dirover the hand-rolled digest protocol — it does the SHA1 diff + upload for you.
Gotchas
- Don't log in. The single biggest contract violation: reaching for
netlify loginwhen the token "isn't working." A bad token is an error to surface, not a login to perform. --no-build, not--build. The CLI builds by default; there is no--buildflag — opt out with--no-build.- Draft vs prod. Omitting
--prodgives a draft/preview deploy (unique URL, production untouched). Only--prodpublishes to the live site. site_idis the key.--name/--account-slugare human-facing; every API op keys on the opaquesite_id(in.netlify/state.jsonafter a link). Note:sites:create --jsonreturns this id underid, notsite_id(.site_idis null on create) — read.idfrom create output and pass it as--site.- The digest-deploy required-files step. A hand-rolled REST
createSiteDeployreturns arequiredarray of SHA1s you must then upload — skip it and the deploy never reachesready. The CLI handles this. - Custom domain needs DNS.
updateSitesetscustom_domain; the domain only resolves once DNS is configured. - Rate limits bite deploys. 3 deploys/min, 100/day — back off on
X-RateLimit-Remaining: 0. stateis a free string. The spec doesn't enumerate deploy states; treat the value list as from docs and confirm against a live deploy.
Anti-patterns
- Running
netlify login. Forbidden — the contract injects the token; never resolve your own auth or prompt the user to log in. - Echoing the token. Never
echo $NETLIFY_AUTH_TOKEN, never paste it into a printed command, never write its value to a file. Reference the env var only. - Guessing a REST body. Resolve
updateSite/createSiteDeploywithendpoint.py— don't hand-write a body from memory. - Deploying without
--site. Relying on link state in a fresh checkout silently deploys to the wrong (or no) site. Always passsite_id. - Using
--allow-anonymous. That creates a claimable site without auth — not this skill's path; always deploy to a known site with the injected token. - Loading the whole spec. Don't
catthe ~690 KB OpenAPI — scan the index, resolve one op.
Output
This skill produces CLI/API side effects (the requested Netlify operation) and returns the parsed JSON to the calling agent. It writes no files of its own (it only consumes the caller-injected token). For a deploy it reports the deploy id + URL + terminal state; for a site it reports the site_id + URL; for reads it returns the requested JSON. The abstract consumer is the calling agent (or a sub-agent) that needs the web-hosting operation performed; secrets never enter that output.
Related
references/credentials.md— the credential contract (token-by-name →NETLIFY_AUTH_TOKEN, the ordered load rule, no-login).references/cli-reference.md— thenetlifyCLI surface + thenetlify apiescape hatch.references/rest-api.md— the REST long tail, the digest-deploy protocol, state + limits.- This is the CLI-first sibling of
github-cli-opsand the OpenAPI-grounded sibling ofatlassian-rest-ops, under the per-provider service-skill pattern.
Progressive disclosure
Heavy content lives in subfolders, loaded only on demand:
references/credentials.md— caller-injected token, the ordered load rule, the Bearer bridge, no-login. Load in Step 1.references/cli-reference.md— the CLI commands + escape hatch + CLI gotchas. Load in Steps 2–3.references/rest-api.md— the REST pattern, priority-op table, digest-deploy protocol, state + limits. Load for the REST tail.references/sources.md— provenance (official OpenAPI + CLI/API docs + the two source skills) + the "confirm live" items.assets/cli-index.md— the CLI quick command list (Step 2).assets/endpoint-index.md— one line per REST operation (Step 2).assets/netlify-openapi.json— the bundled spec (Swagger 2.0; queried via the resolver, never loaded wholesale).scripts/endpoint.py+.validation.md— thepython3$ref-resolver (Step 2).scripts/{create-site,deploy,set-custom-domain,deploy-status}.sh+.validation.md— four validated priority-op scripts.
Standalone usage (optional, not required)
This is a convenience for a human running the skill by hand outside an agent-flow — it is not a dependency of the skill. The skill's normative contract is caller-injection (Step 1); this appendix is only the manual-operator bridge.
To run by hand, create a Netlify PAT (User → Applications → Personal access tokens), store its value in a gitignored .env, install the CLI (npm i -g netlify-cli), then populate the var yourself:
set -a; source .env; set +a # loads $NETLIFY_PROD_TOKEN, never prints it
export NETLIFY_AUTH_TOKEN="$NETLIFY_PROD_TOKEN"
bash scripts/create-site.sh my-site
bash scripts/deploy.sh <site-id> ./dist --prod
The token value is referenced by name only, never printed. Prefer a revocable, short-lived PAT.
Body budget
description≤ 1,024 chars (agentskills.io cap).- Body ≤ ~500 lines / 5,000 tokens — kept in context every turn; CLI/REST detail lives in
references/. assets/netlify-openapi.jsonis large (queried on disk, never loaded into context).