
Set variables in Traffic Policy for cleaner, reusable configs
When our infrastructure engineers started dogfooding Traffic Policy in front of our APIs and apps, one of the first things they asked for was a way to store and reference variables within the state of a single request. The more advanced their usage got, the longer, less readable, and harder their configurations became—but being crafty folks, they figured out a workaround pretty quickly (which I will leave unmentioned).
That was enough of a signal that we needed to build them—and you—a better way.
We're now introducing the set-vars
Traffic Policy action, which lets you create and reference variables during a request or response. Once you set a variable, you can use it again later to filter traffic with expressions or to make your configurations more dynamic with interpolations.
With variables, your Traffic Policy configurations get smaller and simpler, which means they become more manageable and extensible once again. And while variables might feel like low-hanging fruit for configuring your API gateway, it's actually something that most others don't offer—and we believe that managing traffic should be both easy to do and easy to do right.
How to set and reference variables with ngrok
The first way you might use variables is to set a string or list.
on_http_request:
- actions:
- type: set-vars
config:
vars:
# Establish a list of available services and associated endpoints
- endpoints:
- foo
- bar
Or, you can set one variable based on another, like parsing the req.host
variable to take the full hostname, foo.example.com
, and parse out only the subdomain foo
.
on_http_request:
- actions:
- type: set-vars
config:
vars:
# If the hostname of the request is `foo.example.com`, set `subdomain` to foo
- subdomain: "${req.host.split('.example.com')[0].split('.')}"
Now that you've set variables, you can use them again in:
- Expressions, by referencing the variable directly:
var.NAME
. - CEL interpolations, by encapsulating the variable reference in the
${...}
syntax:${vars.NAME}
.
Here's an example with both, and which brings this whole example together to check whether the subdomain of an incoming request matches one of your available services.
on_http_request:
- actions:
- type: set-vars
config:
vars:
# Establish a list of available services and associated endpoints
- endpoints:
- foo
- bar
# If the hostname of the request is `foo.example.com`, set `subdomain` to foo
- subdomain: "${req.host.split('.example.com')[0].split('.')}"
- expressions:
# Check whether `subdomain` matches an available endpoint, and if so, forward to its internal endpoint
- "vars.subdomain in vars.endpoints"
actions:
- type: forward-internal
config:
url: "https://${vars.subdomain}.internal"
- actions:
# Deny all requests that don't match an available endpoint
- type: deny
Before we jump into other examples of how variables simplify your traffic management setups, one more power move to show off: You can also update your variables within the same execution of set-vars
, since Traffic Policy evaluates from top to bottom—if you use value
in another expression or interpolation, it evaluates to 4
. Plenty of ways to have fun with that in your policy-writing future.
on_http_request:
- actions:
- type: set-vars
config:
vars:
- value: ${int(2)}
- value: ${vars.value + int(2)}
Control authenticated users with OAuth and variable lists
Let's say you want to use our oauth
action to handle authentication to a dashboard or portal that only a few folks should have access to. Before set-vars
, the way you'd do that is to write an expression with a long embedded list:
on_http_request:
- actions:
- type: oauth
config:
provider: google
- expressions:
# Check the email used to authenticate via OAuth against allowed_users and deny those not present in the list
- "actions.ngrok.oauth.identity.email not in ['me@example.com', 'coworker@example.com', 'cto@example.com', 'trusted-contractor@gmail.com']"
actions:
- type: deny
Not particularly readable or maintainable whenever you need to add or remove someone from the list—and this example only uses five emails, when it's likely that your organization has an order of magnitude more accounts to authenticate.
With set-vars
, you can set an allowed_users
list once, maintain it in one place, and reuse it as many times as you need.
on_http_request:
- actions:
- type: set-vars
config:
vars:
# Create a list emails that are allowed to authenticate with OAuth
allowed_users:
- me@example.com
- coworker@example.com
- cto@example.com
- trusted-contractor@gmail.com
- actions:
- type: oauth
config:
provider: google
- expressions:
# Check the email used to authenticate via OAuth against allowed_users
# and deny those not present in the list
- "actions.ngrok.oauth.identity.email not in vars.allowed_users"
actions:
- type: deny
It adds a few lines to your Traffic Policy config, but it's also far more readable at a glance and easier to maintain for the long haul—just think about next time you need to deal with the aftermath of that trusted-contractor
no longer being so trusted.
Make blocking unwanted traffic more manageable
Let's say you want to protect your endpoints from AI bots like Anthropic, OpenAI, and Perplexity. You can make a list variable with the names of IP Intelligence category lists you want to block, then use that variable to block their requests with the deny Traffic Policy action.
on_http_request:
- actions:
- type: set-vars
config:
vars:
# IP categories to block
ai_ips:
- com.anthropic
- com.openai
- com.perplexity
- expressions:
- "conn.client_ip.categories in vars.ai_ips"
actions:
- deny
You might also want to start looking at Traffic Inspector for the JA4 fingerprint of potential threat actors. These fingerprints are not tied to IPs, but rather the source of a request, which means you can use them to figure out whether requests came from the same source, like a Python script crawling public sites for vulnerabilities.

Over time, you'll start to identify which JA4 fingerprints are associated with bad actors and requests you'd like to block, which means creating a new variable and filtering traffic based on those, too.
on_http_request:
- actions:
- type: set-vars
config:
vars:
# IP categories to block
ai_ips:
- com.anthropic
- com.openai
- com.perplexity"
# JA4 fingerprints to block
ja4s:
# IceID Malware Dropper
- ge11cn020000_9ed1ff1f7b03_cd8dafe26982
# SoftEther VPN
- t13d880900_fcb5b95cb75a_b0d3b4ac2a14
- expressions:
- "(conn.client_ip.categories in vars.ai_ips) || conn.tls.ja4_fingerprint in vars.ja4s"
actions:
- type: deny
Put request data in your 'language'
WIth Traffic Policy, you can access more than 100 variables for filtering and taking action on requests. We've put a fair bit of thought into how we namespace and organize these variables, but now you can use your own nomenclature for better maintainability over the long haul.
For example, the conn.client_ip
variable gives you access to a client's IP address. Instead of trying to keep that name in your head as you develop Traffic Policy rules, you could instead store its value as ip and reuse it anywhere.
on_http_request:
- actions:
- type: set-vars
config:
vars:
- ip: "${conn.client_ip}"
- type: custom-response
config:
content: "Hello, ${vars.ip}!"
You don't need to set variables this way—you can also use more direct CEL interpolation, like content: "Hello, ${conn.client_ip}"
—but we love that set-vars
now gives you the flexibility to make that choice for the future readability and maintainability of your traffic management rules.
What's next?
That’s a wrap on set-vars
. Cleaner Traffic Policy rules, less puzzling over embedded lists, and no more cursing over duplicating an expression again and again. We're bringing the polish, and sometimes, you're just one var
away from everything finally making sense.
Give set-vars
a spin by setting up a free account, and make sure you check out the docs for even more details. Questions? Give our support team a holler.