Skip to content

Custom Filters

A Custom Filter runs your own Apex against the records an audience has narrowed down so far, and keeps the ones your logic accepts. Use one when the rule cannot be expressed with fields and related objects — an eligibility service, a scoring model, a check against an external system, or any decision that already lives in your own code.

Once a class is deployed, it appears in the audience builder as Custom Filter, alongside Parent Relationship, Related Records, and Aggregate. The option is only offered on objects your class declares support for, so an organization with no Custom Filters sees no change.

A Custom Filter is an Apex class implementing lb.CustomFilter_Interface:

global interface CustomFilter_Interface {
Set<Id> filter(Set<Id> resultIds, List<CustomFilterParameter> parameters, CustomFilterContext context);
List<SObjectType> forObjects();
List<CustomFilterParameter> getParameters();
String getLabel();
String getDescription();
}
Method What it does When CAB calls it
filter Decides which records survive. Return the subset of resultIds that passes; anything you leave out is removed from the audience. Once per batch during every audience run, and again for the record counts shown while someone edits the audience.
forObjects Declares which objects the filter may be attached to. Return null or an empty list to allow every object. When the builder decides whether to offer Custom Filter on a node.
getParameters Declares the configuration the filter needs. CAB renders an input per parameter and hands the values back to filter. Return null for none. When the builder renders the filter’s Settings, and again before each run to merge stored values with your declarations.
getLabel The name users pick from. Without it they would have to read your Apex class name. Wherever the filter is listed or displayed.
getDescription One or two sentences explaining what the filter does. Shown under the picker, and given to Cabby so it can reason about the filter. Same as getLabel.

In practice, extend lb.CustomFilterBase instead of implementing the interface directly. It supplies a default for every method except filter, so you write only what your filter actually needs, and it adds helpers for reading parameter values.

Your class must be global and have an accessible no-argument constructor.

This filter asks an eligibility service the organization already owns which of the current records qualify for an offer, and keeps those. What that service does — a scoring model, a rules engine, a call to another system — is irrelevant to CAB; the filter’s only job is to turn its answer into a set of ids.

global with sharing class OfferEligibility extends lb.CustomFilterBase {
global override String getLabel() {
return 'Eligible for offer';
}
global override String getDescription() {
return 'Keeps records the offer eligibility service approves for the selected offer.';
}
global override List<SObjectType> forObjects() {
return new List<SObjectType>{ Contact.SObjectType };
}
global override List<lb.CustomFilterParameter> getParameters() {
lb.CustomFilterParameter offer = new lb.CustomFilterParameter(
'offerCode', 'Offer code', lb.CustomFilterParameter.DATA_TYPE_TEXT
);
offer.description = 'The offer to check eligibility against.';
offer.isRequired = true;
return new List<lb.CustomFilterParameter>{ offer };
}
global override Set<Id> filter(Set<Id> resultIds,
List<lb.CustomFilterParameter> parameters, lb.CustomFilterContext context) {
String offerCode = lb.CustomFilterBase.stringValue(parameters, 'offerCode');
// Your own logic. CAB neither knows nor cares what happens in here.
Map<Id, Boolean> approved = OfferEligibilityService.evaluate(resultIds, offerCode);
Set<Id> passing = new Set<Id>();
for (Id recordId : resultIds) {
if (approved.get(recordId) == true) {
passing.add(recordId);
}
}
return passing;
}
}

The user adds this filter to a Contact audience, types an offer code, and every contact the service rejects drops out of the audience.

filter is a predicate over the records it is handed, not over the whole audience. CAB processes large audiences in batches, so it calls your method once per batch, each time with a different subset of ids. A fresh instance is constructed for every call, so instance state does not carry over between them.

Two consequences worth planning for:

  • Logic that must see every record at once — “the highest scoring 100 contacts”, for example — cannot be expressed in a Custom Filter, because no single call sees the whole audience. Rank inside your own logic against a fixed threshold instead.
  • Work is repeated per batch. Pass the whole resultIds set to your service in one call, as the example does, rather than calling it per record.

Ids you return that are not of the audience’s object, or that were not in resultIds, are rejected or ignored. A Custom Filter can only ever narrow an audience, never widen it.

Each lb.CustomFilterParameter you return from getParameters becomes an input in the filter’s Settings.

Property Purpose
name The key you read the value back by. Must be unique within the filter.
label Shown above the input.
description Optional help text shown with the input.
dataType Which input to render. See the table below.
isRequired When true, the audience cannot be saved or run without a value.
referenceSettings For a record parameter, which object to pick from and whether several may be chosen.
picklistSettings For a picklist parameter, where the choices come from and whether several may be chosen.
value The default when you declare it; the configured value when it comes back to filter.
Data type Constant Shown as
Text DATA_TYPE_TEXT A text field
Number DATA_TYPE_NUMBER A number field
Checkbox DATA_TYPE_BOOLEAN A checkbox
Date DATA_TYPE_DATE A date field
Date and time DATA_TYPE_DATETIME A date field and a time field
Picklist DATA_TYPE_PICKLIST A dropdown, single or multi-select
Record DATA_TYPE_ID A record picker, single or multi-select

