Trigger and scope
Trigger on: Fastly Fiddle, fiddle.fastly.dev URLs, the Fiddle HTTP API, CI testing of VCL services, real-edge VCL tests, shareable VCL reproductions, clientFetch.*/events.*/originFetches.* test expressions, .test.js / Mocha specs that target fiddles, SSE updateResult / waitingForSync events, remote VCL linting via Fiddle, or validating real Fastly behavior (geo, WAF, ESI, clustering, shielding) that local tools cannot simulate.
Do NOT use for: local VCL unit testing (use falco), fast TDD loops (Fiddle has a 10-20s edge-sync floor per publish), Fastly Compute/Wasm testing (use viceroy or fastlike), production service deployment (use fastly-cli), or anything requiring authenticated Fastly API access — Fiddle does not use your Fastly API key.
Fastly Fiddle is a web-based sandbox at https://fiddle.fastly.dev that compiles and runs VCL on real Fastly edge nodes. Because it uses the production VCL compiler and real POPs, it's the only way outside a real service to test VCL features that depend on edge infrastructure — geolocation data, WAF, ESI, clustering, shielding, rate limiting, real TLS, and real cache behavior.
Official UI: https://fiddle.fastly.dev
Demo CI runner: https://github.com/fastly/demo-fiddle-ci
API base: https://fiddle.fastly.dev (undocumented but stable; no auth required for public fiddles)
Prerequisites and public uploads
The helper requires Bash, curl 7.76 or newer, jq, and network access to fiddle.fastly.dev.
If a tool is missing, name it and provide installation guidance before running the helper.
Creating or updating a fiddle uploads its VCL, origins, request headers, request bodies, and tests to a public service, including with --lint-only.
Only publish when the user has authorized a public Fiddle upload or shareable reproduction.
For a request limited to local testing, use Falco and keep the spec local.
Never include credentials or private project data in a fiddle.
Set FIDDLE_SKILL_DIR to the absolute directory containing this SKILL.md, using the installed skill location supplied by the client:
FIDDLE_SKILL_DIR=/absolute/path/to/fastly-fiddle
This is a variable you assign, not a client-provided environment variable.
Keep the user's project as the working directory so inputs such as fiddle.json and spec.json remain project-relative.
Bundled scripts and examples use FIDDLE_SKILL_DIR instead.
When Fiddle, when Falco
| Need | Use |
|---|---|
| Fast local iteration (< 1s), watch mode, offline | falco test |
| Real Fastly VCL compiler and semantics | Fiddle |
Real client.geo.*, WAF, ESI, rate limiting, shield |
Fiddle |
| Shareable URL for bug repros and support tickets | Fiddle |
| CI against real edge nodes | Fiddle |
| Structured lint with line/col (no execution required) | Either |
| Fastly Compute (WASM) | Neither — use viceroy/fastlike |
Common workflow: iterate locally with falco test for speed, then push edge cases to Fiddle when you need real Fastly behavior or a shareable link. See falco-vs-fiddle.md for full trade-offs.
Workflow: deliverable first, then the cheapest check that answers your question
Two rules keep you out of the slow path, which is what wastes time and gets agents killed by a wall-clock limit:
-
If your job is to produce a spec file, write it to disk first, before any network call. The file is the deliverable — it must exist even if a later publish stalls on a cold edge-sync. Don't build the spec only inside a
curl --dataargument and lose it when the call blocks. -
Match the check to the question. "Does this VCL compile / is the spec shape accepted?" is answered by a single
POST /fiddlereadingvalid— ~1-3s, no execution, no edge-sync wait (see gotcha #5). You only need the full publish → execute → SSE round trip when you must observe runtime assertion results (actualclientFetch/originFetches/eventsvalues), and that pays the 10-120s edge-sync floor per publish. Executing is the exception, not the default — reach for it deliberately, and always bound your wait; never block indefinitely on the stream.
Lint-only (the common case): compile check, no execution
bash "$FIDDLE_SKILL_DIR/scripts/run-fiddle.sh" --lint-only fiddle.json
# {fiddle_id, url, valid, lintStatus}. Exit 0 = compiles, 2 = lint error
# (details on stderr). No /execute, no SSE, no edge-sync wait.
The raw equivalent is one call — POST and read valid:
UA='fiddle-skill-example/1.0'
curl -sS --max-time 30 -X POST https://fiddle.fastly.dev/fiddle \
-H 'Content-Type: application/json' -H "User-Agent: $UA" \
--data @fiddle.json | jq '{valid, lintStatus}' # valid==true ⇒ it compiles
Full round trip (only when you need runtime results)
When you genuinely need to see assertions pass/fail on the edge, the bundled
helper handles publish → execute → SSE → completion-detection in one call,
with a bounded --max-wait (default 180s per attempt) so it can't hang:
bash "$FIDDLE_SKILL_DIR/scripts/run-fiddle.sh" "$FIDDLE_SKILL_DIR/examples/robots.json"
# Prints fiddle URL, then pass/fail JSON per assertion (including body_preview
# and status by default). Exits non-zero on failure. Pass --no-bodies for
# compact output.
# Iterate against an already-published fiddle without paying edge-sync again:
bash "$FIDDLE_SKILL_DIR/scripts/run-fiddle.sh" --id <fiddle-id> # re-execute (warmest, ~2s)
bash "$FIDDLE_SKILL_DIR/scripts/run-fiddle.sh" --id <fiddle-id> spec.json # PUT then execute
The equivalent raw-curl flow, for reference or when the helper isn't available:
UA='fiddle-skill-example/1.0'
# 1. Create. Capture the ID and the validity flag.
RESP=$(curl -sS -X POST https://fiddle.fastly.dev/fiddle \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-H "User-Agent: $UA" \
--data '{
"origins": ["https://http-me.fastly.dev"],
"vcl": {
"init": "# Synthetic robots.txt response\n# via error restart pattern",
"recv": "if (req.url.path == \"/robots.txt\") {\n error 601;\n}",
"error": "if (obj.status == 601) {\n set obj.status = 200;\n synthetic {\"User-agent: BadBot\"};\n return(deliver);\n}"
},
"requests": [
{ "path": "/robots.txt",
"headers": "X-Custom: value\nX-Other: second",
"tests": ["clientFetch.status is 200", "clientFetch.bodyPreview includes \"BadBot\""] }
]
}')
FID=$(echo "$RESP" | jq -r '.fiddle.id')
echo "$RESP" | jq '{valid, lintStatus}' # bail here if valid is false
# 2. Execute. Subscribe to the SSE stream IMMEDIATELY — session IDs expire fast.
SID=$(curl -sS -X POST "https://fiddle.fastly.dev/fiddle/$FID/execute?cacheID=1" \
-H 'Accept: application/json' -H "User-Agent: $UA" | jq -r '.sessionID')
# 3. Stream results. Emits repeated `event: waitingForSync` (~10-20s on first
# publish) then `event: updateResult` with full pass/fail data. ALWAYS cap
# the stream with --max-time so a slow/cold sync can't block you forever;
# on timeout, re-execute the same $FID (warm, ~2s) rather than re-publishing.
curl -sS -N --max-time 120 -H "User-Agent: $UA" "https://fiddle.fastly.dev/results/$SID/stream"
Full protocol details in api.md.
Wire-format gotchas
Non-obvious behavior that will break tools round-tripping fiddles programmatically:
-
vclon input,srcon output. YouPOST {"vcl": {"recv": "..."}}butGETreturns{"src": {"recv": "..."}}. The server renames the key on normalization. Any tool that fetches a fiddle and re-publishes it must mapsrc→vcl(or sendsrc— both work on input). Update existing fiddles withPUT /fiddle/:id— same body shape asPOST, but partial updates are not supported; omitted subroutines are cleared. -
testsis a string on the wire. You can sendtests: ["a", "b"]but a subsequentGETreturnstests: "a\nb". One assertion per line. Split on\nwhen reading. -
headersis a newline-joined string, not an array. Unliketests(which accepts both),headersmust be a string:"headers": "User-Agent: BadBot/1.0\nX-Custom: value". An array will be rejected with a validation error. -
Request fields are auto-defaulted by the server. A
GETof a fiddle you just created will include fields you didn't send:method: "GET",connType: "h2"(HTTP/2 by default — matters for tests that depend on protocol),enableCluster: true,enableShield: false,useFreshCache: false,sourceIP: "client",followRedirects: false,delay: 0. Set them explicitly if you care. -
Invalid VCL still gets a fiddle ID.
POSTreturns{valid: false, lintStatus: {...}, fiddle: {id, ...}}for broken VCL. On a create/update (POST/PUT) response,validis the lint result —trueif the VCL compiles,falseif not, with details inlintStatus. That's the number to trust for "does this compile?", and you get it without executing anything. Don't rely on HTTP status.But
validmeans something different on aGET. There it tracks execution, not compilation: it staysfalseuntil the fiddle has been executed at least once, then flips totrue. A fiddle that lints perfectly cleanly still reads back asvalid: falseright after you create it. So judge compilation from the create/update response (or by re-submitting the spec) — never from aGET. AGETcan't distinguish a valid-but-not-yet-run fiddle from a genuinely broken one: both come backvalid: falsewith an emptylintStatus. -
VCL string concat with
+rejects parenthesized operands.set X = "used=" + (a - b);fails with a misleading "Remove the trailing+operator" suggestion — the+is fine, the(is what the parser rejects. Compute the sub-expression into a local variable first. See spec-shape.md. -
error 8NN;/error 9NN;is rejected by Fiddle lint — use 6xx. Any 800–999 code fails with "8xx and 9xx error codes are used internally by Fastly. Use 6xx instead.", in any subroutine and any context (bare or insideif/else/switch). So the classicerror 801 <url>;redirect idiom trips Fiddle lint even inside anif— useerror 602 "<url>";and build the 301 invcl_error. Codes 400–799 are accepted. See spec-shape.md. -
Some test expressions have built-in delays.
originFetches.count() is 0returnsasyncDelay: 2500— the server waits 2.5s before evaluating "did nothing happen?". Client wait time must accommodate this; 45-60s is a safe ceiling. -
SSE session IDs are short-lived. Subscribe to
/results/<sessionID>/streamwithin seconds of receiving the ID from/execute. Delayed connections get a 404. Always have the stream open before you start waiting on results. -
Test DSL has unusual syntax. No
.first(), noreqHeaderValue(), noisnt/emptyoperators. Event objects expose onlyurl/method/return/status/ttl— not arbitraryreq.http.*values. Read test-dsl.md before writing assertions. -
The server assigns a fresh fiddle ID on every
POST /fiddle— even for byte-identical input. IDs are not a content hash; back-to-back POSTs of the same body return different IDs, and each new ID needs its own edge-sync pass before/executecan produce results.PUT /fiddle/<id>keeps the URL stable (good for shared bug-repro links) but the new VCL still recompiles and propagates, so PUTs pay the same edge-sync cost as POSTs. The genuinely warm path is re-executing an unchanged fiddle ID — same content, repeat/executecalls finish in ~2s. Capture the ID from the first POST and reuse it; varycacheIDto force cold caches without re-publishing. (PUT is not partial — omitted subroutines are cleared, see #1.) -
originFetches.count() is Nis fragile under retries and sharedcacheID. A retry — automatic inrun-fiddle.sh, or manual via--id <fid>— re-executes against the samecacheID, so any origin response cached on the previous attempt is now a HIT andoriginFetches.count()drops to 0. Two reliable fixes: setuseFreshCache: trueon the request (forces a fresh cache, ignoring the sessioncacheID— see spec-shape.mdRequest objects), or assert viaevents.where(fnName=fetch).count()(counts subroutine entries, not network calls). The same goes fororiginFetches[0].*assertions whenever the test runs after a possible warmup.
Authoring conventions
Fiddles are read by humans in a browser. These aren't surprises, but they make shared fiddles useful:
- Set a
title. Makes fiddles findable in browser tabs, bookmarks, and shared links. Example:"title": "fastly_info.state: compound values deep dive". - Use
initas a header comment. Theinitsubroutine renders first in the UI. Put a short summary there (~55 chars/line):"init": "# fastly_info.state: compound values\n# Demonstrates MISS-CLUSTER, HIT-CLUSTER, HIT-SYNTH". - Format VCL with
\nand indentation rather than cramming everything onto one line. - Send
User-Agent: <tool>/<version>on every API call. Fiddle is unauthenticated shared infra; defaultcurl/x.yor library UAs are bad citizenship. This is the API call's UA, not the simulated request'sheaders.
Testing in CI
Reference implementation: fastly/demo-fiddle-ci — a Node + Mocha harness. Clone it and write your {spec, scenarios[]}.
The one non-obvious thing: it publishes the fiddle once, then re-executes the same fiddle ID per scenario with different requests[]. Only re-execution is warm (~2s); a fresh publish pays the 10-20s edge-sync floor (see "Limits" below and gotcha #11). Keep scenarios sequential.
References
| Topic | File | Use when... |
|---|---|---|
| Helper script | scripts/run-fiddle.sh | Publishing + executing + streaming a fiddle in one shell command |
| Example payload | examples/robots.json | Starting from a known-good minimal fiddle spec |
| HTTP API | api.md | Calling Fiddle endpoints directly, driving it from any language |
| Fiddle spec shape | spec-shape.md | Building the JSON payload: origins, src, requests, defaults |
| Test DSL | test-dsl.md | Writing clientFetch.*, events.where(...), originFetches.* |
| Falco vs Fiddle | falco-vs-fiddle.md | Choosing the right tool, or combining them in one workflow |
Limits and cautions
- No auth required, no quota documented. Be a good citizen: don't hammer the API in tight loops, and always send a descriptive
User-Agenton API calls (see Authoring conventions). UsecacheIDconsistently across requests that need to share cache, and vary it to force cold caches. - Edge-sync floor is ~10-20s per publish, sometimes much longer. Applies per fiddle ID, not per unique content — and every
POST /fiddlemints a new ID (see Wire-format gotchas #11), so re-publishing identical input still pays the full sync cost. Cold publishes regularly take 60-120s in practice. Unusable for TDD. Batch changes; execute once per meaningful delta; reuse IDs viaPUTwhen iterating. - Execution hops through a real POP (tests observed running from IAD on node
kiad7000140). Geographic assertions reflect wherever the fiddle executor landed. - Fiddles are public by default. Don't put secrets in VCL you publish.
- The API is undocumented. Field names and behavior can change.