netlify-ops
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
state field) 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 (see references/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.md for sites:create, deploy, status, sites:list, and the netlify api <operationId> --data escape hatch. Detail: references/cli-reference.md.
- REST (long tail): scan
assets/endpoint-index.md (one line per operation), then python3 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 the id field — --json keys it as id, not site_id — pass that id as --site to deploy).
deploy.sh <site-id> <dir> [--prod] — netlify deploy --dir … --site … --json (draft by default; --prod publishes).
set-custom-domain.sh <site-id> <domain> — REST PATCH updateSite (custom_domain).
deploy-status.sh <site-id> <deploy-id> — getSiteDeploy (the state field).
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 --json output (the deploy_id / id field) to feed deploy-status.sh. A deploy moves building → uploading → processing → ready (or error). Poll deploy-status.sh (or getSiteDeploy) until state is terminal; a draft deploy has a unique preview URL, --prod publishes to the live site.
- Custom domain ≠ DNS. Setting
custom_domain does not make it resolve — follow with DNS config (configureDNSForSite / dns_zones records).
- Errors / limits: non-2xx JSON; rate limits are 500 req/min general and deploys 3/min · 100/day — honor the
X-RateLimit-* headers. A 401 means a missing/expired token.
Rules
Hard rules (never violate):
- Never
netlify login / netlify logout. Auth is the injected token via NETLIFY_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) or Bearer $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_id with --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 pure curl over the netlify api escape hatch (the escape-hatch body-key convention is unverified); use the escape hatch for path-params-only ops.
- Prefer the CLI
netlify deploy --dir over 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 login when 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 --build flag — opt out with --no-build.
- Draft vs prod. Omitting
--prod gives a draft/preview deploy (unique URL, production untouched). Only --prod publishes to the live site.
site_id is the key. --name/--account-slug are human-facing; every API op keys on the opaque site_id (in .netlify/state.json after a link). Note: sites:create --json returns this id under id, not site_id (.site_id is null on create) — read .id from create output and pass it as --site.
- The digest-deploy required-files step. A hand-rolled REST
createSiteDeploy returns a required array of SHA1s you must then upload — skip it and the deploy never reaches ready. The CLI handles this.
- Custom domain needs DNS.
updateSite sets custom_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.
state is 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 / createSiteDeploy with endpoint.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 pass site_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
cat the ~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 — the netlify CLI surface + the netlify api escape hatch.
references/rest-api.md — the REST long tail, the digest-deploy protocol, state + limits.
- This is the CLI-first sibling of
github-cli-ops and the OpenAPI-grounded sibling of atlassian-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 — the python3 $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.json is large (queried on disk, never loaded into context).
1---2name: netlify-ops3description: Use when driving Netlify web-hosting directly — creating a site, deploying a build (draft or production), setting + verifying a custom domain, reading deploy status, and listing/inspecting sites/deploys/DNS. CLI-first on the `netlify` CLI (which reads NETLIFY_AUTH_TOKEN and runs headlessly — never `netlify login`) with a REST fallback on Netlify's official OpenAPI at https://api.netlify.com/api/v1 (Authorization: Bearer <token>). Resolves any of the ~174 REST operations from a bundled spec via an endpoint index + a $ref-resolver, with the digest-deploy protocol, rate limits, and site_id conventions handled explicitly. Consumes caller-injected credentials (a token resolved by variable name) — it does not provision or resolve them; the token value is read only by the CLI/curl subprocess, never printed. For Netlify static/JAMstack + serverless hosting, not full-stack container apps.4---56# netlify-ops78## Overview910This 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.1112## When to activate1314- ✅ Creating a Netlify **site**, or deploying a build to one (draft/preview or `--prod`).15- ✅ Setting + verifying a **custom domain**, or configuring a site's **DNS**.16- ✅ Reading **deploy status** (the `state` field) or listing/inspecting sites/deploys.1718**Do NOT activate when:**1920- 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.21- You only need credential setup — credentials are provided by the caller; this skill does not provision or resolve them, and never runs `netlify login` (see `references/credentials.md`).2223## Workflow2425### Step 1 — Receive the injected credentials2627The 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`.2829Bridge 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`](references/credentials.md).3031### Step 2 — Pick the common path (CLI) or the long tail (REST)3233- **CLI (common path):** scan [`assets/cli-index.md`](assets/) for `sites:create`, `deploy`, `status`, `sites:list`, and the `netlify api <operationId> --data` escape hatch. Detail: [`references/cli-reference.md`](references/cli-reference.md).34- **REST (long tail):** scan [`assets/endpoint-index.md`](assets/) (one line per operation), then `python3 scripts/endpoint.py <operationId>` to resolve params + body + a curl skeleton. Detail: [`references/rest-api.md`](references/rest-api.md). Never load the ~690 KB spec into context.3536### Step 3 — Run it3738The four bundled scripts cover the priority ops:3940- `create-site.sh <name> [account-slug]` — `netlify sites:create … --json` (returns the site; its id is the `id` field — `--json` keys it as `id`, not `site_id` — pass that id as `--site` to deploy).41- `deploy.sh <site-id> <dir> [--prod]` — `netlify deploy --dir … --site … --json` (draft by default; `--prod` publishes).42- `set-custom-domain.sh <site-id> <domain>` — REST `PATCH updateSite` (`custom_domain`).43- `deploy-status.sh <site-id> <deploy-id>` — `getSiteDeploy` (the `state` field).4445For anything else: CLI command from `cli-index.md`, or a `curl` against the resolved REST op.4647### Step 4 — Handle the response4849- **Deploys are async + multi-stage.** Pull the deploy id from `deploy.sh`'s `--json` output (the `deploy_id` / `id` field) to feed `deploy-status.sh`. A deploy moves `building → uploading → processing → ready` (or `error`). Poll `deploy-status.sh` (or `getSiteDeploy`) until `state` is terminal; a draft deploy has a unique preview URL, `--prod` publishes to the live site.50- **Custom domain ≠ DNS.** Setting `custom_domain` does not make it resolve — follow with DNS config (`configureDNSForSite` / `dns_zones` records).51- **Errors / limits:** non-2xx JSON; rate limits are 500 req/min general and **deploys 3/min · 100/day** — honor the `X-RateLimit-*` headers. A `401` means a missing/expired token.5253## Rules5455**Hard rules (never violate):**5657- **Never `netlify login` / `netlify logout`.** Auth is the injected token via `NETLIFY_AUTH_TOKEN` (or `--auth`); the CLI runs headlessly. A missing token is a hard caller error, not a cue to log in.58- **Never read or print the token value.** Reference it only as `$NETLIFY_AUTH_TOKEN` (CLI env) or `Bearer $NETLIFY_AUTH_TOKEN` (REST); the subprocess reads it from the environment. The token lives only in `.env` (gitignored) or the environment.59- **Resolve before constructing a REST call.** Build a body/params from the `$ref`-resolved schema (`endpoint.py`) — never from a guessed field set.60- **Deploy to a known `site_id` with `--site`.** A fresh checkout has no `.netlify/state.json`; never rely on link state, never use `--allow-anonymous`.61- **CLI-first, REST for the tail.** Use the CLI for create/deploy/status; drop to REST for custom domain, DNS, and fine-grained reads.62- **This skill never writes credentials.** Credentials are provided by the caller.6364**Preferences (override-able):**6566- Pass `--json` (machine output) and `--force` (skip prompts) to the CLI for non-interactive use.67- For body-carrying REST writes (`updateSite`, `createSiteDeploy`) prefer the pure `curl` over the `netlify api` escape hatch (the escape-hatch body-key convention is unverified); use the escape hatch for path-params-only ops.68- Prefer the CLI `netlify deploy --dir` over the hand-rolled digest protocol — it does the SHA1 diff + upload for you.6970## Gotchas7172- **Don't log in.** The single biggest contract violation: reaching for `netlify login` when the token "isn't working." A bad token is an error to surface, not a login to perform.73- **`--no-build`, not `--build`.** The CLI builds by default; there is no `--build` flag — opt out with `--no-build`.74- **Draft vs prod.** Omitting `--prod` gives a draft/preview deploy (unique URL, production untouched). Only `--prod` publishes to the live site.75- **`site_id` is the key.** `--name`/`--account-slug` are human-facing; every API op keys on the opaque `site_id` (in `.netlify/state.json` after a link). Note: `sites:create --json` returns this id under `id`, not `site_id` (`.site_id` is null on create) — read `.id` from create output and pass it as `--site`.76- **The digest-deploy required-files step.** A hand-rolled REST `createSiteDeploy` returns a `required` array of SHA1s you must then upload — skip it and the deploy never reaches `ready`. The CLI handles this.77- **Custom domain needs DNS.** `updateSite` sets `custom_domain`; the domain only resolves once DNS is configured.78- **Rate limits bite deploys.** 3 deploys/min, 100/day — back off on `X-RateLimit-Remaining: 0`.79- **`state` is a free string.** The spec doesn't enumerate deploy states; treat the value list as from docs and confirm against a live deploy.8081## Anti-patterns8283- **Running `netlify login`.** Forbidden — the contract injects the token; never resolve your own auth or prompt the user to log in.84- **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.85- **Guessing a REST body.** Resolve `updateSite` / `createSiteDeploy` with `endpoint.py` — don't hand-write a body from memory.86- **Deploying without `--site`.** Relying on link state in a fresh checkout silently deploys to the wrong (or no) site. Always pass `site_id`.87- **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.88- **Loading the whole spec.** Don't `cat` the ~690 KB OpenAPI — scan the index, resolve one op.8990## Output9192This 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.9394## Related9596- [`references/credentials.md`](references/credentials.md) — the credential contract (token-by-name → `NETLIFY_AUTH_TOKEN`, the ordered load rule, no-login).97- [`references/cli-reference.md`](references/cli-reference.md) — the `netlify` CLI surface + the `netlify api` escape hatch.98- [`references/rest-api.md`](references/rest-api.md) — the REST long tail, the digest-deploy protocol, state + limits.99- This is the CLI-first sibling of `github-cli-ops` and the OpenAPI-grounded sibling of `atlassian-rest-ops`, under the per-provider service-skill pattern.100101## Progressive disclosure102103Heavy content lives in subfolders, loaded only on demand:104105- [`references/credentials.md`](references/credentials.md) — caller-injected token, the ordered load rule, the Bearer bridge, no-login. Load in Step 1.106- [`references/cli-reference.md`](references/cli-reference.md) — the CLI commands + escape hatch + CLI gotchas. Load in Steps 2–3.107- [`references/rest-api.md`](references/rest-api.md) — the REST pattern, priority-op table, digest-deploy protocol, state + limits. Load for the REST tail.108- [`references/sources.md`](references/sources.md) — provenance (official OpenAPI + CLI/API docs + the two source skills) + the "confirm live" items.109- `assets/cli-index.md` — the CLI quick command list (Step 2).110- `assets/endpoint-index.md` — one line per REST operation (Step 2).111- `assets/netlify-openapi.json` — the bundled spec (Swagger 2.0; queried via the resolver, never loaded wholesale).112- `scripts/endpoint.py` + `.validation.md` — the `python3` `$ref`-resolver (Step 2).113- `scripts/{create-site,deploy,set-custom-domain,deploy-status}.sh` + `.validation.md` — four validated priority-op scripts.114115## Standalone usage (optional, not required)116117This 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.118119To 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:120121```bash122set -a; source .env; set +a # loads $NETLIFY_PROD_TOKEN, never prints it123export NETLIFY_AUTH_TOKEN="$NETLIFY_PROD_TOKEN"124bash scripts/create-site.sh my-site125bash scripts/deploy.sh <site-id> ./dist --prod126```127128The token value is referenced by name only, never printed. Prefer a revocable, short-lived PAT.129130## Body budget131132- `description` ≤ 1,024 chars (agentskills.io cap).133- Body ≤ ~500 lines / 5,000 tokens — kept in context every turn; CLI/REST detail lives in `references/`.134- `assets/netlify-openapi.json` is large (queried on disk, never loaded into context).