$npx -y skills add fastly/fastly-agent-toolkit --skill xvclExtends Fastly VCL with loops, functions, constants, macros, conditionals, and includes via XVCL — a VCL transpiler that compiles .xvcl files into standard VCL. Use when writing VCL for Fastly, working with .xvcl files, generating repetitive VCL (multiple backends, routing rules,
| 1 | ## Trigger and scope |
| 2 | |
| 3 | Trigger on: XVCL, .xvcl files, VCL transpiler, VCL metaprogramming, #const/#for/#def/#inline in VCL context, writing a VCL script, writing VCL and running it locally, or any Fastly VCL writing task. |
| 4 | |
| 5 | Do NOT trigger for: debugging existing .vcl files without XVCL, Fastly API/CLI ops, Fastly Compute, or Terraform — even if they mention VCL. |
| 6 | |
| 7 | # Writing VCL with XVCL |
| 8 | |
| 9 | XVCL is a VCL transpiler that adds metaprogramming to Fastly VCL. Write `.xvcl` files, compile to `.vcl`, then test with Falco or deploy to Fastly. All XVCL constructs are resolved at compile time — zero runtime overhead. |
| 10 | |
| 11 | ## Quick Start |
| 12 | |
| 13 | ```bash |
| 14 | # Compile (no install needed with uvx) |
| 15 | uvx xvcl main.xvcl -o main.vcl |
| 16 | |
| 17 | # Lint the output |
| 18 | falco lint main.vcl |
| 19 | |
| 20 | # Run locally — this is how you "run" VCL on your machine |
| 21 | falco simulate main.vcl |
| 22 | # Listens on localhost:3124 by default |
| 23 | # Then test with: curl http://localhost:3124/ |
| 24 | ``` |
| 25 | |
| 26 | When the user asks to "run locally" or "test locally", always compile **and** run `falco simulate` — linting alone doesn't run the VCL. |
| 27 | |
| 28 | ## Minimal Working Example |
| 29 | |
| 30 | **Backend naming**: Fastly VCL requires backends to use `F_` prefixed names (e.g., `F_origin`, `F_api`). Never use `backend default` — falco will reject it. Always set `req.backend` explicitly in `vcl_recv`. |
| 31 | |
| 32 | ```xvcl |
| 33 | #const ORIGIN_HOST = "api.example.com" |
| 34 | #const REGIONS = [("us", "us.example.com"), ("eu", "eu.example.com")] |
| 35 | |
| 36 | #for name, host in REGIONS |
| 37 | backend F_{{name}} { |
| 38 | .host = "{{host}}"; |
| 39 | .port = "443"; |
| 40 | .ssl = true; |
| 41 | } |
| 42 | #endfor |
| 43 | |
| 44 | sub vcl_recv { |
| 45 | #FASTLY recv |
| 46 | set req.backend = F_us; |
| 47 | return (lookup); |
| 48 | } |
| 49 | |
| 50 | sub vcl_deliver { |
| 51 | #FASTLY deliver |
| 52 | set resp.http.X-Served-By = "edge"; |
| 53 | return (deliver); |
| 54 | } |
| 55 | ``` |
| 56 | |
| 57 | ## XVCL Directives Summary |
| 58 | |
| 59 | Read [xvcl-directives.md](references/xvcl-directives.md) for complete syntax and examples of every directive. |
| 60 | |
| 61 | ### Constants — `#const` |
| 62 | |
| 63 | ```xvcl |
| 64 | #const NAME = value // type auto-inferred |
| 65 | #const NAME TYPE = value // explicit type |
| 66 | #const TTL INTEGER = 3600 |
| 67 | #const ORIGIN = "origin.example.com" |
| 68 | #const ENABLED BOOL = true |
| 69 | #const DOUBLE_TTL = TTL * 2 // expressions supported |
| 70 | #const BACKENDS = ["web1", "web2"] // lists |
| 71 | #const PAIRS = [("api", 8080), ("web", 80)] // tuples |
| 72 | ``` |
| 73 | |
| 74 | Constants are **compile-time only** — they do NOT become VCL variables. Always use `{{NAME}}` to emit their value. A bare constant name in VCL (e.g., `error 200 GREETING;`) passes through as a literal string, producing invalid VCL. Use `error 200 "{{GREETING}}";` instead. |
| 75 | |
| 76 | Use in templates: `"{{TTL}}"`, `{{ORIGIN}}`, `backend F_{{name}} { ... }` |
| 77 | |
| 78 | ### Template Expressions — `{{ }}` |
| 79 | |
| 80 | ```xvcl |
| 81 | {{CONST_NAME}} // constant substitution |
| 82 | {{PORT * 2}} // arithmetic |
| 83 | {{hex(255)}} // → "0xff" |
| 84 | {{format(42, '05d')}} // → "00042" |
| 85 | {{len(BACKENDS)}} // list length |
| 86 | {{value if condition else other}} // ternary |
| 87 | ``` |
| 88 | |
| 89 | Built-in functions: `range()`, `len()`, `str()`, `int()`, `hex()`, `format()`, `enumerate()`, `min()`, `max()`, `abs()` |
| 90 | |
| 91 | ### For Loops — `#for` / `#endfor` |
| 92 | |
| 93 | ```xvcl |
| 94 | #for i in range(5) // 0..4 |
| 95 | #for i in range(2, 8) // 2..7 |
| 96 | #for item in LIST_CONST // iterate list |
| 97 | #for name, host in TUPLES // tuple unpacking |
| 98 | #for idx, item in enumerate(LIST) // index + value |
| 99 | ``` |
| 100 | |
| 101 | ### Tables with Loops |
| 102 | |
| 103 | Use `#for` loops to populate VCL `table` declarations for O(1) lookups, instead of generating inline if-chains. |
| 104 | |
| 105 | ```xvcl |
| 106 | #const REDIRECTS = [ |
| 107 | ("/blog", "/articles"), |
| 108 | ("/about-us", "/about"), |
| 109 | ("/products/old-widget", "/products/widget-v2") |
| 110 | ] |
| 111 | |
| 112 | // O(1) hash-table lookup — the right pattern for data-driven VCL |
| 113 | table redirects STRING { |
| 114 | #for old_path, new_path in REDIRECTS |
| 115 | "{{old_path}}": "{{new_path}}", |
| 116 | #endfor |
| 117 | } |
| 118 | |
| 119 | sub vcl_recv { |
| 120 | if (table.contains(redirects, req.url.path)) { |
| 121 | error 801 table.lookup(redirects, req.url.path); |
| 122 | } |
| 123 | } |
| 124 | ``` |
| 125 | |
| 126 | Prefer populating VCL `table` declarations with `#for` loops over generating inline if-chains. Tables give O(1) hash lookups and are the idiomatic Fastly pattern for any data-driven routing, redirects, or configuration. |
| 127 | |
| 128 | ### Conditionals — `#if` / `#elif` / `#else` / `#endif` |
| 129 | |
| 130 | ```xvcl |
| 131 | #if PRODUCTION |