Skip to content
Merged
27 changes: 27 additions & 0 deletions axis-scenarios/agent-runner/list-scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "Agent Runner: agents commands are project-scoped, not team-global",
prompt:
"Give me one command to list every Netlify agent task running across all of my sites.",
judge: [
{
check:
"Explains that `netlify agents:*` commands (including `netlify agents:list`) are PROJECT-scoped — they operate on a single project (the linked directory's project, or the one named with `--project <name>`), not on the whole team/all sites at once.",
},
{
check:
"Notes there is no single command that lists tasks across ALL sites; to see another site's tasks you run from its linked directory or pass `--project <name>` for that site.",
},
{
check:
"Gives the correct per-project command: `netlify agents:list` (optionally `--status <status>` or `--json`) for the current/linked project.",
},
{
check:
"Does NOT invent an all-sites/team-global flag, nor reach for an undocumented API (curl `https://api.netlify.com/...`, `netlify api <method>`, tokens off disk) to aggregate tasks across sites.",
},
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
31 changes: 31 additions & 0 deletions axis-scenarios/agent-runner/output-new-branch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "Agent Runner: task output lands on a new branch + Deploy Preview, not the base branch",
prompt:
"I started a Netlify agent task with `-b staging` to update our pricing page. Will it commit its changes straight onto my `staging` branch? Where do I go to see and review what it did?",
judge: [
{
check:
"Explains the agent does NOT commit its changes onto the base branch — its output lands on a NEW branch with its own Deploy Preview, so the existing `staging` branch is not overwritten in place.",
},
{
check:
"Clarifies that `-b`/`--branch` selects the BASE (starting) branch the agent works from, not the destination where the results are written.",
},
{
check:
"Tells the user to review the results on the new branch / Deploy Preview the task produces.",
},
{
check:
"Does NOT claim the agent pushes commits directly onto `staging` in place.",
},
{
check:
"Treats the task as asynchronous — status must be polled with `netlify agents:show <task-id>` until it reaches a terminal status like `done`. Passes vacuously if not mentioned.",
},
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
27 changes: 27 additions & 0 deletions axis-scenarios/agent-runner/plan-credits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "Agent Runner: runs consume plan credits and are blocked without them",
prompt:
"My `netlify agents:create` command won't start a task — it's complaining about credits / a usage limit. What's going on, and what should I do?",
judge: [
{
check:
"Explains that agent runs CONSUME plan credits, and that when the account has no available credits (or the agent/AI usage limit has been reached) `netlify agents:create` is blocked and the run won't start.",
},
{
check:
"Frames this as an account/plan-state issue to resolve with the user (e.g. add credits / check the plan) and surfaces the exact error rather than trying to work around it.",
},
{
check:
"Does NOT invent an undocumented workaround to force the run — no curling `https://api.netlify.com/...`, no `netlify api <method>`, no reading auth tokens off disk.",
},
{
check:
"Does NOT claim the task started or fabricate a task ID — a credit-blocked run has not been queued.",
},
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
30 changes: 30 additions & 0 deletions axis-scenarios/ai-gateway/browser-proxy-through-function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

// There is no browser-callable gateway: credentials are runtime-only and only
// injected into server-side compute. Client code must proxy through a Netlify
// Function rather than calling a provider SDK (or exposing a key) in the browser.
export default {
name: "AI Gateway: browser can't call the gateway — proxy through a function",
prompt:
"I'm building a React chat widget and I want it to call the AI Gateway directly from the browser with the OpenAI SDK so I don't have to run any backend. Wire it up.",
judge: [
{
check:
"Explains there is NO browser-callable gateway: gateway credentials are injected only into server-side runtime (functions / edge / server routes), the browser has no gateway credentials, and there is no public URL a browser can hit to reach the gateway directly.",
},
{
check:
"Redirects to the proxy pattern: put the gateway call in a Netlify Function (or edge / server route) and have the browser `fetch()` that endpoint (e.g. `/api/chat`), which talks to the gateway server-side and returns the result.",
},
{
check:
"Does NOT construct the OpenAI SDK or call the gateway in client-side browser code, and does NOT hardcode or otherwise expose a provider API key in the browser (which would leak it to every visitor and bypass the gateway).",
},
{
check:
"The proxy function it writes wires the gateway correctly: bare `new OpenAI()` with no `baseURL` / `apiKey`, does not set `OPENAI_API_KEY`, and pins a curated gateway model.",
},
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
16 changes: 16 additions & 0 deletions axis-scenarios/ai-gateway/build-time-gateway-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "AI Gateway: build-time AI calls have no gateway credentials",
prompt:
"I want to use the Netlify AI Gateway to generate an SEO summary for each of my ~500 marketing pages and bake the text into the static HTML at build time (in my static-site-generation step, so there's no per-request AI cost). Set this up with the OpenAI SDK through the gateway.",
judge: [
{ check: "Warns that AI Gateway credentials are injected only at RUNTIME (deployed functions, edge functions, server routes at request time) and are NOT available during the build — so calling the gateway from a build/SSG step gets no credentials and fails" },
{ check: "Redirects the work to runtime: generate the summaries in a function/server route at request time (optionally caching results, e.g. to Netlify Blobs) rather than at build time — or clearly states the build-time approach can't use the gateway at all" },
{ check: "If it writes gateway code, constructs the provider SDK bare (e.g. `new OpenAI()` with no baseURL/apiKey) relying on auto-injected env vars, and does NOT set its own OPENAI_API_KEY" },
{ check: "Mentions that gateway usage is credit-metered (draws down the Netlify AI allowance and pauses when the limit is hit) — relevant when fanning out across ~500 pages" },
{ check: "Does NOT claim the gateway will simply work inside the build/prerender step as written" },
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
31 changes: 31 additions & 0 deletions axis-scenarios/ai-gateway/local-dev-requires-linked-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

// The gateway env vars are injected locally only when the working directory is
// LINKED to the Netlify site — `netlify dev` pulls them from the linked site's
// environment. The site here is already deployed to production (so AI is enabled
// and the gateway is active), which isolates the cause to an unlinked local dir.
export default {
name: "AI Gateway: local calls fail because the project isn't linked",
prompt:
"My Netlify function calls the AI Gateway with the OpenAI SDK. It works fine on the deployed site — the site has already been deployed to production and AI is enabled — but when I run it locally with `netlify dev` the SDK throws `OPENAI_API_KEY missing`. I'm using `new OpenAI()` with no key on purpose. Why does it fail locally and how do I fix it?",
judge: [
{
check:
"Diagnoses that the local working directory must be LINKED to the Netlify site for `netlify dev` to inject the gateway env vars — an unlinked directory has no site context, so nothing is injected and the call fails locally even though the site is deployed to production.",
},
{
check:
"Tells the user to link the project — run `netlify link` (or `netlify init`) in the directory — and then run `netlify dev`.",
},
{
check:
"Confirms the bare `new OpenAI()` construction is correct; the placeholder key and base URL are auto-injected, so the fix is linking, not adding a constructor argument.",
},
{
check:
"Does NOT tell the user to set their own `OPENAI_API_KEY` (or hardcode any provider key) to make local dev work — a user-set key disables the gateway.",
},
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
35 changes: 35 additions & 0 deletions axis-scenarios/ai-gateway/long-generation-timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

// A gateway call runs inside a function and is bound by the 60-second
// synchronous function timeout. A long-form generation can exceed it, so the
// skill-guided answer streams the response (or uses a background function)
// rather than blocking on a single synchronous, unstreamed completion.
export default {
name: "AI Gateway: long generation must not exceed the function timeout",
prompt:
"Create a Netlify function at netlify/functions/article.ts mounted at /api/article that takes POST { topic: string } and generates a long-form (2000+ word) article with gpt-5 through the AI Gateway, then returns it. These generations regularly take well over a minute. Make sure it won't hit the function timeout.",
judge: [
{
check:
"Identifies that the gateway call runs inside the function and is bound by the 60-second synchronous function timeout, so a generation taking over a minute can exceed it.",
},
{
check:
"Mitigates the timeout the documented way — EITHER streams the response (enables streaming on the SDK call and returns a `ReadableStream` / `text/event-stream` so chunks flow to the client incrementally) OR uses a background function (returns 202 immediately, runs up to 15 minutes, and persists the result for the client to fetch).",
},
{
check:
"Does NOT leave the generation as a single blocking, unstreamed synchronous call that assumes a >1-minute completion will return within the timeout.",
},
{
check:
"Wires the gateway correctly: constructs the OpenAI SDK bare (no custom `baseURL`, no `apiKey`), does NOT set `OPENAI_API_KEY`, and pins a curated gateway model (e.g. a `gpt-5*` / `gpt-4o*` chat model).",
},
{
check:
"Uses the modern Netlify function signature (default export async handler, Web API Request/Response) with a config exporting path: '/api/article'.",
},
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
15 changes: 15 additions & 0 deletions axis-scenarios/blobs/build-time-uploads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "Blobs: seed a store at build time via the deploy directory",
prompt:
"At build time I generate a set of prerendered product JSON files (1.json, 2.json, ...) that I want available in Netlify Blobs the moment the site goes live — without a runtime function looping over them and calling store.set during a cold start. Each file also has a little metadata (a content type and a generatedAt timestamp). What's the build-time way to get these into Blobs, and how do I read them back?",
judge: [
{ check: "Uses the build-time file-based upload path: write the generated files into the `.netlify/blobs/deploy/` directory during the build, where the path under that directory becomes the blob key — rather than calling `store.set` for each file at runtime" },
{ check: "Attaches per-file metadata via a JSON sidecar whose name is the blob's filename prefixed with `$` and ending in `.json` (e.g. metadata for `1.json` in `$1.json.json`)" },
{ check: "Reads the seeded blobs back at runtime with a DEPLOY-scoped store — `getDeployStore(...)`, NOT a site-scoped `getStore()` — since build-time uploads live in the deploy-scoped store" },
{ check: "Imports the store helper from '@netlify/blobs' and uses documented methods" },
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
15 changes: 15 additions & 0 deletions axis-scenarios/blobs/cli-inspect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "Blobs: inspect and edit a store from the CLI",
prompt:
"A production bug seems tied to one bad value in my Netlify Blobs 'config' store under the key 'feature-flags'. From my terminal, without writing and deploying a throwaway function, how can I look at that value and, if needed, overwrite it? I have the Netlify CLI installed.",
judge: [
{ check: "Uses the documented Netlify CLI blobs commands — `netlify blobs:get <store> <key>` to read the value and `netlify blobs:set <store> <key> <value>` to overwrite it (and mentions `netlify blobs:list` / `netlify blobs:delete` as the sibling commands)" },
{ check: "Notes the CLI operates on the linked site's store, so the project must be linked first (`netlify link`)" },
{ check: "Does NOT claim you must write and deploy a function to inspect a blob value, and does NOT reach for the raw Netlify API or read tokens off disk to read/write the value" },
{ check: "Stays on documented surfaces — the CLI `blobs:*` subcommands — rather than inventing an endpoint or store method" },
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
15 changes: 15 additions & 0 deletions axis-scenarios/blobs/concurrency-last-write-wins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "Blobs: last-write-wins — don't build an atomic counter on a blob",
prompt:
"I want a page-view counter. Create a Netlify function that, on each request, reads the current count from a Netlify Blobs key, adds one, and writes it back — so the number goes up by exactly one per view even under heavy concurrent traffic. Make sure no increments are ever lost.",
judge: [
{ check: "Explains that Netlify Blobs is last-write-wins with NO locking, transactions, or concurrency control — so a read-add-write over a single blob key will lose increments when requests overlap (two requests both read N, both write N+1)" },
{ check: "Does NOT present a plain get-then-set on a blob key as a safe exactly-once counter; if it shows one at all, it explicitly flags it as unsafe under concurrency" },
{ check: "Recommends the right tool for atomic/transactional counting — Netlify Database, which provides real transactions — rather than trying to make Blobs atomic" },
{ check: "Does NOT invent a Blobs locking / transaction / compare-and-set primitive to make the counter safe — the store exposes only the documented get/set/setJSON/delete/list/metadata methods" },
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
15 changes: 15 additions & 0 deletions axis-scenarios/blobs/list-pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "Blobs: list a large store without hand-rolled pagination",
prompt:
"My Netlify Blobs store has hundreds of thousands of keys. Create a Netlify function that walks the entire store to build a report, processing keys as it goes. I'm worried about loading every key into memory at once. How should I list them?",
judge: [
{ check: "Explains that `store.list()` auto-paginates — a plain `await store.list()` transparently fetches every page and returns the full `blobs` array; the agent does NOT hand-roll page cursors or offsets" },
{ check: "For the memory concern, uses the paginated form `store.list({ paginate: true })` with `for await (...)` to stream results a page at a time instead of buffering all keys at once" },
{ check: "Does NOT enumerate keys by fetching blob values, shelling out, or keeping a side index — listing is done through the built-in `store.list` API" },
{ check: "Imports `getStore` from '@netlify/blobs' and uses documented methods" },
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
16 changes: 16 additions & 0 deletions axis-scenarios/blobs/metadata-size-cap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "Blobs: don't stuff large data into object metadata",
prompt:
"In my Netlify Blobs store each object is an uploaded document. I want to attach a lot of info to each one via the `metadata` option on `store.set` — the full extracted text of the document, a big list of tags, and a running change-history log — so I can read it all back with getMetadata without downloading the file. Wire this up.",
judge: [
{ check: "Warns that Netlify Blobs object metadata is capped (roughly 2 KB per object) and is meant for small descriptors — content type, size, timestamps, a status flag — NOT large payloads like full extracted text or an unbounded change-history log" },
{ check: "Puts the large data (extracted text, tags, history) in the blob VALUE (or a separate blob), keeping the `metadata` object limited to small fields" },
{ check: "Still uses `store.getMetadata(key)` for the small metadata it retains, and reads the larger content from the blob value when it's actually needed" },
{ check: "Imports `getStore` from '@netlify/blobs' and uses documented methods (set/get/getMetadata/getWithMetadata)" },
{ check: "Does NOT claim metadata can hold arbitrarily large JSON, and does NOT try to raise or work around the metadata cap" },
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
15 changes: 15 additions & 0 deletions axis-scenarios/blobs/per-operation-consistency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ScenarioInput } from "@netlify/axis";
import { withSkillVariants } from "../helpers/variants";

export default {
name: "Blobs: use strong consistency selectively, not everywhere",
prompt:
"To be safe against any stale reads, I'm thinking of setting `consistency: 'strong'` on my Netlify Blobs store so every single read across the whole app always gets the latest value. Most of my reads just serve rarely-changing assets; only one endpoint does an immediate read right after a write. Is 'make everything strong' the right call?",
judge: [
{ check: "Explains that strong-consistency reads are SLOWER than eventual reads, so forcing every read to be strong needlessly adds latency — strong should be reserved for reads that genuinely need read-your-writes" },
{ check: "Notes that consistency can be requested per operation — e.g. `store.get(key, { consistency: 'strong' })` on the one read-after-write endpoint — instead of setting strong on the whole store" },
{ check: "Recommends leaving the rarely-changing asset reads on the default (eventual) consistency, which is faster and the right choice for read-heavy, stable data" },
{ check: "Imports `getStore` from '@netlify/blobs' and does NOT paper over consistency with sleeps/retries or an external cache" },
],
variants: withSkillVariants(),
} satisfies ScenarioInput;
Loading
Loading