> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getflowpilot.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Variables and SDK context

> Feed user data into a flow as SDK context, read it back as flow variables, and target audiences on the same attributes.

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.

```swift theme={null}
public enum VariableValue: Sendable, Equatable, Codable {
    case string(String)
    case number(Double)
    case boolean(Bool)
    case stringList([String])
    case numberList([Double])
    case booleanList([Bool])
}
```

| Accessor            | Returns     | Notes                                                |
| ------------------- | ----------- | ---------------------------------------------------- |
| `.stringValue`      | `String?`   |                                                      |
| `.numberValue`      | `Double?`   | Coerces a numeric string (for example `"9.99"`).     |
| `.intValue`         | `Int?`      | Coerces a numeric string; truncates a `Double`.      |
| `.boolValue`        | `Bool?`     | 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).

```swift theme={null}
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]`.

```swift theme={null}
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:

```json theme={null}
{ "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.

```swift theme={null}
// 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:

```swift theme={null}
FlowPilot.shared?.updateContext([
    "user.isPremium": true,
    "plan": "pro_monthly"
])
```

<Warning>
  `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.
</Warning>

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:

```swift theme={null}
// 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"]
)
```

<Note>
  `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.
</Note>

## 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.

```swift theme={null}
// 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`](/ios-sdk/results-and-outcomes), 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.

<Note>
  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](/dashboard/audience-targeting) for how filters evaluate properties.
</Note>

## Example: one attribute, three uses

Configure the SDK once:

```swift theme={null}
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](/editor/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.

<Warning>
  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 in the current SDK.
</Warning>

## 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.

## Related pages

* [Audience targeting](/dashboard/audience-targeting)
* [Experiments](/dashboard/experiments)
* [Variables (editor)](/editor/variables)
* [Conversions and revenue](/ios-sdk/conversions)
* [Results and outcomes](/ios-sdk/results-and-outcomes)
