Skip to main content
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.
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.

Common problems

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:
// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: ['react-native-reanimated/plugin'], // must be last
  };
};
npx expo start --clear
In a bare or dev-client workflow, run npx expo prebuild and rebuild the native app. See Installation.
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):
npx expo install \
  expo-file-system expo-font expo-haptics expo-image expo-linking expo-secure-store \
  react-native-reanimated react-native-safe-area-context react-native-svg \
  @react-native-community/slider lottie-react-native
A present that returns without showing anything almost always means FlowPilot had no flow to show, or the SDK was never set up. Work through these in order:
  1. The SDK is not configured. If FlowPilot.configure(...) never ran (or threw on a bad key and the throw was swallowed), every present resolves to an error outcome with code SDK_NOT_INITIALIZED. 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 and 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.
  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:
const session = await FlowPilot.resolveSession('onboarding');
return session
  ? <FlowPilotPresenter session={session} onComplete={handleComplete} />
  : <MyNativeOnboarding />;
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.
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.
  • 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.
  • 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.
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.
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). Unknown types are coerced to a benign placeholder so the rest of the screen still works.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.
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.
  • 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.
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 and Media preloading.
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.
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.
Fix: attach the new version to the placement, then clear the cache (or wait out the TTL).
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. There is no public API to set a custom user id in this build, so you cannot pin it yourself yet.Fix: test variant stickiness on a persistent device/simulator without resetting it between launches. Confirm expo-secure-store is installed so the id persists. See Variables and context and Reading experiment results.
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.

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.