Values always reach your class as text. Read them with the helpers on lb.CustomFilterBase rather than parsing them yourself:

String offerCode = lb.CustomFilterBase.stringValue(parameters, 'offerCode');
Decimal threshold = lb.CustomFilterBase.decimalValue(parameters, 'threshold');
Boolean active = lb.CustomFilterBase.booleanValue(parameters, 'activeOnly');
Date since = lb.CustomFilterBase.dateValue(parameters, 'since');
Datetime cutoff = lb.CustomFilterBase.datetimeValue(parameters, 'cutoff');
Id campaignId = lb.CustomFilterBase.idValue(parameters, 'campaignId');

Use the constructor that takes an SObjectType. Your class receives the selected record’s Id.

lb.CustomFilterParameter campaign = new lb.CustomFilterParameter(
'campaignId', 'Campaign member of', Campaign.SObjectType
);

Pass true as a fourth argument and the user can pick several records instead of one. Read the selection with idValues, which returns a list for both shapes — so you can make a parameter multi-select later without changing how you read it.

lb.CustomFilterParameter campaigns = new lb.CustomFilterParameter(
'campaignIds', 'Member of any campaign', Campaign.SObjectType, true
);
List<Id> campaignIds = lb.CustomFilterBase.idValues(parameters, 'campaignIds');

A picklist’s choices come from one of two places, and you pick which by the constructor you call.

From a Salesforce picklist field, which reads its active values and the user’s translated labels every time the filter is opened — so it stays current as the field changes:

lb.CustomFilterParameter sources = new lb.CustomFilterParameter(
'leadSources', 'Lead sources',
new lb.CustomFilterParameter.PicklistSettings(Contact.LeadSource, true)
);

Or from a fixed list you declare, for choices that are your filter’s own rather than a field’s:

lb.CustomFilterParameter mode = new lb.CustomFilterParameter(
'mode', 'Match mode',
new lb.CustomFilterParameter.PicklistSettings(new List<String>{ 'Strict', 'Relaxed' })
);

In both, the second argument makes the parameter multi-select. Read a single choice with stringValue and a multi-select with stringValues:

String mode = lb.CustomFilterBase.stringValue(parameters, 'mode');
List<String> leadSources = lb.CustomFilterBase.stringValues(parameters, 'leadSources');

A filter that declares neither a field nor a list of values is treated as broken and does not appear in the builder.

If a choice stops being available — a picklist value is deactivated, or you shorten your own list — an audience still configured with it fails at the start of its run, naming the value and the setting. It is not silently ignored, because a filter that matches nothing looks the same as one that is working.

Parameters are matched by name, so you can add, remove, or reorder them later. Values stored for a parameter you removed are dropped, and a parameter you added takes its declared default until someone edits the filter.

lb.CustomFilterContext describes the invocation:

Property Use
objectName The object being filtered. Every id in resultIds is of this type.
audienceVersionId The audience version this run belongs to.
nodeId The filter node’s record id, useful for logging.
nodeLabel The name the node carries on the canvas — what getLabel returned, or whatever the user renamed it to.
isExclusion true when the node is set to Without. CAB inverts the result for you — do not invert it yourself.
isPreview true when producing a record count for the builder rather than running the audience.

isPreview is the one to branch on. Counts shown while someone edits an audience also call your filter, so a filter that reaches an external system should skip the call or return a cheaper approximation during a preview.

if (context.isPreview) {
return resultIds;
}
  1. Open the audience and select + on the node whose records you want to filter.
  2. Select Custom Filter.
  3. Choose your class under Filter. Its description appears beneath the picker.
  4. Fill in any settings the filter declares.
  5. Select Without to remove the records your filter returns instead of keeping them.
  6. Select Save.

The filter appears as a node on the canvas under the name your class returns from getLabel. Select the node’s name to rename it — reconfiguring the filter afterwards leaves your name in place. Double-click the node to change the class or its settings.

A Custom Filter combines with its sibling filters exactly like any other related-records filter, including custom filter logic. Because the Apex decides on its own, no further filters can be added beneath it.

Custom Filters can also be used as part of a Filter Library entry, so a rule can be maintained once and reused across audiences.

Your class runs inside the audience run’s own transaction and shares its governor limits. Query against the supplied id set rather than looping, and keep the work proportional to a single batch.

Callouts are allowed, but they count against the transaction’s limits and follow the standard Salesforce rule that a callout cannot follow uncommitted DML.

If your class throws, the audience run fails with an error naming the class. If the class has been renamed or deleted since the audience was saved, the run fails immediately with an explanatory message rather than partway through.

CAB Settings › Custom Filters explains the feature, links to this page, lists every Custom Filter found in your organization, and offers a copy-and-paste example implementation.

Users need access to your Apex class in addition to their Campaign Audience Builder permissions. Because the class lives in your organization rather than in the package, its access is not granted by the packaged permission sets — add it to a permission set yourself, or users hit a class access error when the audience runs.