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-credentials

For 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.

PatternExampleUse caseNotes
String literal'"hello"'Inject a fixed value into a header or body.Wrap in single quotes in YAML.
Variablerequest.pathForward 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.

FieldTypeDescription
requestobjectrequest contains attributes about the incoming HTTP request
request.methodstringThe HTTP method of the request. For example, GET
request.uristringThe complete URI of the request. For example, http://example.com/path.
request.hoststringThe hostname of the request. For example, example.com.
request.schemestringThe scheme of the request. For example, https.
request.pathstringThe path of the request URI. For example, /path.
request.pathAndQuerystringThe path and query of the request URI. For example, /path?foo=bar.
request.versionstringThe version of the request. For example, HTTP/1.1.
request.headersobjectThe headers of the request.
request.bodystringThe body of the request. Warning: accessing the body will cause the body to be buffered.
request.startTimestringThe time the request started
request.endTimestringThe time the request completed
responseobjectresponse contains attributes about the HTTP response
response.codeintegerThe HTTP status code of the response.
response.headersobjectThe headers of the response.
response.bodystringThe body of the response. Warning: accessing the body will cause the body to be buffered.
envobjectenv contains selected process environment attributes exposed to CEL.
This does NOT expose raw environment variables, but rather a subset of well-known variables.
env.podNamestringThe name of the pod (when running on Kubernetes)
env.namespacestringThe namespace of the pod (when running on Kubernetes)
env.gatewaystringThe Gateway we are running as (when running on Kubernetes)
jwtobjectjwt contains the claims from a verified JWT token. This is only present if the JWT policy is enabled.
apiKeyobjectapiKey contains the claims from a verified API Key. This is only present if the API Key policy is enabled.
apiKey.keystring
basicAuthobjectbasicAuth contains the claims from a verified basic authentication Key. This is only present if the Basic authentication policy is enabled.
basicAuth.usernamestring
llmobjectllm contains attributes about an LLM request or response. This is only present when using an ai backend.
llm.streamingbooleanWhether the LLM response is streamed.
llm.requestModelstringThe model requested for the LLM request. This may differ from the actual model used.
llm.responseModelstringThe model that actually served the LLM response.
llm.providerstringThe provider of the LLM.
llm.inputTokensintegerThe number of tokens in the input/prompt.
llm.inputImageTokensintegerThe number of image tokens in the input/prompt.
llm.inputTextTokensintegerThe 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.inputAudioTokensintegerThe number of audio tokens in the input/prompt.
llm.cachedInputTokensintegerThe number of tokens in the input/prompt read from cache (savings)
llm.cacheCreationInputTokensintegerTokens written to cache (costs)
Not present with OpenAI
llm.outputTokensintegerThe number of tokens in the output/completion.
llm.outputImageTokensintegerThe number of image tokens in the output/completion.
llm.outputTextTokensintegerThe number of text tokens in the output/completion.
llm.outputAudioTokensintegerThe 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.reasoningTokensintegerThe number of reasoning tokens in the output/completion.
llm.totalTokensintegerThe total number of tokens for the request.
llm.serviceTierstringThe service tier the provider served the request under.
llm.countTokensintegerThe 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[]objectThe prompt sent to the LLM. Warning: accessing this has some performance impacts for large prompts.
llm.prompt[].rolestring
llm.prompt[].contentstring
llm.completion[]stringThe completion from the LLM. Warning: accessing this has some performance impacts for large responses.
llm.paramsobjectThe parameters for the LLM request.
llm.params.temperaturenumber
llm.params.top_pnumber
llm.params.frequency_penaltynumber
llm.params.presence_penaltynumber
llm.params.seedinteger
llm.params.max_tokensinteger
llm.params.encoding_formatstring
llm.params.dimensionsinteger
llmRequestanyllmRequest 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.
sourceobjectsource contains attributes about the source of the request.
source.addressstringThe IP address of the downstream connection.
source.portintegerThe port of the downstream connection.
source.identityobjectThe (Istio SPIFFE) identity of the downstream connection, if available.
source.identity.trustDomainstringThe trust domain of the identity.
source.identity.namespacestringThe namespace of the identity.
source.identity.serviceAccountstringThe service account of the identity.
source.subjectAltNames[]stringThe subject alt names from the downstream certificate, if available.
source.issuerstringThe issuer from the downstream certificate, if available.
source.subjectstringThe subject from the downstream certificate, if available.
source.subjectCnstringThe CN of the subject from the downstream certificate, if available.
mcpobjectmcp 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.methodNamestring
mcp.sessionIdstring
mcp.toolobject
mcp.tool.targetstringThe target handling the tool call after multiplexing resolution.
mcp.tool.namestringThe resolved tool name sent to the upstream target.
mcp.tool.argumentsobjectThe JSON arguments passed to the tool call.
mcp.tool.resultanyThe terminal tool result payload, if available.
mcp.tool.erroranyThe terminal JSON-RPC error payload, if available.
mcp.promptobject
mcp.prompt.targetstringThe target of the resource
mcp.prompt.namestringThe name of the resource
mcp.resourceobject
mcp.resource.targetstringThe target of the resource
mcp.resource.namestringThe name of the resource
backendobjectbackend contains information about the backend being used.
backend.namestringThe name of the backend being used. For example, my-service or service/my-namespace/my-service:8080.
backend.typestringThe type of backend. For example, ai, mcp, static, dynamic, or service.
backend.protocolstringThe protocol of backend. For example, http, tcp, a2a, mcp, or llm.
extauthzobjectextauthz contains dynamic metadata from ext_authz filters
extprocobjectextproc contains dynamic metadata from ext_proc filters
metadataobjectmetadata 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.

