Skip to main content
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

OperatorMeaningNotes
equalsValue matches exactlyCompared as strings. Case-sensitive.
not_equalsValue does not match
containsAttribute string contains the valueCase-insensitive.
gtGreater thanNumeric comparison.
ltLess thanNumeric comparison.
gteGreater than or equalNumeric comparison.
lteLess than or equalNumeric comparison.

Also supported by the engine

OperatorMeaningNotes
inAttribute is in a listValue is an array.
not_inAttribute is not in a listValue is an array.
existsThe attribute is presentValue is ignored.
not_existsThe attribute is absentValue is ignored.
starts_withString starts with the valueCase-insensitive.
ends_withString ends with the valueCase-insensitive.
regexGlob-style pattern matchOnly * is a wildcard. Not full regular expressions.
version_gtVersion greater thanDotted version compare (3.2.0).
version_ltVersion less thanDotted version compare.
version_gteVersion greater than or equalDotted version compare.
version_lteVersion less than or equalDotted 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:
PropertyLabelWhere it comes from
app.versionApp VersionReported automatically by the iOS SDK (from the app bundle).
app.buildApp BuildReported automatically by the iOS SDK (from the app bundle).
planPlanYou must set it (pass it in the SDK context).
user_typeUser TypeYou must set it.
sessions_countSessions CountYou must set it.
is_verifiedVerified UserYou 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.