NOTE: Available in release MS-0288, currently scheduled for end of October 2026
A reporting selector lets the people using a website narrow every report on it to one value, such as a store, a region or a lead stage. Your app decides what can be chosen. The platform handles the rest: the dropdown on the page, remembering the choice, and putting it into your index queries safely.
This article builds a lead stage selector from start to finish:
Register the selector in your app, with a function that returns the stages to choose from
Filter an index query on the selector's placeholder, $selectedLeadStages
Add the Reporting Selector component to a website page and point it at your selector
Requirements. Reporting selectors need a platform version that includes controllerMappings.addReportingSelector, and reporting-lib 6.3.2 or later on the website for the Reporting Selector component. Ecommerce stores already come as a selector: KProducts 1.6.8 registers $selectedStores, so you can use it without writing any code.
How it works
Your app registers a selector with a name, a title and a values function that returns what may be chosen.
The Reporting Selector component shows those values as a dropdown. The participant's choice is stored in a cookie for the whole site, so it applies on every page.
Index queries reference the selector as
$
. Before the query runs, the platform replaces the placeholder with the chosen values as a JSON array, after checking each one against your values function. When nothing valid is chosen, it uses every value, so an unfiltered page shows all its data.
Step 1: Register the selector
Add a script to your app or lib's APP-INF folder. This example is APP-INF/leadStageSelector.js, for an app on the Nashorn engine (engineVersion="1.1"):
/* global controllerMappings, services, formatter */
// Lead stage reporting selector: index queries filter with stageName in $selectedLeadStages
if (controllerMappings.addReportingSelector) {
controllerMappings
.addReportingSelector('selectedLeadStages')
.title('Lead stage')
.description('Names of the lead stages chosen with the lead stage selector, or every stage when none is chosen')
.valuesFunction('listLeadStagesForSelector')
.build();
}
function listLeadStagesForSelector() {
var stages = [];
formatter.foreach(services.leadManager.allStages.entrySet(), function (entry) {
var name = formatter.toString(entry.key);
var desc = formatter.toString(entry.value);
stages.push({
name: name,
title: formatter.isEmpty(desc) ? name : desc
});
});
return stages;
}
List the script in controllers.xml so it loads when the app starts:
/APP-INF/app.js
/APP-INF/leadStageSelector.js
The builder methods:
addReportingSelector(name): the selector's name, which is also its query placeholder without the $. Start with a letter, and use only letters, digits and underscores.
title(text): the label on the dropdown button and in the component's settings, eg Lead stage: All.
description(text): shown with the placeholder under View all query parameters on the index query page. Write it for the person building the query.
valuesFunction(fn): the name of a global function, or the function itself.
build(): required. Call it at the top level of a script listed in controllers.xml. Registrations made after the app has started are ignored.
Keep the if (controllerMappings.addReportingSelector) guard. Without it, your app fails to load on a platform from before reporting selectors existed.
The values function
It is called with no arguments and returns an array of {name, title} objects. name is what goes into the query, so it must match the values stored in the field you filter on. Here that's the lead's stageName. title is what participants see in the dropdown.
It runs each time a query uses the placeholder and each time the selector renders, so keep it cheap.
A selector narrows results, it never restricts access. With nothing chosen, the placeholder becomes every value the function returns. Don't use a selector to limit what a user may see. Keep organisation or profile scoping as its own filter in the query.
Once the app has loaded, the selector appears in the developer IDE under Extension points (1), in a Reporting selectors group (2). The registration (3) and the values function (4) are in the script beside it:
Apps on GraalJS
Registration is the same in a GraalJS app (engineVersion="2.0"). The values function is still looked up by name, so assign it to globalThis rather than exporting it from the module:
// app.mjs (engineVersion="2.0")
if (controllerMappings.addReportingSelector) {
controllerMappings
.addReportingSelector('selectedRegions')
.title('Region')
.description('Regions chosen with the region selector, or every region when none is chosen')
.valuesFunction('listRegionsForSelector')
.build();
}
// Called from Java by name, so it must be on globalThis rather than exported
globalThis.listRegionsForSelector = () => [
{ name: 'north', title: 'North' },
{ name: 'south', title: 'South' }
];
Your values function can only call Java methods that are exported to GraalJS. services.leadManager.allStages, used in the lead stage example above, is not available from GraalJS yet. Check the method you plan to use before relying on it.
Step 2: Filter an index query on the selector
Go to Reporting > Index Queries, open or create a query on the index that holds your data (here, the lead index), and click Edit filters. Add a rule on the field holding the value (1), choose the in operator (2), and type the placeholder as the value (3). View all query parameters (circled) lists every placeholder you can use.
Your selector is listed there with the built-in parameters, using the description you registered:
Save the filters. The query page's filter summary shows the rule:
If you write the query JSON yourself, use the placeholder as the whole value of a terms clause:
With Qualify lead chosen, the platform runs it as:
{ "terms": { "stageName": ["Qualify"] } }
Only the quoted forms "$selectedLeadStages" and ["$selectedLeadStages"] are replaced with checked, escaped values. Don't put the placeholder inside a longer string, such as a query_string.
Step 3: Add the selector to a website page
Open the page in the content editor and click Add content. Search for reporting selector (1) and drag the Reporting Selector component (2) onto the page. Put it above the reports it controls.
Click the component's settings cog (1). Under Participants can filter by (2), choose your selector. The list shows every selector registered by the apps enabled on this website, by its title.
Then add the reports, eg a Single Value, Pie Chart or Query Table, and choose the index query from Step 2 in each one's settings. If you build pages in code, the component's markup is:
Participants see a dropdown (1) listing the values your function returned (2):
Choosing one reloads the page, and every report that uses the query shows only that stage:
How the selection behaves
Remembered across the site. The choice is kept in a cookie named after the selector (here selectedLeadStages), set for the whole site. It applies to every page whose reports use the placeholder, until the participant chooses Clear selection.
Checked on every request. A cookie value your values function doesn't return is ignored, and the query uses every value. A participant can't use the cookie to reach data outside your list, or to change the query.
Works when logged out. Anonymous visitors get the same filtering. Make sure the reports on the page are ones they may see.
Names must be unique. A selector named after a built-in parameter, eg selectedOrgs, is ignored with a warning in the log. If two apps register the same name, the first app enabled wins.
Leave the cookie name alone. The builder has a cookieName method, but keep the default. Placeholders are matched by prefix elsewhere in query processing, so a cookie named after part of the placeholder, eg selectedLeadStage for $selectedLeadStages, can corrupt it.
Troubleshooting
The selector isn't in the component's settings. The app that registers it isn't enabled on this website, or its script isn't listed in controllers.xml. Check the IDE's Extension points view for the app.
The dropdown says "Nothing to choose from yet". The values function returned an empty array. Check what it returns for the current user.
Reports show nothing. The names your values function returns don't match the values stored in the field the query filters on. Compare them with a query that has no filter.
Reports don't change when a value is chosen. The report's index query doesn't use the placeholder, or uses it inside a longer string.
The component says "Choose a selector in this component's settings". No selector is chosen yet, or the website runs a platform without reporting selectors.