Skip to main content
When a flow ends, FlowPilot tells you how it ended. The imperative and declarative present paths both hand you a FlowPresentationResult with an outcome. For richer lifecycle observation (screen changes, state transitions), a session exposes a delegate.

FlowPresentationResult

Both presentPlacement (resolved value) and <FlowPilotPresenter onComplete={...} /> give you this shape:
interface FlowPresentationResult {
  outcome: 'completed' | 'dismissed' | 'error';
  error?: Error; // populated when outcome === 'error'
}
OutcomeMeaning
completedThe user reached the end of the flow, or a closeFlow action completed it.
dismissedThe user closed the flow before finishing (for example a back-out or a dismiss()).
errorFlowPilot had nothing to present, or the presentation failed before a screen showed. error holds the cause.
const result = await FlowPilot.presentPlacement('paywall_main');

switch (result.outcome) {
  case 'completed':
    grantAccessOrContinue();
    break;
  case 'dismissed':
    // Respect the user's choice to skip.
    break;
  case 'error':
    console.warn('FlowPilot error:', result.error?.message);
    showYourOwnUI();
    break;
}
Unlike the iOS SDK’s FlowResult, the Expo FlowPresentationResult carries only outcome and error. It does not bundle finalVariables, screensVisited, durationMs, or experimentAssignments. Read final variable state from the session (below), and observe experiment assignment through the analytics callback (experiment_exposure).

Reading final variable state

On the declarative path you hold the FlowSession, so you can read its variable store after completion. The session’s variableStore is public.
<FlowPilotPresenter
  session={session}
  onComplete={(result) => {
    if (result.outcome === 'completed' && session) {
      const vars = session.variableStore.getAll();
      // e.g. { 'selected_plan': 'annual', 'opted_in': true }
      console.log('final variables', vars);
    }
    setSession(null);
  }}
/>
variableStore.getAll() returns a Record<string, VariableValue> of the flow’s variables at the moment the flow ended. See Variables and context for the value types and how to read individual keys.

Observing the flow lifecycle

A FlowSession accepts a delegate for finer-grained events than the final outcome. Set it with session.setDelegate(...) before session.start().
type FlowSessionDelegate = {
  onScreenChange(
    screen: ScreenNode,
    transition: TransitionConfig,
    direction: 'forward' | 'back',
  ): void;
  onFlowComplete(outcome: 'completed' | 'dismissed' | 'error'): void;
  onStateChange(state: FlowSessionState): void;
};

type FlowSessionState = 'idle' | 'loading' | 'active' | 'completed' | 'error';
const session = await FlowPilot.createSession('onboarding');

session.setDelegate({
  onScreenChange: (screen, _t, direction) => {
    console.log(`Now on ${screen.name} (${direction})`);
  },
  onFlowComplete: (outcome) => {
    console.log('Flow finished:', outcome);
  },
  onStateChange: (state) => {
    console.log('State:', state);
  },
});

session.start();
<FlowPilotPresenter /> installs its own internal delegate to drive the modal and call onComplete. If you call setDelegate(...) yourself and render the same session through FlowPilotPresenter, the presenter’s delegate wins, so prefer the presenter’s onComplete / onError props when using the component. Use a custom delegate when you drive presentation yourself with the lower-level FlowPresenter.
TODO: verify delegate precedence against flowpilot-EXPO-SDK/src/rendering/FlowPresenter.tsx.

Useful session accessors

A live FlowSession exposes read-only state you can inspect:
session.state          // 'idle' | 'loading' | 'active' | 'completed' | 'error'
session.currentScreen  // the active ScreenNode, or null
session.isFlowClosed   // true once the flow has ended
session.placementId    // the placement key this session was resolved for
session.flow           // the resolved FlowDefinition

Common mistakes

  • Expecting finalVariables on the result object. It is not there in this build. Read session.variableStore.getAll() instead.
  • Reading the session after you cleared it. If you setSession(null) before reading variableStore, the reference may be gone. Read variables first, then clear.
  • Treating dismissed as failure. A dismissal is a normal user choice, not an error. Only error indicates something went wrong.