xvcl

SKILL.md

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

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

VCL Basics

Request Lifecycle

  1. vcl_recv — Request received, choose backend, decide cache behavior
  2. vcl_hash — Generate cache key
  3. vcl_hit — Cache hit handling
  4. vcl_miss — Cache miss, fetch from origin
  5. vcl_pass — Bypass cache
  6. vcl_fetch — Process origin response
  7. vcl_deliver — Send response to client
  8. vcl_error — Error handling
  9. vcl_log — Logging

Return Actions

Subroutine Common Returns
vcl_recv lookup, pass, error
vcl_fetch deliver, pass, error
vcl_deliver deliver
vcl_error deliver

Variable Types

declare local var.str STRING;
declare local var.num INTEGER;
declare local var.flag BOOL;
declare local var.time TIME;
declare local var.duration RTIME;
declare local var.decimal FLOAT;

Key Variables

Request: req.method, req.url, req.url.path, req.url.qs, req.http.* Response: resp.status, resp.http.* Backend Response: beresp.status, beresp.ttl, beresp.http.* Client: client.ip, client.geo.country_code

References

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
Weekly Installs
3
GitHub Stars
3
First Seen
5 days ago
Installed on
amp3
cline3
opencode3
cursor3
kimi-cli3
codex3