FunctionPurpose
jsonParse a string or bytes as JSON. Example: json(request.body).some_field.
toJsonConvert a CEL value into a JSON string. Example: toJson({"hello": "world"}).
unvalidatedJwtPayloadParse 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
withCEL does not allow variable bindings. with allows doing this. Example: json(request.body).with(b, b.field_a + b.field_b)
variablesvariables 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.
mapValuesmapValues applies a function to all values in a map. map in CEL only applies to map keys.
filterKeysReturns 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_")).
mergemerge joins two maps. Example: {"a":2,"k":"v"}.merge({"a":3}) results in {"a":3,"k":"v"}.
flattenUsable 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.
flattenRecursiveUsable only for logging and tracing. Like flatten but recursively flattens multiple levels.
base64.encodeEncodes a string to a base64 string. Example: base64.encode("hello").
base64.decodeDecodes 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.encodeComputes the SHA-1 digest of a string or bytes value and returns the lowercase hex string. Example: sha1.encode("hello").
sha256.encodeComputes the SHA-256 digest of a string or bytes value and returns the lowercase hex string. Example: sha256.encode("hello").
md5.encodeComputes the MD5 digest of a string or bytes value and returns the lowercase hex string. Example: md5.encode("hello").
randomGenerates a number float from 0.0-1.0
defaultResolves to a default value if the expression cannot be resolved. For example default(request.headers["missing-header"], "fallback")
coalesceEvaluates 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")
regexReplaceReplace the string matching the regular expression. Example: "/id/1234/data".regexReplace("/id/[0-9]*/", "/id/{id}/") would result in the string /id/{id}/data.
failUnconditionally fail an expression.
uuidRandomly generate a UUIDv4

Function examples

These functions are used in the documentation examples in this section.

FunctionExample 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.

PhaseDescriptionValid target types
PostRoutingDefault. Transformations are applied after the routing decision is made.Gateway, Listener, HTTPRoute
PreRoutingTransformations 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:

Agentgateway assistant

Ask me anything about agentgateway configuration, features, or usage.

Note: AI-generated content might contain errors; please verify and test all returned information.

Tip: one topic per conversation gives the best results. Use the + button in the chat header to start a new conversation.

Switching topics? Starting a new conversation improves accuracy.
↑↓ navigate select esc dismiss

What could be improved?

Your feedback helps us improve assistant answers and identify docs gaps we should fix.

Need more help? Join us on Discord: https://discord.gg/y9efgEmppm

Want to use your own agent? Add the Solo MCP server to query our docs directly. Get started here: https://search.solo.io/.