Skip to main content
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:),
  • the completion of the callback overload,
  • the fallback overload’s return value,
  • the onResult closure of .flowPresenter,
  • await session.waitForCompletion() when embedding a FlowPresenterView.

The FlowResult shape

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
}
FieldTypeWhat it tells you
outcomeFlowOutcomeHow 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.
durationMsIntHow 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.
errorFlowPilotError?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 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.
AccessorReturns
.stringValueString?
.numberValueDouble?
.intValueInt?
.boolValueBool?
.stringListValue[String]?
.numberListValue[Double]?
.booleanListValue[Bool]?
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:
// 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 for the AnalyticsEvent shape and 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.
A FlowPilotDelegate protocol (flowPilotDidComplete / flowPilotDidDismiss / flowPilotDidFail) is declared public in Sources/FlowPilotSDK/Core/FlowPilot.swift, 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. Re-verify against Core/FlowPilot.swift before relying on the delegate.

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.