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

# Troubleshooting

> Symptoms, causes, and fixes for the most common FlowPilot Expo SDK integration problems.

When a flow does not show up or does not look right, the cause is usually one of a handful of things: the SDK is not configured, the placement has no flow for this user, a peer dependency or the Reanimated plugin is missing, or an asset was not uploaded. This page walks the common symptoms. For the error type and codes, see [Error handling](/expo-sdk/error-handling).

<Tip>
  Before anything else, raise the log level. Set `logLevel: 'debug'` (or `'verbose'`) in `FlowPilot.configure(...)`. The SDK logs each resolve, the delivery source (`cache`, `network`, `stale_cache`, or `bundled_default`), and why a flow was rejected. Most issues below are obvious once logging is on. See [Configuration](/expo-sdk/configuration).
</Tip>

## Common problems

<AccordionGroup>
  <Accordion title="The app throws a Reanimated error on startup">
    Errors like `[Reanimated] Couldn't determine the version of the native part of Reanimated` mean the Babel plugin is missing or the app was not rebuilt.

    **Fix:** add `react-native-reanimated/plugin` as the **last** plugin in `babel.config.js`, then do a clean start:

    ```js theme={null}
    // babel.config.js
    module.exports = function (api) {
      api.cache(true);
      return {
        presets: ['babel-preset-expo'],
        plugins: ['react-native-reanimated/plugin'], // must be last
      };
    };
    ```

    ```bash theme={null}
    npx expo start --clear
    ```

    In a bare or dev-client workflow, run `npx expo prebuild` and rebuild the native app. See [Installation](/expo-sdk/installation).
  </Accordion>

  <Accordion title="`Unable to resolve module expo-...`">
    A peer dependency is not installed. The SDK relies on a set of Expo modules you install in your app.

    **Fix:** install them all with `npx expo install` (not plain `npm install`, so versions match your Expo SDK):

    ```bash theme={null}
    npx expo install \
      expo-file-system expo-font expo-haptics expo-image expo-linear-gradient expo-linking expo-secure-store expo-store-review \
      react-native-reanimated react-native-safe-area-context react-native-svg \
      @react-native-community/slider lottie-react-native
    ```
  </Accordion>

  <Accordion title="Nothing shows when I render a placement">
    A presenter that renders nothing almost always means FlowPilot had no flow to show, or the SDK was never set up. Work through these in order:

    0. **You called `presentPlacement`.** It is deprecated on Expo and renders nothing. Resolve a session with `resolveSession` / `createSession` and render `<FlowPilotPresenter session={...} />`. See [Presenting placements](/expo-sdk/presenting-placements).
    1. **The SDK is not configured.** If `FlowPilot.configure(...)` never ran (or threw on a bad key and the throw was swallowed), every resolve fails with code `SDK_NOT_INITIALIZED` (`resolveSession` returns `null`, `createSession` throws). Check the debug log for the configure line.
    2. **Wrong App ID.** The flow, placement, and key must all belong to the same app. A mismatched `appId` resolves against the wrong app and finds nothing.
    3. **The placement has no flow for this user.** The placement may be paused, have no default flow attached, or exclude this user by audience targeting. The resolve returns `FLOW_NOT_FOUND`. This is expected, not a bug. Check the placement in the dashboard. See [Placements](/dashboard/placements) and [Audience targeting](/dashboard/audience-targeting).
    4. **The flow is a draft, or is not attached.** Only a published flow attached to the placement is served. Publishing alone does not attach it. See [Publishing](/editor/publishing).
    5. **The platform excludes this app.** A placement gates which platforms it serves.
    6. **Offline with a cold cache.** With no network, no cached flow, and no bundled default, there is nothing to present.

    **Fix:** turn on `'debug'` logging and read which step fails. For anything user-facing, use the resilient path so the user always sees something:

    ```tsx theme={null}
    const session = await FlowPilot.resolveSession('onboarding');
    return session
      ? <FlowPilotPresenter session={session} onComplete={handleComplete} />
      : <MyNativeOnboarding />;
    ```
  </Accordion>

  <Accordion title="`configure` throws, or every present is SDK_NOT_INITIALIZED">
    `FlowPilot.configure(...)` validates synchronously and throws on a bad input:

    * `Invalid API key. Must start with "fp_".` The `apiKey` is wrong or unset.
    * `App ID is required.` The `appId` is empty.

    If you wrap `configure` in an empty `catch`, the SDK stays unconfigured and every later present throws `SDK_NOT_INITIALIZED`.

    **Fix:** call `configure` exactly once, as early as possible, with a valid key and app id, and let any thrown error surface during development. See [Configuration](/expo-sdk/configuration).
  </Accordion>

  <Accordion title="`TIMEOUT` or very slow first present on poor networks">
    The live resolve did not finish within `resolveTimeout` (default 4s) and there was nothing cached or bundled to fall back to.

    **Fixes, in order of impact:**

    * **Ship a bundled default flow** so the first present always has something offline. See [Offline and bundled flows](/expo-sdk/offline-bundled-flows).
    * **Prefetch at launch** so the placement is warm before the user reaches it: `prefetchOnLaunch: ['onboarding']`, or `await FlowPilot.prefetch(['onboarding'])` (add `{ warmMedia: true }` for first-screen images). See [Prefetching](/expo-sdk/prefetching).
    * **Use `resolveSession`** so a timeout falls through to cache, then bundled, then your own UI instead of an error.
    * **Raise `resolveTimeout`** only if your audience is on consistently slow networks. It bounds the whole resolve; the minimum is `0.5`.

    See [Caching](/expo-sdk/caching).
  </Accordion>

  <Accordion title="A component renders blank (`CUSTOM_COMPONENT_NOT_FOUND`)">
    The flow references a custom component the SDK could not find, so it shows a placeholder. Causes:

    * **Not registered.** You never called `registerCustomComponent`, or registered it after presenting. Register before you present.
    * **Key mismatch.** The `key` you registered does not match the component key in the flow exactly (case-sensitive).
    * **Version mismatch.** The SDK matches on `key + version`. If the flow uses version `2` and you registered version `1`, it will not match.

    **Fix:** register the exact key and version the flow uses, at startup. See [Custom components](/expo-sdk/custom-components).
  </Accordion>

  <Accordion title="A component shows a 'Not supported' / unknown placeholder">
    The flow uses a component type this SDK build does not recognize (a newer schema shipped a type an older SDK has never heard of, or an editor-gated type that the SDK does not render yet). In development builds (`__DEV__`) the unknown type renders a visible placeholder; in production builds it renders nothing (so the rest of the screen still works), rather than crashing.

    **Fix:** ask whoever authored the flow to use components this SDK version renders, or update the app's SDK version. The 14 rendered component kinds are listed in the [Component reference](/reference/component-reference).
  </Accordion>

  <Accordion title="Fonts look wrong or fall back to the system font">
    A flow renders with the wrong typeface when the SDK cannot resolve the font:

    * **The font was not uploaded.** Custom fonts must be uploaded to the workspace so the resolve response includes them. See [Custom fonts](/dashboard/custom-fonts).
    * **Family, weight, or style mismatch.** The flow references a variant that does not exist among the uploaded fonts, so it falls back to the system font.
    * **Offline with no bundled font assets.** A bundled (offline) flow only has its fonts when you ship them in the asset bundle. Without a `fonts` entry in the `.flowassets` manifest, an offline render uses the system font.
    * **`expo-font` missing.** Without it, custom fonts cannot register. Confirm it is installed.

    **Fix:** upload the exact families and weights the flow uses, and for offline support include the fonts in the bundled asset manifest. See [Offline and bundled flows](/expo-sdk/offline-bundled-flows).
  </Accordion>

  <Accordion title="Images do not load when offline">
    Online, images stream from the CDN and get cached. Offline, an image only appears if its bytes are already cached or shipped with the flow.

    * **No bundled assets.** A bundled default flow with no image assets shows blank images offline. Ship the images in the `.flowassets` bundle so the SDK seeds the image cache before first paint.
    * **Cold cache.** If the user has never seen the flow online, nothing is cached. Preload or prefetch while the network is available.

    **Fix:** for guaranteed offline rendering, register the bundled flow with its assets. See [Offline and bundled flows](/expo-sdk/offline-bundled-flows) and [Media preloading](/expo-sdk/media-preloading).
  </Accordion>

  <Accordion title="The flow renders under the status bar or home indicator">
    The renderer reads safe-area insets from `react-native-safe-area-context`. Without a provider, `useSafeAreaInsets()` returns zeros.

    **Fix:** wrap your app in `<SafeAreaProvider>` and pass `safeAreaInsets={useSafeAreaInsets()}` to `<FlowPilotPresenter />`. See [Installation](/expo-sdk/installation).
  </Accordion>

  <Accordion title="I republished the flow but the app still shows the old version">
    Two things can keep an old flow on screen:

    * **Cache TTL.** The SDK serves a fresh cached flow without hitting the network until it goes stale (the resolve response's `cacheTtlSeconds`, or the environment default). Call `FlowPilot.clearCache()` to force a re-resolve during testing.
    * **The placement still points at the old version.** Publishing a new version does not automatically repoint a placement. Until you attach the new version, the resolve keeps returning the old one. This is the most common cause. See [Publishing](/editor/publishing).

    **Fix:** attach the new version to the placement, then clear the cache (or wait out the TTL).
  </Accordion>

  <Accordion title="An experiment user keeps switching variants">
    Variant assignment is deterministic and sticky: the backend buckets a user by their per-install user id, so the same user gets the same variant every time. If a user keeps flipping variants, their **user id is not stable**.

    The SDK generates a stable per-install user id and persists it with `expo-secure-store`, so it normally survives restarts. Switching usually means that id is being reset: a reinstall, cleared secure store, or testing across multiple devices. To pin a stable identity yourself, call `FlowPilot.identify(yourUserId)` after `configure(...)` so bucketing keys off your account id instead of the per-install id.

    **Fix:** test variant stickiness on a persistent device/simulator without resetting it between launches, or call `FlowPilot.identify(...)` with a stable account id. Confirm `expo-secure-store` is installed so the anonymous id persists. See [Variables and context](/expo-sdk/variables-and-context) and [Reading experiment results](/dashboard/reading-results).
  </Accordion>

  <Accordion title="Events not appearing in the dashboard">
    Events are batched (10 per request or every 30s) and persisted to disk, so they may appear slightly later than they fired.

    * **Force-quit before the flush timer.** Queued events persist to disk and replay on next launch; they are not lost.
    * **Network error after retries.** A batch is retried 3 times, then dropped (visible as a `Logger.warn` line).
    * **Missing `expo-file-system`.** On-disk persistence needs it. Confirm it is installed.

    **Fix:** set `logLevel: 'debug'` to see batch flushes. Background the app to force an immediate flush during testing. See [Analytics](/expo-sdk/analytics).
  </Accordion>
</AccordionGroup>

## When you cannot reproduce it

* **Test the resilient path.** Turn on Airplane Mode and present. With `resolveSession` (or the presenter `fallback`) and a bundled flow, the user should still see something. A blank screen means a missing bundled default or its assets.
* **Confirm the delivery source.** The `delivery_source` on analytics events (and the debug logs) tells you whether a render came from the network, cache, stale cache, or bundled default. A surprising source often explains a surprising flow.
* **Check the dashboard first.** Most "nothing shows" cases are a paused placement, an unattached flow, or a targeting rule, not an SDK bug.

## Related pages

* [Error handling](/expo-sdk/error-handling)
* [Configuration](/expo-sdk/configuration)
* [Offline and bundled flows](/expo-sdk/offline-bundled-flows)
* [Caching](/expo-sdk/caching)
* [Prefetching](/expo-sdk/prefetching)
* [Installation](/expo-sdk/installation)
* [API reference](/expo-sdk/api-reference)
