Templating language
Agentgateway transformation templates are written in Common Expression Language (CEL). CEL is a fast, portable, and safely executable language that goes beyond declarative configurations. With CEL, you can develop more complex expressions in a readable, developer-friendly syntax and use them to extract and transform values from requests and responses.
You can apply CEL transformations to routes for LLM providers, MCP servers, inference services, agents, and HTTP services.
Where can CEL be used?
Transformations can modify the headers and body of a request or response. Each transformation is expressed as static values, built-in CEL functions, or context variables to extract and inject information.
AgentgatewayPolicy structure for transformations:
traffic:
transformation:
request:
set:
add:
remove:
body:
metadata:
response:
set:
add:
remove:
body:
metadata:Header transformations
Use header transformations to add, overwrite, or remove headers on a request before it reaches the upstream, or on a response before it reaches the client. Three operations are supported:
set: Creates a header or overwrites it if it already exists.add: Adds a value to a header without removing existing values. If the header already exists, the header is not overwritten. Instead, another header with the same key is added and set to the value of your transformation.remove: Strips a header entirely.
Each set or add entry takes a name and a value. The value is a CEL expression that can be a static string, a call to a built-in function, or a reference to a context variable such as request.headers["x-foo"], jwt.sub, or llm.requestModel.
You might use these transformations for injecting routing hints, auth context, or tracing metadata that the upstream expects but the client does not send.
Request header example to build a forwarded URI from context variables:
traffic:
transformation:
request:
set:
- name: x-forwarded-uri
value: 'request.scheme + "://" + request.host + request.path'For more information, see Create redirect URLs.
Response header example to encode a header value with a CEL function, set a dynamic status code with a conditional expression, and remove a header:
traffic:
transformation:
response:
set:
- name: x-user-id-encoded
value: 'base64.encode(request.headers["x-user-id"])'
- name: ":status"
value: 'request.uri.contains("foo=bar") ? 401 : 403'
remove:
- access-control-allow-credentialsFor more information, see Inject response headers.
Body transformations
Use body transformations to replace the entire body of a request or response with a new value. The body field takes a single CEL expression that must evaluate to a string. You can build the new body from static values, CEL functions such as json() and toJson(), or context variables such as request.body or response.body.
Response body example to construct a JSON response body from request context variables:
traffic:
transformation:
response:
body: '"{\"path\": \"" + request.path + "\", \"method\": \"" + request.method + "\"}"'For more information, see Inject response bodies.
Request body example to strip internal fields and merge in defaults before forwarding:
traffic:
transformation:
request:
body: 'toJson(json(request.body).filterKeys(k, !k.startsWith("x_")).merge({"model": "gpt-4o", "max_tokens": 2048}))'For more information, see Filter and merge request body fields.
Pre-compute values with metadata
Use the metadata field to evaluate a CEL expression once and make the result available as metadata.<name> in the set, add, and body fields of the same transformation. metadata keys are evaluated before any other fields in the transformation, so they can be referenced anywhere in the same block.
This field is useful when the same complex expression would otherwise be repeated. For example, if you parse a JSON body field to inject it as a header and also use it in a condition, writing it twice creates noise and a maintenance risk. With metadata, you write it once.
traffic:
transformation:
response:
metadata:
parsedModel: 'string(json(response.body).model)'
set:
- name: x-actual-model
value: metadata.parsedModel
- name: x-model-changed
value: 'metadata.parsedModel != string(json(request.body).model) ? "true" : "false"'metadata values are only available within the same transformation block. They are not accessible in access log or tracing CEL expressions.
For a full example, see Inject LLM model headers.
CEL syntax quick reference
Use these patterns to build expressions for header values, body content, and conditional logic in your transformation policies.
| Pattern | Example | Use case | Notes |
|---|---|---|---|
| String literal | '"hello"' | Inject a fixed value into a header or body. | Wrap in single quotes in YAML. |
| Variable | request.path | Forward a request property as-is, such as echoing the path into a header. | No quotes needed. |
| Concatenation | '"prefix-" + request.path' | Build a value from a mix of static text and dynamic variables, such as constructing a URL or adding a namespace prefix to a header value. | Wrap in single quotes in YAML. |
| Conditional expression | 'request.headers["x-foo"] == "bar" ? "yes" : "no"' | Conditionally set a value based on a request property, such as changing a response status code when a specific query parameter is present. The pattern is condition ? value_if_true : value_if_false. In the example, if the x-foo header equals "bar", the expression returns "yes"; otherwise it returns "no". | Wrap in single quotes in YAML. Both sides must be the same type, such as strings or integers on both sides. |
| Header lookup | 'request.headers["x-my-header"]' | Read the value of a specific request header and forward it or use it in another expression. | Wrap in single quotes in YAML. |
YAML quoting: When a CEL expression is a string literal or starts with a quote, wrap it in single quotes in YAML so the inner double quotes are preserved:
Context variables
Context variables give CEL expressions access to information about the current request, response, and connection. They are populated automatically by agentgateway at runtime so you do not need to declare or configure them. Use them to read values such as headers, path, method, JWT claims, or LLM model names and inject them into headers, bodies, or conditions.
Variables are only populated when they are relevant to the current request. For example, jwt is only present when a JWT has been validated, and llm is only present when the route is backed by an LLM provider. Referencing an absent variable in a CEL expression produces an error. Use default(expression, fallback) to avoid this error.
Not all variables are available in every policy type. The table below lists which variables are available depending on where the CEL expression is evaluated.
| Field | Type | Description |
|---|---|---|
request | object | request contains attributes about the incoming HTTP request |
request.method | string | The HTTP method of the request. For example, GET |
request.uri | string | The complete URI of the request. For example, http://example.com/path. |
request.host | string | The hostname of the request. For example, example.com. |
request.scheme | string | The scheme of the request. For example, https. |
request.path | string | The path of the request URI. For example, /path. |
request.pathAndQuery | string | The path and query of the request URI. For example, /path?foo=bar. |
request.version | string | The version of the request. For example, HTTP/1.1. |
request.headers | object | The headers of the request. |
request.body | string | The body of the request. Warning: accessing the body will cause the body to be buffered. |
request.startTime | string | The time the request started |
request.endTime | string | The time the request completed |
response | object | response contains attributes about the HTTP response |
response.code | integer | The HTTP status code of the response. |
response.headers | object | The headers of the response. |
response.body | string | The body of the response. Warning: accessing the body will cause the body to be buffered. |
env | object | env contains selected process environment attributes exposed to CEL.This does NOT expose raw environment variables, but rather a subset of well-known variables. |
env.podName | string | The name of the pod (when running on Kubernetes) |
env.namespace | string | The namespace of the pod (when running on Kubernetes) |
env.gateway | string | The Gateway we are running as (when running on Kubernetes) |
jwt | object | jwt contains the claims from a verified JWT token. This is only present if the JWT policy is enabled. |
apiKey | object | apiKey contains the claims from a verified API Key. This is only present if the API Key policy is enabled. |
apiKey.key | string | |
basicAuth | object | basicAuth contains the claims from a verified basic authentication Key. This is only present if the Basic authentication policy is enabled. |
basicAuth.username | string | |
llm | object | llm contains attributes about an LLM request or response. This is only present when using an ai backend. |
llm.streaming | boolean | Whether the LLM response is streamed. |
llm.requestModel | string | The model requested for the LLM request. This may differ from the actual model used. |
llm.responseModel | string | The model that actually served the LLM response. |
llm.provider | string | The provider of the LLM. |
llm.inputTokens | integer | The number of tokens in the input/prompt. |
llm.inputImageTokens | integer | The number of image tokens in the input/prompt. |
llm.inputTextTokens | integer | The number of text tokens in the input/prompt. Note: this field is only set in multi-modal calls where the total token count is split out by text/image/audio; for standard all-text calls, this is unset. |
llm.inputAudioTokens | integer | The number of audio tokens in the input/prompt. |
llm.cachedInputTokens | integer | The number of tokens in the input/prompt read from cache (savings) |
llm.cacheCreationInputTokens | integer | Tokens written to cache (costs) Not present with OpenAI |
llm.outputTokens | integer | The number of tokens in the output/completion. |
llm.outputImageTokens | integer | The number of image tokens in the output/completion. |
llm.outputTextTokens | integer | The number of text tokens in the output/completion. |
llm.outputAudioTokens | integer | The number of audio tokens in the output/completion. Note: this field is only set in multi-modal calls where the total token count is split out by text/image/audio; for standard all-text calls, this is unset. |
llm.reasoningTokens | integer | The number of reasoning tokens in the output/completion. |
llm.totalTokens | integer | The total number of tokens for the request. |
llm.serviceTier | string | The service tier the provider served the request under. |
llm.countTokens | integer | The number of tokens in the request, when using the token counting endpoint These are not counted as ‘input tokens’ since they do not consume input tokens. |
llm.prompt | []object | The prompt sent to the LLM. Warning: accessing this has some performance impacts for large prompts. |
llm.prompt[].role | string | |
llm.prompt[].content | string | |
llm.completion | []string | The completion from the LLM. Warning: accessing this has some performance impacts for large responses. |
llm.params | object | The parameters for the LLM request. |
llm.params.temperature | number | |
llm.params.top_p | number | |
llm.params.frequency_penalty | number | |
llm.params.presence_penalty | number | |
llm.params.seed | integer | |
llm.params.max_tokens | integer | |
llm.params.encoding_format | string | |
llm.params.dimensions | integer | |
llmRequest | any | llmRequest contains the raw LLM request before processing. This is only present during LLM policies;policies occurring after the LLM policy, such as logs, will not have this field present even for LLM requests. |
source | object | source contains attributes about the source of the request. |
source.address | string | The IP address of the downstream connection. |
source.port | integer | The port of the downstream connection. |
source.identity | object | The (Istio SPIFFE) identity of the downstream connection, if available. |
source.identity.trustDomain | string | The trust domain of the identity. |
source.identity.namespace | string | The namespace of the identity. |
source.identity.serviceAccount | string | The service account of the identity. |
source.subjectAltNames | []string | The subject alt names from the downstream certificate, if available. |
source.issuer | string | The issuer from the downstream certificate, if available. |
source.subject | string | The subject from the downstream certificate, if available. |
source.subjectCn | string | The CN of the subject from the downstream certificate, if available. |
mcp | object | mcp contains attributes about the MCP request.Request-time CEL only includes identity fields such as tool, prompt, or resource.Post-request CEL may also include fields like methodName, sessionId, and tool payloads. |
mcp.methodName | string | |
mcp.sessionId | string | |
mcp.tool | object | |
mcp.tool.target | string | The target handling the tool call after multiplexing resolution. |
mcp.tool.name | string | The resolved tool name sent to the upstream target. |
mcp.tool.arguments | object | The JSON arguments passed to the tool call. |
mcp.tool.result | any | The terminal tool result payload, if available. |
mcp.tool.error | any | The terminal JSON-RPC error payload, if available. |
mcp.prompt | object | |
mcp.prompt.target | string | The target of the resource |
mcp.prompt.name | string | The name of the resource |
mcp.resource | object | |
mcp.resource.target | string | The target of the resource |
mcp.resource.name | string | The name of the resource |
backend | object | backend contains information about the backend being used. |
backend.name | string | The name of the backend being used. For example, my-service or service/my-namespace/my-service:8080. |
backend.type | string | The type of backend. For example, ai, mcp, static, dynamic, or service. |
backend.protocol | string | The protocol of backend. For example, http, tcp, a2a, mcp, or llm. |
extauthz | object | extauthz contains dynamic metadata from ext_authz filters |
extproc | object | extproc contains dynamic metadata from ext_proc filters |
metadata | object | metadata contains values set by transformation metadata expressions. |
Built-in functions
Built-in functions extend CEL with capabilities that go beyond simple variable access and arithmetic. Use them to parse and serialize data, encode values, generate identifiers, and manipulate strings and maps. For example, json() parses a raw request body string into a map so you can access individual fields, base64.encode() encodes a header value for safe transmission, and default() provides a fallback when a variable might not be present.
The output of one function can be passed as the input of another. For example, string(json(request.body).model) parses the body, extracts the model field, and converts the result to a string in a single expression.
| Function | Purpose |
|---|---|
json | Parse a string or bytes as JSON. Example: json(request.body).some_field. |
toJson | Convert a CEL value into a JSON string. Example: toJson({"hello": "world"}). |
unvalidatedJwtPayload | Parse the payload section of a JWT without verifying the signature. This splits the token, base64url-decodes the middle segment, and parses it as JSON. Example: unvalidatedJwtPayload(request.headers.authorization.split(" ")[1]).sub |
with | CEL does not allow variable bindings. with allows doing this. Example: json(request.body).with(b, b.field_a + b.field_b) |
variables | variables exposes all of the variables available as a value. CEL otherwise does not allow accessing all variables without knowing them ahead of time. Warning: this automatically enables all fields to be captured. |
mapValues | mapValues applies a function to all values in a map. map in CEL only applies to map keys. |
filterKeys | Returns a new map keeping only entries where the key matches the predicate (must evaluate to bool). Example: {"a":1,"b":2}.filterKeys(k, k == "a") results in {"a":1}. To remove keys, invert the predicate: m.filterKeys(k, !k.startsWith("x_")). |
merge | merge joins two maps. Example: {"a":2,"k":"v"}.merge({"a":3}) results in {"a":3,"k":"v"}. |
flatten | Usable only for logging and tracing. flatten will flatten a list or struct into many fields. For example, defining headers: 'flatten(request.headers)' would log many keys like headers.user-agent: "curl", etc. |
flattenRecursive | Usable only for logging and tracing. Like flatten but recursively flattens multiple levels. |
base64.encode | Encodes a string to a base64 string. Example: base64.encode("hello"). |
base64.decode | Decodes a string in base64 format. Example: string(base64.decode("aGVsbG8K")). Warning: this returns bytes, not a String. Various parts of agentgateway will display bytes in base64 format, which may appear like the function does nothing if not converted to a string. |
sha1.encode | Computes the SHA-1 digest of a string or bytes value and returns the lowercase hex string. Example: sha1.encode("hello"). |
sha256.encode | Computes the SHA-256 digest of a string or bytes value and returns the lowercase hex string. Example: sha256.encode("hello"). |
md5.encode | Computes the MD5 digest of a string or bytes value and returns the lowercase hex string. Example: md5.encode("hello"). |
random | Generates a number float from 0.0-1.0 |
default | Resolves to a default value if the expression cannot be resolved. For example default(request.headers["missing-header"], "fallback") |
coalesce | Evaluates expressions from left to right and returns the first one that resolves successfully to a non-null value. null values are skipped while searching, but if every expression is either null or an error and at least one expression resolved to null, the result is null. Unlike default, it swallows any error from earlier expressions, not just missing keys or undeclared references. Example: coalesce(request.headers["x-id"], json(request.body).id, "fallback") |
regexReplace | Replace the string matching the regular expression. Example: "/id/1234/data".regexReplace("/id/[0-9]*/", "/id/{id}/") would result in the string /id/{id}/data. |
fail | Unconditionally fail an expression. |
uuid | Randomly generate a UUIDv4 |
Function examples
These functions are used in the documentation examples in this section.
| Function | Example topic |
|---|---|
base64.encode(bytes) | Encode base64 |
base64.decode(string) | Encode base64 |
default(expression, fallback) | Validate and set request body defaults |
expression.with(variable, result) | Rewrite dynamic path segments |
fail() | Validate and set request body defaults |
json(string) | Inject response bodies |
map.filterKeys(k, predicate) | Filter and merge request body fields |
map.merge(map2) | Filter and merge request body fields |
metadata.<name> | Inject LLM model headers |
random() | Generate request tracing headers |
string(value) | Encode base64 |
string.contains(substring) | Change the response status |
string.regexReplace(pattern, replacement) | Rewrite dynamic path segments |
toJson(value) | Filter and merge request body fields |
uuid() | Generate request tracing headers |
variables() | Enrich access logs |
Transformation phases
Transformations in spec.traffic support a phase field that controls when the policy is evaluated in the request lifecycle. If phase is omitted, PostRouting is used.
| Phase | Description | Valid target types |
|---|---|---|
PostRouting | Default. Transformations are applied after the routing decision is made. | Gateway, Listener, HTTPRoute |
PreRouting | Transformations are applied before the routing decision is made. Useful for gateway-level gates that apply to all routes. | Gateway, Listener |
Example:
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: http
traffic:
phase: PreRouting
transformation:
request:
set:
- name: x-phase
value: '"pre-routing"'Next steps
To learn more about how to use CEL, refer to the following resources: