The Rules Engine is a flexible module that allows host companies to define JSON configurations for controlling various aspects of requests, rather than having to code exceptions in a standalone module or in the core of Prebid Server.
Most of the use cases for this version are centered around controlling which bid adapters are called in particular scenarios, based on:
The first phase is limited in several ways:
The team will tackle enhancements as important use cases drive priority.
For those who prefer to jump right in, this PBS-Go account configuration illustrates a Rules Engine experiment. The module is named prebid.rulesengine in PBS-Go and pb-rule-engine in PBS-Java. Their configuration and execution-plan field names differ; do not copy the Go examples directly into PBS-Java. The following configuration:
98% of the time:
2% of the time:
{
"hooks": {
"modules": {
"prebid": {
"rulesengine": {
"enabled": true,
"timestamp": "20250314 00:00:00",
"rulesets": [
{
"stage": "processed_auction_request",
"name": "remove-bidder-by-datacenter",
"version": "1234",
"modelgroups": [
{
"weight": 98,
"analyticsKey": "rm-bidder-by-dc",
"version": "4567",
"schema": [
{
"function": "dataCenter"
}
],
"default": [],
"rules": [
{
"conditions": [
"eu"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA"
],
"seatnonbid": 203,
"analyticsValue": "rm-eu"
}
},
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderD"
],
"seatnonbid": 203,
"ifSyncedId": false,
"analyticsValue": "rm-eu-nosync"
}
}
]
},
{
"conditions": [
"apac"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderB",
"bidderF"
],
"seatnonbid": 203,
"analyticsValue": "rm-apac"
}
}
]
}
]
},
{
"weight": 2,
"analyticsKey": "rm-bidder-by-dc",
"default": [
{
"function": "logATag",
"args": {
"analyticsValue": "control"
}
}
]
}
]
}
],
"generate_rules_from_bidderconfig": false
}
}
},
"enabled": true
}
}
Similar to the Floors feature, it’s assumed that an offline machine-learning algorithm will be part of a bidder optimization feedback loop to adjust to changing conditions. Here’s the assumed architecture:

The initial goal of this module is to define the runtime capabilities that are required to surgically select bidders. We’ve defined a technical interface and a runtime behavior. Some uses of the module might be manually configured, but the most valuable use of the system will be in the periodic offline machine learning. The goal there is for Prebid to step out of the way and let members of the ecosystem compete to do the best job.
We envision a time when, like the floors system, there are multiple vendors providing optimization services of different kinds.
Configuration is covered in several sections:
The following fields describe the PBS-Go account-level module configuration (hooks.modules.prebid.rulesengine). For PBS-Java, see its pb-rule-engine module configuration and use camel-case field names such as ruleSets and modelGroups instead of Go’s rulesets and modelgroups:
| Property | Scope | Type | Description | Example |
|---|---|---|---|---|
| enabled | optional | boolean | Whether the module as a whole is enabled. Allows for easy temporary deactivation. Defaults to true. | true |
| generate_rules_from_bidderconfig | optional | boolean | (PBS-Go only) Generate geoscope rules from bidder YAML configuration. Set explicitly; the Go default is false. |
false |
| rulesets[] | required | array of objects | One or more independent sets of rules. | none |
| timestamp | optional | string | Account-level rules configuration revision (informational in PBS-Go; the cache compares a hash of configuration). | “2025-03-14T00:00:00Z” |
| rulesets[] |
optional | string | Version of the ruleset. | “1234” |
| rulesets[] |
required | string | Which module stage this object applies to. Initially, the currently supported stage is processed_auction_request |
“processed_auction_request” |
| rulesets[] |
optional | string | Just for human readability. | “remove-bidder-by-datacenter” |
| rulesets[] |
required | array of objects | This is where the rule details are stored. It’s an array in case there’s a desire to A/B test different rule configs. Model groups work here like they do for floors. | |
| rulesets[] |
optional | integer | The relative weight of this specific modelGroup entry. Only one array entry within this modelGroup will be chosen. Default weight is 1, and they must range from 1 to 100. | 10 |
| rulesets[] |
optional | string | In case the machine learning system would like to track the specifics of an experiment. | “123” |
| rulesets[] |
optional | array of objects | The default results object to be used if anything goes wrong while traversing the rule tree or if there aren’t any rules defined. | |
| rulesets[] |
optional | string | Which results function to call. | “logATag” |
| rulesets[] |
optional | object | Which arguments to pass to the function. | |
| rulesets[] |
optional | array of objects | Array of functions used to define the tree. | |
| rulesets[] |
optional | string | Which schema function to call. | “deviceCountry” |
| rulesets[] |
optional | object | Named arguments for the schema function. | {“countries”:[“USA”,”CAN”]} |
| rulesets[] |
optional | string | Used to produce aTags and intended to identify an experiment or optimization target. | “456” |
| rulesets[] |
optional | array of objects | The rules themselves. If not present, only the default rule will be chosen. | |
| rulesets[] |
optional | array of strings | Defines a node on the rule tree. The order of the entries in this array must match the order defined in the schema. | [“true”, “false”, “app”] |
| rulesets[] |
optional | array of objects | One or more results functions to run when this leaf of the tree is chosen. | |
| rulesets[] |
optional | string | Which results function to call. | “logATag” |
| rulesets[] |
optional | object | Which arguments to pass to the function. |
For each ruleset, the Rules Engine creates a data structure called the ‘rule tree’. Each level of the tree is a particular dimension and each branch a specific value of that dimension. At runtime, the module will walk down the tree dimension-by-dimension until it finds a ‘leaf’. That final node contains the operations that will be applied to the request.
Mapping these concepts to the JSON syntax:
This model differs from how rules are handled in the floors system. In that model, the runtime system has the ability to “go back up” the tree if it finds a dead-end. While friendlier, that model can create performance problems. The Rules Engine system is a little harder to utilize, but it guarantees that each dimension is processed only once.
Here’s an example rule tree:

Tip: you can also think of the rule tree as an N-dimensional matrix where each intersection of schema function values resolves to a ‘results’ node.
See the runtime processing section below for more details.
The module provides a set of functions that parse the OpenRTB request for specific fields, extracting a particular value. For instance, finding out which specific country or channel is associated with this request. These functions are called ‘schema functions’ because they form the structure of the Rule Tree as described in the section above.
These functions operate on the OpenRTB request at the Processed Auction Request stage. Availability varies by server implementation; functions marked Java-only are not supported by PBS-Go.
| Schema Function | Args | Description | Output Example |
|---|---|---|---|
| deviceCountry | null | Returns OpenRTB device.geo.country. | “CAN” |
| deviceCountryIn | {"countries": ["USA", "CAN"]} |
Returns “true” if device.geo.country matches a country in the non-empty array (case sensitive); otherwise “false”. |
“true” |
| dataCenter | null | Returns the value of the datacenter, which is host-company specific. Please work with your host company to get the list of values available in your environment. | “eu-west” |
| dataCenterIn | {"datacenters": ["eu-west"]} |
Tests the datacenter against a non-empty array of host-specific codes (case sensitive). | “true” |
| channel | null | Returns ext.prebid.channel – e.g. web, amp, app. Returns empty string if there is no channel. | “app” |
| eidAvailable | null | Returns “true” if user.eids array exists and is non-empty. Returns “false” if user.eids doesn’t exist or is empty. | “false” |
| eidIn | {"sources": ["pubcid.org"]} |
Returns “true” if any supplied EID source is present; the array must not be empty. | “true” |
| userFpdAvailable | null | Scans user.ext.data and user.data Returns “true” if either user.ext.data or user.data exists and is non-empty. If neither exists with data, returns “false”. Maybe be extended in the future to support specific fields. | “false” |
| fpdAvailable | null | Returns “true” if any of the following objects exists and has data: user.ext.data, user.data, site.ext.data, site.content.data, app.ext.data, app.content.data. Otherwise returns “false”. | “false” |
| gppSidAvailable | null | Returns “true” if there are any values on regs.gpp_sid greater than 0. Otherwise returns “false”. | “true” |
| gppSidIn | {"sids": [7, 8]} |
Returns “true” if any of the regs.gpp_sid values are on the supplied array. (i.e. intersection is true). Otherwise returns “false”. | “true” |
| tcfInScope | null | Returns “true” if regs.gdpr is “1”. Otherwise returns “false”. | “true” |
| percent | {"pct": 90} |
Returns “true” if a random integer between 0 and 100 inclusive is less than the supplied number. If the ‘args’ is null, default to 5. | “true” |
| domain (Java only) | null | Returns {site,app,dooh}.publisher.domain | “example.com” |
| domainIn (Java only) | array of strings representing domains | Returns “true” or “false” based on whether any of the supplied domains is on {site,app,dooh}.publisher.domain or {site,app,dooh}.domain | “true” |
| bundle (Java only) | null | Returns app.bundle | “com.example.myapp” |
| bundleIn (Java only) | array of strings representing app bundles | Returns “true” or “false” based on whether any of the supplied bundles is on app.bundle. | “false” |
| mediaTypeIn (Java only) | {"types": ["video"]} |
This function is called for each impression object on the request. It returns “true” or “false” based on whether any of the supplied mediaTypes is on the imp. Valid mediaTypes are “banner”, “video”, “native” | “true” |
| adUnitCode (Java only) | null | This function is called for each impression object on the request. It returns the first of: imp.ext.gpid, imp.tagid, imp.ext.data.pbadslot, imp.ext.prebid.storedrequest.id | “/home/med-rect” |
| adUnitCodeIn (Java only) | array of strings | This function is called for each impression object on the request. It returns “true” or “false” based on whether any of the supplied strings is on imp.ext.gpid, imp.tagid, imp.ext.data.pbadslot, imp.ext.prebid.storedrequest.id. | “true” |
| deviceType (Java only) | null | Returns the OpenRTB device.devicetype. See the AdCOM device type list for the integers that may be returned. Returns empty string if no device.devicetype exists. | “1” |
| deviceTypeIn (Java only) | array of integers | Returns “true” or “false” based on whether any of the supplied integers matches device.devicetype. | “true” |
Notes:
{"countries":["USA","CAN"]}). For *In functions, the arrays must contain at least one value; empty arrays are invalid. "schema": [
{"function": "deviceCountry", "args": [["FRA","DEU","GBR"]]}, // returns true if the country matches any in this list
{"function": "deviceCountry", "args": [["USA","CAN"]]}, // returns true if the country matches any in this list
{"function": "percent", "args": [50]}, // returns true 50% of the time
{"function": "mediaType", "args": [["video"]]}, // returns true for imps that have an imp.video block
{"function": "channel"}, // returns "web", "app", or "amp"
{"function": "eidAvailable", "args": [["pubcid.org"]]}, // returns true if the EIDs array contains a SharedID
{"function": "userFpdAvailable" } // returns true if there's any User First Party Data available
],
Once the Rule Tree is processed, a ‘leaf’ is chosen. The syntax of the leaf is an array of results functions that operate on the request and/or analytics. (Someday, the system will likely support functions that operate on the response.)
These are the currently defined results functions:
These functions modify the imp[].ext.prebid.bidders object. They have the same set of parameters, the only difference between them being that exclude removes the named bidders while include keeps only the named bidders.
Here’s an example invocation that means:
"results": [{
"function": "excludeBidders",
"args": [{
"bidders": [ "bidderA"],
"seatnonbid": 203,
"analyticsValue": "rm-eu"
},{
"bidders": ["bidderD"],
"seatnonbid": 203,
"ifSyncedId": false,
"analyticsValue": "rm-eu-nosync"
}
]
}
]
Both functions expect an args object with the following parameters. Use multiple results entries to perform multiple include or exclude operations:
| Arg | Value Type | Required? | Meaning | Default |
|---|---|---|---|---|
| bidders | non-empty array of strings | yes | The bidders to include or exclude | none |
| seatnonbid | integer | no | The seatnonbid code to use | 203 |
| ifSyncedId | boolean | no | Whether to consider this bidder’s synced ID status | none |
| analyticsValue | string | no | Sent to the aTag | none |
Here’s how they work:
{
status: "success",
values: {
analyticsKey: "ANALYTICS_KEY", // defined at the modelGroup level
analyticsValue: "ANALYTICS_VAL",
modelVersion: "MODEL_VERSION",
conditionFired: CONDITION_ARRAY,
resultFunctions: ["exclude/includeBidders"],
// log the removed bidders for both include/exclude
biddersRemoved: ["bidderF", "bidderJ"],
seatnonbid: "SEATNONBID"
}
appliedTo: {
impIds: ["IMP.ID"]
}
}
Analytics tags are how Prebid Server modules communicate their activity to the analytics adapters. Each analytics adapter needs to know how to retrieve these values. Check with your analytics vendor to make sure they can receive this data.
The logATag results function communicates some details to the analytics adapters via the analytics tags. The idea is that data science teams will design experiments (e.g. try running bidderA on 5% of Brazilian traffic) and they will need analytics data for the experiment and the control. The syntax of the Rules Engine allows for flexibly defining how the analytics system sees various decisions made at runtime.
Here’s an example invocation that means:
"default": [{
"function": "logATag",
"args": {
"analyticsValue": "control"
}
}
]
There’s only one argument to this results function:
| Arg | Value Type | Required? | Meaning | Default |
|---|---|---|---|---|
| analyticsValue | string | yes | Sent to the aTag | none |
The resulting analytics tag as seen by analytics adapters is:
{
status: "success",
values: {
analyticsKey: "ANALYTICS_KEY", // defined at the modelGroup level
analyticsValue: "ANALYTICS_VAL",
modelVersion: "MODEL_VERSION",
conditionFired: CONDITION_ARRAY,
resultFunctions: ["logATag"]
}
appliedTo: {
impIds: ["*"]
}
}
For those curious about the technical details, here’s how the module works in detail:
processed_auction_request stage, exit (PBS-Go).{
"hooks": {
"modules": {
"prebid": {
"rulesengine": {
"enabled": true,
"timestamp": "20250314 00:00:00",
"rulesets": [
{
"stage": "processed_auction_request",
"name": "remove-bidder-by-datacenter",
"version": "1234",
"modelgroups": [
{
"weight": 100,
"analyticsKey": "rm-bidder-by-dc",
"version": "4567",
"schema": [
{
"function": "dataCenter"
}
],
"default": [],
"rules": [
{
"conditions": [
"eu"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA"
],
"seatnonbid": 203,
"analyticsValue": "rm-eu"
}
},
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderD"
],
"seatnonbid": 203,
"ifSyncedId": false,
"analyticsValue": "rm-eu-nosync"
}
}
]
},
{
"conditions": [
"apac"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderB",
"bidderF"
],
"seatnonbid": 203,
"analyticsValue": "rm-apac"
}
}
]
}
]
}
]
}
],
"generate_rules_from_bidderconfig": false
}
}
},
"enabled": true
}
}
{
"hooks": {
"modules": {
"pb-rule-engine": {
"enabled": true,
"timestamp": "20250314 00:00:00",
"ruleSets": [
{
"stage": "processed-auction-request",
"name": "remove-bidder-mediatype-percent",
"version": "6789",
"modelGroups": [
{
"weight": 100,
"analyticsKey": "test-video-bidders",
"version": "4567",
"schema": [
{
"function": "mediaTypeIn",
"args": {
"types": [
"video"
]
}
},
{
"function": "percent",
"args": {
"pct": 90
}
}
],
"default": [
{
"function": "logATag",
"args": {
"analyticsValue": "control"
}
}
],
"rules": [
{
"conditions": [
"true",
"true"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderD"
],
"analyticsValue": "leaf1"
}
}
]
}
]
}
]
}
]
}
},
"enabled": true
}
}
This is a complex example Rule Tree with 12 leaves:
Rules engine config:
{
"hooks": {
"modules": {
"prebid": {
"rulesengine": {
"enabled": true,
"timestamp": "20250314 00:00:00",
"rulesets": [
{
"stage": "processed_auction_request",
"name": "remove-bidder-by-country-channel-eid-userFpd",
"version": "5678",
"modelgroups": [
{
"weight": 100,
"analyticsKey": "filter-bidder",
"version": "4567",
"schema": [
{
"function": "deviceCountryIn",
"args": {
"countries": [
"FRA",
"DEU",
"GBR"
]
}
},
{
"function": "deviceCountryIn",
"args": {
"countries": [
"JPN",
"AUS",
"KOR"
]
}
},
{
"function": "channel"
},
{
"function": "eidIn",
"args": {
"sources": [
"pubcid.org"
]
}
},
{
"function": "userFpdAvailable"
}
],
"default": [],
"rules": [
{
"conditions": [
"true",
"false",
"app",
"true",
"*"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA"
],
"analyticsValue": "leaf1"
}
}
]
},
{
"conditions": [
"true",
"false",
"app",
"false",
"true"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA",
"bidderD"
],
"analyticsValue": "leaf2"
}
}
]
},
{
"conditions": [
"true",
"false",
"app",
"false",
"false"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA",
"bidderB"
],
"analyticsValue": "leaf3"
}
}
]
},
{
"conditions": [
"true",
"false",
"*",
"*",
"*"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderC"
],
"analyticsValue": "leaf4"
}
}
]
},
{
"conditions": [
"false",
"true",
"web",
"true",
"true"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA",
"bidderE"
],
"analyticsValue": "leaf5"
}
}
]
},
{
"conditions": [
"false",
"true",
"web",
"true",
"false"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA",
"bidderB",
"bidderE"
],
"analyticsValue": "leaf6"
}
}
]
},
{
"conditions": [
"false",
"true",
"web",
"false",
"*"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA",
"bidderD",
"bidderF"
],
"analyticsValue": "leaf7"
}
}
]
},
{
"conditions": [
"false",
"true",
"*",
"*",
"true"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA",
"bidderC"
],
"analyticsValue": "leaf8"
}
}
]
},
{
"conditions": [
"false",
"true",
"*",
"*",
"false"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderA",
"bidderF"
],
"analyticsValue": "leaf9"
}
}
]
},
{
"conditions": [
"false",
"false",
"web",
"*",
"*"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [],
"analyticsValue": "leaf10"
}
}
]
},
{
"conditions": [
"false",
"false",
"*",
"true",
"*"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderB",
"bidderE"
],
"analyticsValue": "leaf11"
}
}
]
},
{
"conditions": [
"false",
"false",
"*",
"false",
"*"
],
"results": [
{
"function": "excludeBidders",
"args": {
"bidders": [
"bidderE",
"bidderF"
],
"analyticsValue": "leaf12"
}
}
]
}
]
}
]
}
],
"generate_rules_from_bidderconfig": false
}
}
},
"enabled": true
}
}
And here’s how these scenarios would play out in various runtime conditions:
Input: request from country FRA, channel web, contains eid(pubcid.org), userFpd is available
Input: request from country JPN, contains eid(pubcid.org), userFpd is available
Input: request from country JPN, channel app, contains eid(pubcid.org), userFpd is available
Input: request from country FRA, channel is app, no eids, userFpd is available
Enable the hooks system and register the Rules Engine hook in the host execution plan. Here is a PBS-Go host-configuration example:
{
"hooks": {
"enabled": true,
"host_execution_plan": {
"endpoints": {
"/openrtb2/auction": {
"stages": {
"processed_auction_request": {
"groups": [{
"timeout": 10,
"hook_sequence": [{
"module_code": "prebid.rulesengine",
"hook_impl_code": "prebid-rulesengine-processed-auction-hook"
}]
}]
}
}
}
}
}
}
}
PBS-Java uses module code pb-rule-engine, hook implementation code pb-rule-engine-processed-auction-request-hook, and its own execution-plan field names. Consult your deployment’s hooks configuration before applying a host execution plan.
If you have any feedback about how you’d like to see the Rules Engine evolve, please let us know by joining the Prebid Server committee or posting an issue in the github repo.