xvcl
Trigger and scope
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.
Do NOT trigger for: debugging existing .vcl files without XVCL, Fastly API/CLI ops, Fastly Compute, or Terraform — even if they mention VCL.
Writing VCL with XVCL
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.
Quick Start
# Compile (no install needed with uvx)
uvx xvcl main.xvcl -o main.vcl
# Lint the output
falco lint main.vcl
# Run locally — this is how you "run" VCL on your machine
falco simulate main.vcl
# Listens on localhost:3124 by default
# Then test with: curl http://localhost:3124/
When the user asks to "run locally" or "test locally", always compile and run falco simulate — linting alone doesn't run the VCL.
Minimal Working Example
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.
#const ORIGIN_HOST = "api.example.com"
#const REGIONS = [("us", "us.example.com"), ("eu", "eu.example.com")]
#for name, host in REGIONS
backend F_{{name}} {
.host = "{{host}}";
.port = "443";
.ssl = true;
}
#endfor
sub vcl_recv {
#FASTLY recv
set req.backend = F_us;
return (lookup);
}
sub vcl_deliver {
#FASTLY deliver
set resp.http.X-Served-By = "edge";
return (deliver);
}
XVCL Directives Summary
Read xvcl-directives.md for complete syntax and examples of every directive.
Constants — #const
#const NAME = value // type auto-inferred
#const NAME TYPE = value // explicit type
#const TTL INTEGER = 3600
#const ORIGIN = "origin.example.com"
#const ENABLED BOOL = true
#const DOUBLE_TTL = TTL * 2 // expressions supported
#const BACKENDS = ["web1", "web2"] // lists
#const PAIRS = [("api", 8080), ("web", 80)] // tuples
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.
Use in templates: "{{TTL}}", {{ORIGIN}}, backend F_{{name}} { ... }
Template Expressions — {{ }}
{{CONST_NAME}} // constant substitution
{{PORT * 2}} // arithmetic
{{hex(255)}} // → "0xff"
{{format(42, '05d')}} // → "00042"
{{len(BACKENDS)}} // list length
{{value if condition else other}} // ternary
Built-in functions: range(), len(), str(), int(), hex(), format(), enumerate(), min(), max(), abs()
For Loops — #for / #endfor
#for i in range(5) // 0..4
#for i in range(2, 8) // 2..7
#for item in LIST_CONST // iterate list
#for name, host in TUPLES // tuple unpacking
#for idx, item in enumerate(LIST) // index + value
Tables with Loops
Use #for loops to populate VCL table declarations for O(1) lookups, instead of generating inline if-chains.
#const REDIRECTS = [
("/blog", "/articles"),
("/about-us", "/about"),
("/products/old-widget", "/products/widget-v2")
]
// O(1) hash-table lookup — the right pattern for data-driven VCL
table redirects STRING {
#for old_path, new_path in REDIRECTS
"{{old_path}}": "{{new_path}}",
#endfor
}
sub vcl_recv {
if (table.contains(redirects, req.url.path)) {
error 801 table.lookup(redirects, req.url.path);
}
}
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.
Conditionals — #if / #elif / #else / #endif
#if PRODUCTION
set req.http.X-Env = "prod";
#elif STAGING
set req.http.X-Env = "staging";
#else
set req.http.X-Env = "dev";
#endif
Supports: boolean constants, comparisons (==, !=, <, >), operators (and, or, not).
Variable Shorthand — #let
#let cache_key STRING = req.url.path;
// expands to:
// declare local var.cache_key STRING;
// set var.cache_key = req.url.path;
Functions — #def / #enddef
// Single return value
#def normalize_path(path STRING) -> STRING
declare local var.result STRING;
set var.result = std.tolower(path);
return var.result;
#enddef
// Tuple return (multiple values)
#def parse_pair(s STRING) -> (STRING, STRING)
declare local var.key STRING;
declare local var.value STRING;
set var.key = regsub(s, ":.*", "");
set var.value = regsub(s, "^[^:]*:", "");
return var.key, var.value;
#enddef
// Call sites
set var.clean = normalize_path(req.url.path);
set var.k, var.v = parse_pair("host:example.com");
Functions compile to VCL subroutines with parameters passed via req.http.X-Func-* headers.
Inline Macros — #inline / #endinline
#inline cache_key(url, host)
digest.hash_md5(url + "|" + host)
#endinline
// Zero-overhead text substitution. Auto-parenthesizes arguments
// containing operators to prevent precedence bugs.
set req.hash += cache_key(req.url, req.http.Host);
Includes — #include
#include "includes/backends.xvcl" // relative path
#include <stdlib/security.xvcl> // include path (-I)
Include-once semantics. Circular includes are detected and reported.
Compilation
# Basic
uvx xvcl input.xvcl -o output.vcl
# With include paths
uvx xvcl main.xvcl -o main.vcl -I ./includes -I ./shared
# Debug mode (shows expansion traces)
uvx xvcl main.xvcl -o main.vcl --debug
# Source maps (adds BEGIN/END INCLUDE markers)
uvx xvcl main.xvcl -o main.vcl --source-maps
| Option | Description |
|---|---|
-o, --output |
Output file (default: replace .xvcl with .vcl) |
-I, --include |
Add include search path (repeatable) |
--debug / -v |
Show expansion traces |
--source-maps |
Add source location comments |
--error-format |
Error output format: text (default) or json |
Common Mistakes
- Bare constant names in VCL:
error 200 GREETING;passes through as a literal string. Useerror 200 "{{GREETING}}";with template syntax. - Generating if-chains instead of tables: When you have data-driven routing or redirects, always populate a VCL
tablewith#for— not an inline if-chain. If-chains are O(n); tables are O(1). - Forgetting
#FASTLYmacros: Every VCL subroutine (vcl_recv,vcl_fetch,vcl_deliver,vcl_error,vcl_hit,vcl_miss,vcl_pass) needs#FASTLY recv(or the appropriate name) at the top. - Using
backend default: Fastly VCL requiresF_prefixed backend names. Usebackend F_origin { ... }andset req.backend = F_origin;.
VCL Gotchas
VCL runtime pitfalls that are easy to get wrong:
- No modulo operator: VCL has no
%operator. For traffic splitting, usesubstr()on a hash digest:if (substr(digest.hash_sha256(client.ip), 0, 1) ~ "^[0-7]$")gives ~50%. Or userandomint(0, 99) < 50. - Vary MUST be set in
vcl_fetch: The Vary header controls the cache key. Setting it only invcl_deliveris too late — the object is already cached without Vary dimensions. Always append Vary invcl_fetch(and optionally mirror invcl_deliverfor client-visible headers). Never overwrite existing Vary: check and append. req.url.pathis read-only in falco tests: In test subroutines, useset req.url = "/path"instead ofset req.url.path = "/path". The.pathproperty is computed fromreq.urland cannot be set directly.req.requestis deprecated: Usereq.methodinstead. Falco accepts both butreq.methodis the modern form.- Cookie parsing: Use
subfield(req.http.Cookie, "name", ";")instead of regex. Regex likeCookie ~ "name=(\w+)"false-matches cookies with similar prefixes (e.g.,name_v2=X).
References
For VCL basics (request lifecycle, return actions, variable types), see the VCL syntax and subroutines references below.
Read the relevant reference file completely before implementing specific features.
| Topic | File | Use when... |
|---|---|---|
| XVCL Directives | xvcl-directives.md | Writing any XVCL code — complete syntax for all directives |
| VCL Syntax | vcl-syntax.md | Working with data types, operators, control flow |
| Subroutines | subroutines.md | Understanding request lifecycle, custom subs |
| Headers | headers.md | Manipulating HTTP headers |
| Backends | backends.md | Configuring origins, directors, health checks |
| Caching | caching.md | Setting TTL, grace periods, cache keys |
| Strings | strings.md | String manipulation functions |
| Crypto | crypto.md | Hashing, HMAC, base64 encoding |
| Tables/ACLs | tables-acls.md | Lookup tables, access control lists |
| Testing VCL | testing-vcl.md | Writing unit tests, assertions, test helpers |
Project Structure
vcl/
├── main.xvcl
├── config.xvcl # shared constants
└── includes/
├── backends.xvcl
├── security.xvcl
├── routing.xvcl
└── caching.xvcl
More from fastly/fastly-agent-toolkit
fastly
Configures, manages, and debugs the Fastly CDN platform — covering service and backend setup, caching and VCL, security features like DDoS/WAF/NGWAF/rate limiting/bot management, TLS certificates and cache purging, the Compute platform, and the REST API. Use when working with Fastly services or domains, setting up edge caching or origin shielding, configuring security features, making Fastly API calls, enabling products, or looking up Fastly documentation. Also applies when troubleshooting 503 errors or SSL/TLS certificate mismatches on Fastly, and for configuring logging endpoints, load balancing, ACLs, or edge dictionaries.
45viceroy
Runs Fastly Compute WASM applications locally with Viceroy, specifically for Rust and Component Model projects. Use when starting a local Fastly Compute dev server with Viceroy, configuring fastly.toml for local backend overrides and store definitions, running Rust unit tests with cargo-nextest against the Compute runtime, debugging Compute apps locally, adapting core WASM modules to the Component Model, or troubleshooting local Compute testing issues (connection refused, missing backends, store config). For non-Rust Compute work or understanding the Compute API, prefer the fastlike skill instead — its source code is easier to understand as a Fastly Compute API reference.
42fastly-cli
Executes Fastly CLI commands for managing CDN services, Compute deploys, and edge infrastructure. Use when running `fastly` CLI commands, creating or managing Fastly services from the terminal, deploying Fastly Compute applications, managing backends/domains/VCL snippets via command line, purging cache, configuring log streaming, setting up TLS certificates, managing KV/config/secret stores, checking service stats, authenticating with Fastly SSO, or working with fastly.toml. Also applies when working with Fastly service IDs in CLI context, or with `fastly service`, `fastly compute`, `fastly auth`, or any Fastly CLI subcommand. Covers service CRUD, version management, autocloning, and troubleshooting common CLI errors.
42falco
Lints, tests, simulates, and formats Fastly VCL code using the falco tool. Also serves as the authoritative VCL reference via the falco Go source, which implements Fastly's full VCL dialect. Use when validating VCL syntax, running VCL linting, testing VCL locally, simulating VCL request handling, formatting VCL files, writing VCL unit tests with assertions, debugging VCL logic errors, looking up VCL function signatures or variable scopes, understanding VCL subroutine behavior, or running `falco lint`/`falco simulate`/`falco test`/`falco fmt`. Also applies when working with VCL syntax errors, type mismatches in VCL, choosing which VCL subroutine to use, or setting up a local VCL development and testing environment.
39fastlike
Runs Fastly Compute WASM binaries locally and serves as the authoritative reference for Compute platform internals. The fastlike source code is highly readable and covers the host ABI, caching and purging APIs, KV/config/secret store interfaces, rate limiting with counters and penalty boxes, ACL lookups, the full request lifecycle, and backend fetch semantics. Use when working with Compute runtime internals or host calls, understanding how edge data stores behave at runtime, exploring the WASM Component Model adaptation layer, or testing WASM binaries locally. Prefer this skill over Viceroy for any non-Rust Compute work — its source code is easier to understand as a Fastly Compute API reference.
18fastly-ngwaf
Performs an internal audit of Fastly Next-Gen WAF (NGWAF) workspaces to audit that critical templated protection rules are configured and enabled. Use when auditing NGWAF workspace security posture, checking for missing or disabled login protection rules (LOGINDISCOVERY, LOGINATTEMPT, LOGINSUCCESS, LOGINFAILURE), auditing credit card validation rules (CC-VAL-ATTEMPT, CC-VAL-FAILURE, CC-VAL-SUCCESS), auditing gift card protection rules (GC-VAL-ATTEMPT, GC-VAL-FAILURE, GC-VAL-SUCCESS), or identifying potential login endpoints not covered by NGWAF rules.
5