← Files Fastly Agent ToolkitARCHIVED FILE
skills/xvcl/references/xvcl-directives.md
15.8 KB · Oct 10, 2026 · 18:29 UTC
# XVCL Directives — Complete Reference
All XVCL directives are resolved at compile time. The compiler runs 6 passes:
1. Join multi-line directives (unbalanced brackets auto-join lines)
2. Extract `#const` declarations
3. Process `#include` files
4. Extract `#inline` macros
5. Extract `#def` functions
6. Process `#for`, `#if`, `#let`, template expressions `{{ }}`
## Table of Contents
- [Constants — #const](#constants)
- [Template Expressions — {{ }}](#template-expressions)
- [For Loops — #for](#for-loops)
- [Conditionals — #if](#conditionals)
- [Variable Shorthand — #let](#variable-shorthand)
- [Functions — #def](#functions)
- [Inline Macros — #inline](#inline-macros)
- [Includes — #include](#includes)
- [Multi-line Directives](#multi-line-directives)
- [Error Messages](#error-messages)
---
## Constants
```xvcl
#const NAME = value // type auto-inferred
#const NAME TYPE = value // explicit type
```
### Supported Types
- `INTEGER` — whole numbers: `#const PORT INTEGER = 8080`
- `STRING` — text: `#const HOST STRING = "example.com"`
- `FLOAT` — decimals: `#const VERSION FLOAT = 1.5`
- `BOOL` — `true`/`false` (case-insensitive, `True`/`False` also work): `#const DEBUG BOOL = false`
### Type Inference
When type is omitted, it's inferred from the value:
```xvcl
#const PORT = 8080 // INTEGER
#const HOST = "example.com" // STRING
#const ENABLED = true // BOOL
#const RATIO = 1.5 // FLOAT
#const ITEMS = ["a", "b"] // LIST
#const PAIRS = [("a", 1)] // LIST of tuples
```
### Expressions in Constants
```xvcl
#const BASE_TTL = 3600
#const DOUBLE_TTL = BASE_TTL * 2 // 7200
#const LABEL = "v" + str(VERSION) // string concatenation
```
### Lists and Tuples
```xvcl
#const REGIONS = ["us-east", "us-west", "eu-west"]
#const BACKENDS = [("api", "api.example.com", 443), ("web", "www.example.com", 80)]
```
Multi-line lists are supported (lines auto-join when brackets are unbalanced):
```xvcl
#const BACKENDS = [
("api", "api.example.com", 443),
("web", "www.example.com", 80),
("static", "static.example.com", 443)
]
```
### Scope
Constants defined in any file (including included files) are available globally. Constants must be defined before use.
---
## Template Expressions
Embed compile-time expressions anywhere in code using `{{ }}`:
```xvcl
{{CONST_NAME}} // constant value
{{PORT * 2}} // arithmetic
{{value if condition else other}} // ternary
```
### Built-in Functions
| Function | Description | Example |
| ------------------- | ------------------------ | ---------------------------------------- |
| `range(n)` | Integers 0 to n-1 | `range(5)` → 0,1,2,3,4 |
| `range(start, end)` | Integers start to end-1 | `range(2, 5)` → 2,3,4 |
| `len(x)` | Length of list or string | `len(REGIONS)` → 3 |
| `enumerate(list)` | (index, value) tuples | `enumerate(["a","b"])` → (0,"a"),(1,"b") |
| `str(x)` | Convert to string | `str(8080)` → "8080" |
| `int(x)` | Convert to integer | `int("42")` → 42 |
| `hex(n)` | Integer to hex string | `hex(255)` → "0xff" |
| `format(x, fmt)` | Format with Python spec | `format(42, '05d')` → "00042" |
| `min(a, b, ...)` | Minimum value | `min(5, 3)` → 3 |
| `max(a, b, ...)` | Maximum value | `max(5, 3)` → 5 |
| `abs(n)` | Absolute value | `abs(-42)` → 42 |
| `true` / `false` | Boolean literals (also `True`/`False` in expressions) | |
### Operators in Expressions
- Arithmetic: `+`, `-`, `*`, `/`, `//` (integer division), `%`
- Comparison: `==`, `!=`, `<`, `>`, `<=`, `>=`
- Boolean: `and`, `or`, `not`
- String: `+` (concatenation)
### Examples
```xvcl
#const PORT = 8080
set req.http.X-Port = "{{PORT}}";
set req.http.X-Double = "{{PORT * 2}}";
set req.http.X-Hex = "{{hex(PORT)}}";
set req.http.X-Padded = "{{format(42, '05d')}}";
set req.http.X-Debug = "{{ENV if ENV != 'prod' else 'disabled'}}";
// In backend names
#for i in range(3)
backend web_{{i}} { .port = "{{8080 + i}}"; }
#endfor
```
---
## For Loops
```xvcl
#for variable in iterable
// body
#endfor
```
### Iteration Sources
```xvcl
// Range (single argument)
#for i in range(5)
// i = 0, 1, 2, 3, 4
#endfor
// Range (two arguments)
#for i in range(2, 8)
// i = 2, 3, 4, 5, 6, 7
#endfor
// List constant
#const REGIONS = ["us", "eu", "ap"]
#for region in REGIONS
backend F_{{region}} { .host = "{{region}}.example.com"; }
#endfor
// Tuple unpacking
#const BACKENDS = [("api", "api.example.com"), ("web", "www.example.com")]
#for name, host in BACKENDS
backend F_{{name}} { .host = "{{host}}"; }
#endfor
// Triple unpacking
#const SERVICES = [("api", "api.example.com", 443), ("web", "www.example.com", 80)]
#for name, host, port in SERVICES
backend F_{{name}} {
.host = "{{host}}";
.port = "{{port}}";
}
#endfor
// Enumerate (index + value)
#for idx, region in enumerate(REGIONS)
set req.http.X-Region-{{idx}} = "{{region}}";
#endfor
```
### Nested Loops
```xvcl
#const REGIONS = ["us", "eu"]
#const ENVS = ["prod", "staging"]
#for region in REGIONS
#for env in ENVS
backend {{region}}_{{env}} {
.host = "{{region}}-{{env}}.example.com";
}
#endfor
#endfor
// Generates: us_prod, us_staging, eu_prod, eu_staging
```
### Loop Variables in Expressions
Loop variables are available in `{{ }}` template expressions within the loop body:
```xvcl
#for i in range(3)
set req.http.X-Index-{{i}} = "{{i * 10}}";
#endfor
```
---
## Conditionals
```xvcl
#if condition
// code
#endif
#if condition
// code
#else
// fallback
#endif
#if condition1
// ...
#elif condition2
// ...
#else
// ...
#endif
```
### Supported Conditions
```xvcl
#if PRODUCTION // boolean constant
#if VERSION == 2 // equality
#if PORT > 8000 // comparison
#if DEBUG and ENV == "prod" // boolean operators
#if not ENABLED // negation
#if VERSION >= 2 or LEGACY // combined
```
### Nesting
```xvcl
#if PRODUCTION
#if VERSION >= 2
set req.http.X-Feature = "new";
#else
set req.http.X-Feature = "legacy";
#endif
#endif
```
### Dead Code Elimination
False branches are completely excluded from the compiled output — no runtime cost.
---
## Variable Shorthand
`#let` is shorthand for `declare local` + `set`:
```xvcl
#let cache_key STRING = req.url.path;
#let counter INTEGER = 42;
#let is_cached BOOL = true;
```
Expands to:
```vcl
declare local var.cache_key STRING;
set var.cache_key = req.url.path;
declare local var.counter INTEGER;
set var.counter = 42;
declare local var.is_cached BOOL;
set var.is_cached = true;
```
Supported types: `STRING`, `INTEGER`, `FLOAT`, `BOOL`
---
## Functions
Reusable functions with typed parameters and return values. Compiled to VCL subroutines with parameters passed via `req.http.X-Func-*` headers.
### Single Return Value
```xvcl
#def function_name(param1 TYPE, param2 TYPE) -> RETURN_TYPE
// VCL body
return value;
#enddef
```
Example:
```xvcl
#def normalize_path(path STRING) -> STRING
declare local var.result STRING;
set var.result = std.tolower(path);
set var.result = regsuball(var.result, "//+", "/");
return var.result;
#enddef
sub vcl_recv {
#FASTLY recv
#let clean STRING = normalize_path(req.url.path);
}
```
### Tuple Return (Multiple Values)
```xvcl
#def function_name(params) -> (TYPE1, TYPE2)
// body
return val1, val2;
#enddef
```
Example:
```xvcl
#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
sub vcl_recv {
#FASTLY recv
declare local var.k STRING;
declare local var.v STRING;
set var.k, var.v = parse_pair("host:example.com");
}
```
### How Functions Compile
A function call like `set var.result = normalize_path(req.url.path);` expands to:
```vcl
set req.http.X-Func-normalize_path-path = req.url.path;
call normalize_path;
set var.result = req.http.X-Func-normalize_path-Return;
```
Type conversions are automatic:
- `INTEGER` ↔ STRING via `std.itoa()` / `std.atoi()`
- `FLOAT` ↔ STRING via string concatenation / `std.atof()`
- `BOOL` ↔ STRING via `"true"`/`"false"` strings
### Functions with Logic
```xvcl
#def should_cache(url STRING) -> BOOL
declare local var.cacheable BOOL;
if (url ~ "^/api/") {
set var.cacheable = false;
} else if (url ~ "\.(jpg|png|css|js)$") {
set var.cacheable = true;
} else {
set var.cacheable = false;
}
return var.cacheable;
#enddef
```
### Parameters Without Types
Parameters without explicit types default to `STRING`:
```xvcl
#def concat(a, b) -> STRING
declare local var.result STRING;
set var.result = a + b;
return var.result;
#enddef
```
---
## Inline Macros
Zero-overhead compile-time text substitution. Use for simple expressions; use `#def` for complex logic.
```xvcl
#inline macro_name(param1, param2, ...)
expression
#endinline
```
### Basic Usage
```xvcl
#inline cache_key(url, host)
digest.hash_md5(url + "|" + host)
#endinline
#inline normalize_host(host)
std.tolower(regsub(host, "^www\.", ""))
#endinline
sub vcl_hash {
#FASTLY hash
set req.hash += cache_key(req.url, normalize_host(req.http.Host));
}
```
### Auto-Parenthesization
When macro arguments contain operators, they are automatically wrapped in parentheses during expansion. This prevents precedence bugs when a macro's result is used inside a function call:
```xvcl
#inline cache_key(url, host)
digest.hash_md5(url + "|" + host)
#endinline
// Calling with a complex expression as argument:
set req.hash += cache_key(req.url, std.tolower(req.http.Host));
// Arguments are safely substituted into the digest.hash_md5() call
```
**Note:** Fastly VCL does not support parenthesized grouped expressions in string concatenation (e.g., `("a") + ("b")` is invalid). Macros work best when their body uses VCL function calls like `digest.hash_md5()`, `std.tolower()`, or `regsub()` rather than raw `+` operators.
### Nested Macros
Macros can call other macros (up to 10 levels of nesting):
```xvcl
#inline normalize_host(host)
std.tolower(regsub(host, "^www\.", ""))
#endinline
#inline cache_key(url, host)
digest.hash_md5(url + "|" + host)
#endinline
// Nested call:
set req.hash += cache_key(req.url, normalize_host(req.http.Host));
// Inner macro expands first, then outer
```
### Macros vs Functions
| | Macro (`#inline`) | Function (`#def`) |
| --------- | ------------------------------ | -------------------------------- |
| Expansion | Compile-time text substitution | Runtime subroutine call |
| Overhead | None | Subroutine call + header passing |
| Body | Single expression only | Full VCL statements |
| Return | Expression result | Explicit `return` |
| Use case | Simple expressions, wrappers | Complex logic, conditionals |
---
## Includes
```xvcl
#include "relative/path/file.xvcl" // relative to current file
#include <path/file.xvcl> // searches -I include paths
```
### Include Path Resolution
1. Relative to the directory of the current file
2. Relative to paths specified with `-I` flag on the command line
### Include-Once Semantics
Each file is included at most once. Duplicate includes are silently skipped:
```xvcl
#include "shared.xvcl" // processed
#include "shared.xvcl" // skipped (already included)
```
### Circular Include Detection
Circular includes produce an error:
```
Error: Circular include detected: main.xvcl -> util.xvcl -> main.xvcl
```
### Shared Constants
Constants defined in included files are available to the parent file and all subsequent includes.
### Typical Project Layout
```
main.xvcl // entry point
includes/
config.xvcl // #const definitions (shared across all files)
backends.xvcl // backend declarations
security.xvcl // security headers, ACLs
routing.xvcl // request routing logic
caching.xvcl // cache rules
```
```xvcl
// main.xvcl
#include "includes/config.xvcl"
#include "includes/backends.xvcl"
#include "includes/security.xvcl"
#include "includes/routing.xvcl"
#include "includes/caching.xvcl"
```
Compile with: `uvx xvcl main.xvcl -o main.vcl -I ./includes`
---
## Multi-line Directives
Lines with unbalanced brackets/parentheses are automatically joined. This allows readable multi-line declarations:
```xvcl
#const BACKENDS = [
("api", "api.example.com", 443),
("web", "www.example.com", 80),
("static", "static.example.com", 443)
]
#for name, port in [
("api", 8080),
("web", 80)
]
backend {{name}} { .port = "{{port}}"; }
#endfor
```
Multi-line function calls are also auto-joined:
```xvcl
set var.result = some_function(
arg1,
arg2,
arg3
);
```
---
## Error Messages
Errors use a rustc-style structured format with rule IDs, source lines, carets, and actionable help. Use `--error-format json` for machine-readable output.
### Format
```
error[undefined-function]: Function 'normlize_path' is not defined
--> main.xvcl:12
|
12 | set var.clean = normlize_path(req.url.path);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= did you mean: normalize_path?
= note: defined functions: normalize_path, parse_pair
```
Each error includes a `rule` ID (e.g., `undefined-function`, `unclosed-block`, `return-count-mismatch`) that identifies the kind of problem.
### Common Errors
| Rule ID | Cause | Fix |
| -------------------------- | ---------------------------------- | ------------------------------------------------------- |
| `compile-error` | Undefined constant or variable | Check spelling; use `--debug` to list available names |
| `invalid-for` | Malformed `#for` syntax | Use `#for VARIABLE in EXPRESSION` |
| `unclosed-block` | Missing `#endfor` or `#endif` | Add the matching closing directive |
| `undefined-function` | Call to undefined XVCL function | Check spelling; VCL builtins like `regsub` are ignored |
| `return-count-mismatch` | Wrong number of return variables | Match variable count to function's return type |
| `func-arg-count` | Wrong number of arguments | Check function/macro signature |
| `macro-recursion` | Infinite macro expansion | Remove circular macro references |
| `unpack-count-mismatch` | Tuple size mismatch in `#for` | Match variable count to tuple size |
| `file-not-found` | Missing include or input file | Check path; add `-I` flag |
### Name Suggestions
When an undefined name or function is used, xvcl suggests similar defined names:
```
error[compile-error]: Name 'PROT' is not defined
--> main.xvcl:5
= did you mean: PORT?
= note: available: PRODUCTION, PORT, MAX_AGE
```
### JSON Error Output
Use `--error-format json` for structured diagnostics (useful for editor integrations):
```bash
uvx xvcl main.xvcl -o main.vcl --error-format json
```
### Debugging Tips
- Use `--debug` / `-v` to see expansion traces for each pass
- Use `--source-maps` to track which include file generated which output lines
- Use `--error-format json` for machine-readable diagnostics
- Start with a minimal file and add complexity incrementally
SHA-256: 9960fa4c1d160c6eab65cb348ff9e42b913632e9ded5e63a86c5c50ba013fbea