An audience filter decides whether a given user qualifies for a placement (or an experiment). It is evaluated at resolve time against the attributes your app sends to the SDK. If the user matches, they get the flow; if not, the placement returns nothing.
Targeting is only as good as the attributes you send. The backend only sees what the SDK puts in the resolve request, so the contract between your dashboard filter and your app’s attributes has to line up exactly.
The filter shape
A filter has a match mode and a list of conditions:
{
"match": "all",
"conditions": [
{ "property": "plan", "operator": "equals", "value": "premium" },
{ "property": "sessions_count", "operator": "gte", "value": "5" }
]
}
match is all (every condition must be true, an AND) or any (at least one condition must be true, an OR). In the audience builder this is the “Show flow when ALL / ANY conditions match” toggle.
- Each condition is a
property (the attribute key), an operator, and a value.
No filter means everyone matches. A placement with no audience filter serves its flow to every user who passes the platform check.
Operators
The resolution engine supports the operators below. The dashboard’s audience builder currently lets you pick the first group (equality and numeric); the others are evaluated by the engine and are useful to know when you inspect a stored filter or manage placements through the API.
Selectable in the audience builder
| Operator | Meaning | Notes |
|---|
equals | Value matches exactly | Compared as strings. Case-sensitive. |
not_equals | Value does not match | |
contains | Attribute string contains the value | Case-insensitive. |
gt | Greater than | Numeric comparison. |
lt | Less than | Numeric comparison. |
gte | Greater than or equal | Numeric comparison. |
lte | Less than or equal | Numeric comparison. |
Also supported by the engine
| Operator | Meaning | Notes |
|---|
in | Attribute is in a list | Value is an array. |
not_in | Attribute is not in a list | Value is an array. |
exists | The attribute is present | Value is ignored. |
not_exists | The attribute is absent | Value is ignored. |
starts_with | String starts with the value | Case-insensitive. |
ends_with | String ends with the value | Case-insensitive. |
regex | Glob-style pattern match | Only * is a wildcard. Not full regular expressions. |
version_gt | Version greater than | Dotted version compare (3.2.0). |
version_lt | Version less than | Dotted version compare. |
version_gte | Version greater than or equal | Dotted version compare. |
version_lte | Version less than or equal | Dotted version compare. |
The version_* operators parse versions part by part (3.10.0 is greater than 3.9.0) and strip non-numeric suffixes, so 1.0.0-beta is treated as 1.0.0.
Properties (the attributes contract)
A condition’s property is a key the SDK includes in the resolve request’s attributes map. Only what the SDK sends is reachable. The dashboard’s audience builder suggests a curated set of properties:
| Property | Label | Where it comes from |
|---|
app.version | App Version | Reported automatically by the iOS SDK (from the app bundle). |
app.build | App Build | Reported automatically by the iOS SDK (from the app bundle). |
plan | Plan | You must set it (pass it in the SDK context). |
user_type | User Type | You must set it. |
sessions_count | Sessions Count | You must set it. |
is_verified | Verified User | You must set it. |
The first two are populated for you on iOS. The rest are custom: your app supplies them. You are not limited to this list, any key you put in the SDK context can be targeted, but the dashboard only suggests these.
Setting attributes from the iOS SDK
Attributes that you control are set through the SDK context, either at configuration time or at runtime:
// At configuration time
let config = FlowPilotConfiguration(
apiKey: "fp_live_xxx",
appId: "your-app-id",
context: [
"plan": "premium",
"sessions_count": 5
]
)
FlowPilot.configure(config)
// Or update later, before you present the placement
FlowPilot.shared?.updateContext([
"plan": "premium",
"sessions_count": 5
])
On iOS, the SDK also adds device and app attributes automatically (app.version, app.build, and device info). See Variables and SDK context for the full list and how context flows into a flow.
Dot-notation property names are read as nested paths. The engine treats a property like device.platform as a lookup into a nested object (attributes["device"]["platform"]). The iOS SDK reports app.version and app.build as flat keys whose name happens to contain a dot, not as nested objects, so a filter on those dotted names may not match as expected. For reliable targeting, prefer flat custom keys you set yourself (for example a plan or app_version key in your context). TODO: confirm intended behavior of app.version / app.build targeting with the platform team before relying on it.
Example: premium users with enough sessions
This filter targets users on the premium plan who have opened the app at least five times.
Dashboard side (the stored audience_filter):
{
"match": "all",
"conditions": [
{ "property": "plan", "operator": "equals", "value": "premium" },
{ "property": "sessions_count", "operator": "gte", "value": "5" }
]
}
In the builder you would set “Show flow when ALL conditions match”, then add Plan equals premium and Sessions Count greater or equal 5.
App side (the matching attributes your iOS app sends):
FlowPilot.shared?.updateContext([
"plan": "premium",
"sessions_count": 5
])
// Later, at the trigger point:
try await FlowPilot.shared?.presentPlacement("paywall", from: viewController)
The keys in your context (plan, sessions_count) must match the property names in the filter exactly. Numeric operators like gte work whether the value arrives as a number or a numeric string, so 5 and "5" both compare correctly.
Common mistakes
- Property name mismatch. The filter property and the attribute key must be identical. A filter on
plan will never match an attribute named subscription_plan.
- Expecting server-side enrichment. The backend only evaluates what the SDK sends. It does not look up user records or enrich attributes. If you do not send
is_verified, a filter on it cannot match (use exists carefully, an absent attribute is treated as not present).
- Treating
regex as full regex. The regex operator is glob-style: only * is a wildcard. A PCRE pattern like ^prem.*$ will not behave as you expect.
- Case assumptions.
equals is case-sensitive, but contains, starts_with, and ends_with are case-insensitive. Normalize values you care about.
- Dotted property names. See the warning above. Use flat custom keys for targeting you depend on.
Related pages