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

# Results and outcomes

> Every flow presentation resolves to a FlowResult describing how the flow ended and what state it produced.

Every way you present a flow ends with a `FlowResult`. It tells you how the flow finished, which screens the user saw, what variables the flow captured, and which experiment variant (if any) the user got. Read it to branch your app logic, persist a captured choice, or attribute a conversion.

You get a `FlowResult` from:

* the awaited return of [`presentPlacement(_:from:options:)`](/ios-sdk/presenting-placements),
* the `completion` of the callback overload,
* the `fallback` overload's return value,
* the `onResult` closure of [`.flowPresenter`](/ios-sdk/swiftui),
* `await session.waitForCompletion()` when embedding a `FlowPresenterView`.

## The FlowResult shape

```swift theme={null}
public struct FlowResult: Sendable {
    public let outcome: FlowOutcome
    public let finalVariables: [String: VariableValue]
    public let screensVisited: [String]
    public let durationMs: Int
    public let experimentAssignments: [String: String]
    public let error: FlowPilotError?
}

public enum FlowOutcome: String, Sendable {
    case completed
    case dismissed
    case error
}
```

| Field                   | Type                      | What it tells you                                                                     |
| ----------------------- | ------------------------- | ------------------------------------------------------------------------------------- |
| `outcome`               | `FlowOutcome`             | How the flow ended: `.completed`, `.dismissed`, or `.error`. Branch on this first.    |
| `finalVariables`        | `[String: VariableValue]` | Every flow variable and its value at the end. Read user choices captured by the flow. |
| `screensVisited`        | `[String]`                | Screen node ids in the order the user saw them.                                       |
| `durationMs`            | `Int`                     | How long the flow was on screen, in milliseconds.                                     |
| `experimentAssignments` | `[String: String]`        | Experiment id to assigned variant id, for any experiment the user was bucketed into.  |
| `error`                 | `FlowPilotError?`         | Set when `outcome == .error`. `nil` otherwise.                                        |

### Outcomes

* **`.completed`** means the flow reached its end (a `closeFlow` action or the final screen). This is the success case.
* **`.dismissed`** means the user left before finishing (swiped away, a back action off the first screen). This is not a completion; do not treat it as one.
* **`.error`** means FlowPilot had nothing presentable, or the presentation failed. Read `error` for the reason. The never-throws [`fallback`](/ios-sdk/presenting-placements) overload returns this when it falls back to your native UI.

## Reading captured variables

`finalVariables` maps each variable key to a `VariableValue`, an enum with typed accessors. Use the accessor for the type you expect; it returns `nil` if the value is absent or the wrong type.

| Accessor            | Returns     |
| ------------------- | ----------- |
| `.stringValue`      | `String?`   |
| `.numberValue`      | `Double?`   |
| `.intValue`         | `Int?`      |
| `.boolValue`        | `Bool?`     |
| `.stringListValue`  | `[String]?` |
| `.numberListValue`  | `[Double]?` |
| `.booleanListValue` | `[Bool]?`   |

```swift theme={null}
func handle(_ result: FlowResult) {
    switch result.outcome {
    case .completed:
        // Read a choice the flow captured into a variable.
        if let plan = result.finalVariables["selected_plan"]?.stringValue {
            applySelectedPlan(plan)
        }
        // Attribute the experiment variant the user was shown.
        if let variant = result.experimentAssignments["paywall_pricing_test"] {
            analytics.setProperty("paywall_variant", variant)
        }

    case .dismissed:
        // User closed the flow early. Not a completion.
        analytics.track("onboarding_abandoned", ["screens": result.screensVisited.count])

    case .error:
        // Nothing to present, or presentation failed.
        log("Flow error: \(result.error?.message ?? "unknown")")
    }
}
```

## Completion callbacks on the SDK

For analytics and errors that span flows, set closures on the SDK once. These are the confirmed, public callbacks:

```swift theme={null}
// Forward every automatic analytics event to your own analytics.
FlowPilot.shared?.setAnalyticsCallback { event in
    analytics.track(event.eventName, event.properties)
}

// Receive SDK-level errors.
FlowPilot.shared?.setErrorCallback { error in
    log("FlowPilot error: \(error)")
}
```

See [Analytics integration](/ios-sdk/analytics) for the `AnalyticsEvent` shape and [Error handling](/ios-sdk/error-handling) for error codes.

There is **no global flow-completion or flow-dismissal callback** to register. Learn how a specific presentation ended from its `FlowResult` (returned, delivered to `completion`, or passed to `onResult`), not from a global callback.

<Warning>
  A `FlowPilotDelegate` protocol (`flowPilotDidComplete` / `flowPilotDidDismiss` / `flowPilotDidFail`) is declared `public`, but `FlowPilot` exposes no property to set a delegate and never calls these methods. It is not usable today. Use the `FlowResult` and the `setAnalyticsCallback` / `setErrorCallback` closures above.
</Warning>

## Common mistakes

* **Treating `.dismissed` as success.** A dismissed flow did not complete. Only `.completed` means the user reached the end.
* **Assuming a variable exists.** `finalVariables["key"]` is optional, and so is the typed accessor. Always use optional binding or a default; never force-unwrap.
* **Ignoring `error` on `.error`.** When `outcome == .error`, `error` carries the reason (for example `flow_not_found`, `timeout`). Log or branch on it.
* **Reading the wrong variable type.** `.boolValue` on a string variable returns `nil` (unless the string coerces). Match the accessor to the variable's declared type.

## Related pages

* [Presenting placements (UIKit)](/ios-sdk/presenting-placements)
* [SwiftUI integration](/ios-sdk/swiftui)
* [Variables and SDK context](/ios-sdk/variables-and-context)
* [Analytics integration](/ios-sdk/analytics)
* [Error handling](/ios-sdk/error-handling)
