Skip to main content
Your app knows things a flow wants: the user id, whether they are premium, their country, the plan they are on. You pass those values into the SDK as context, and flows read them as sdk-sourced variables. The same attributes also drive audience targeting at resolve time, so the data you send does triple duty: content, in-flow conditions, and who sees the flow at all.

The VariableValue type

Every variable value the SDK hands you is a VariableValue, a public enum with typed accessors. Use the accessor for the type you expect; it returns nil if the value is absent or the wrong type.
public enum VariableValue: Sendable, Equatable, Codable {
    case string(String)
    case number(Double)
    case boolean(Bool)
    case stringList([String])
    case numberList([Double])
    case booleanList([Bool])
}
AccessorReturnsNotes
.stringValueString?
.numberValueDouble?Coerces a numeric string (for example "9.99").
.intValueInt?Coerces a numeric string; truncates a Double.
.boolValueBool?Coerces "true"/"1"/"yes" and non-zero numbers.
.stringListValue[String]?
.numberListValue[Double]?
.booleanListValue[Bool]?
It also exposes isEmpty, the type-check flags isString / isNumber / isBoolean / isList, typeName, and displayString (the same string the SDK uses when interpolating {{var}} into text).
let value: VariableValue = .number(9.99)
value.numberValue   // 9.99
value.stringValue   // nil
value.displayString // "9.99"

Setting context at launch

Pass a context dictionary when you configure the SDK. SDKContext is a public typealias for [String: Any].
FlowPilot.configure(
    FlowPilotConfiguration(
        apiKey: "fp_live_...",
        appId: "your-app-id",
        context: [
            "user.id": currentUserId,
            "user.isPremium": false,
            "country": "US"
        ]
    )
)
Each key is matched against any flow variable whose source is sdk. On the dashboard a flow variable defined as an App Parameter carries a source like:
{ "kind": "sdk", "path": "user.isPremium" }
At runtime the SDK looks up that path as a literal, flat key in your context dictionary. So a variable with path: "user.isPremium" reads context["user.isPremium"]. The dot is part of the key, not a nested-object lookup. Send flat keys with dots in them, not nested dictionaries.
// Correct: flat key with a dot.
context: ["user.isPremium": true]

// Wrong: the SDK will not walk into nested objects for variable resolution.
context: ["user": ["isPremium": true]]
If a context key is missing, the variable falls back to its defaultValue (or the type default: "", 0, false, or an empty list).

Updating context later

updateContext merges new values into the SDK’s context dictionary:
FlowPilot.shared?.updateContext([
    "user.isPremium": true,
    "plan": "pro_monthly"
])
updateContext updates the context used by the next resolve and the next flow you present. It does not re-resolve and does not change a flow already on screen, because each session is seeded with a snapshot of the context at the moment it is created. Call updateContext before you present the flow whose values you want to change. Verified against Core/FlowPilot.swift and Variables/VariableStore.swift.
For values that only matter to a single presentation, pass additionalContext instead. It is merged on top of the launch context for that one session:
// UIKit
try await FlowPilot.shared?.presentPlacement(
    "onboarding",
    from: self,
    options: PresentationOptions(additionalContext: ["referrer": "push"])
)

// SwiftUI
let session = try await FlowPilot.shared?.createSession(
    placementKey: "onboarding",
    additionalContext: ["referrer": "push"]
)
additionalContext seeds the variables for that presentation only. It does not affect audience targeting, because the placement is resolved before the session is created. Targeting always uses the launch context plus any values added with updateContext. See the targeting section below.

Reading and writing variables during a flow

A live flow exposes its variables through the FlowSession you hold (from createSession or FlowPresenterView). All four methods are public.
// Read one variable.
let plan = session.getVariable("selected_plan")?.stringValue

// Read a snapshot of every variable.
let all: [String: VariableValue] = session.getAllVariables()

// Write a writable variable. Returns false if the variable is unknown
// or read-only.
let didApply = session.setVariable("selected_plan", value: .string("pro"))

// Reset all variables to their declared defaults, optionally merging
// fresh context. Useful when restarting a flow without re-resolving.
session.resetVariables(sdkContext: ["country": "GB"])
VariableValue conforms to the standard literal protocols, so you can write session.setVariable("count", value: 3) or .setVariable("active", value: true) directly. After a flow ends, read the final state from FlowResult.finalVariables, keyed by variable key.

The targeting connection

When the SDK resolves a placement, it sends your context (plus a few device and app attributes) as the attributes the dashboard audience filters match against. The SDK adds these automatically:
  • device.platform (always "ios")
  • device.os_version, device.model
  • app.version (CFBundleShortVersionString), app.build (CFBundleVersion)
Everything else comes from the context you sent. So an audience filter on user.isPremium == true matches the context key user.isPremium you provided at launch. The filter property name on the dashboard must match the context key you send, exactly.
Audience filters and flow-variable resolution can treat dotted property names differently (variable resolution uses a flat-key lookup; the resolver may treat a filter property as a nested path). When you target on a dotted property, confirm it matches as expected. See Audience targeting for how filters evaluate properties.

Example: one attribute, three uses

Configure the SDK once:
FlowPilot.configure(
    FlowPilotConfiguration(
        apiKey: "fp_live_...",
        appId: "your-app-id",
        context: [
            "user.id": currentUserId,
            "user.isPremium": false,
            "country": "US"
        ]
    )
)
On the dashboard, the same attributes are used:
  1. Content: a text component bound to {{user.id}} (a flow variable with source { kind: "sdk", path: "user.id" }) renders the id in the flow.
  2. In-flow logic: a Dynamic Value branches on the user.isPremium variable to show a different headline. See Dynamic values.
  3. Audience: a placement audience filter user.isPremium == false so only non-premium users get the paywall flow.

A stable user id is automatic

Experiment bucketing is sticky on the user id. The SDK generates one persistent id per install and stores it in the Keychain, so a given device keeps the same variant across launches without any work from you.
There is currently no public API to supply your own user id (for example, to align bucketing with your backend’s user id). The SDK manages the id internally. If you need custom user identity, treat it as not yet available and re-verify against Core/FlowPilot.swift and Core/SessionManager.swift in a future SDK version.

Common mistakes

  • Context key does not match the variable’s sdk path. The key you send must equal the variable’s path character for character, dots included. A mismatch leaves the variable on its default.
  • Nesting context and expecting dot-paths to resolve. Variable resolution is a flat-key lookup. Send ["user.id": ...], not ["user": ["id": ...]].
  • Calling updateContext to change a flow already on screen. It only affects the next resolve and the next session. Update before presenting.
  • Writing to a read-only variable and assuming it took. App Parameters (sdk-sourced variables) are created read-only in the editor. setVariable returns false and the write is ignored. Check the return value.
  • Sending non-JSON types. Context values are Any, but stick to String, numbers, Bool, and arrays of those so they convert cleanly.