Skip to main content
A custom screen is the full-screen sibling of a custom component: instead of embedding native UI inside a flow screen, the whole screen node is your React component, while the flow still owns navigation and persistent zones around it. The SDK exposes the registration API and types for this today, but the renderer does not yet invoke them.
Custom screens are not rendered in this build. FlowPilot.registerCustomScreen(...) accepts and stores the definition, but the renderer never looks it up or mounts the component. Treat the custom-screen types as a roadmap API. For native UI inside a flow today, use a custom component, which is fully rendered.
TODO: re-check whether the renderer resolves custom screens (a getScreen(...) call site in flowpilot-EXPO-SDK/src/rendering/) on each SDK update, and promote this page to “live” once it does.

The registration API (for forward compatibility)

The shape mirrors custom components, plus screen-level context. Registering now is harmless and keys your code to the eventual contract, but nothing renders yet.
import { FlowPilot } from '@flowpilotjs/react-native-sdk';
import { MyNativeScreen } from './MyNativeScreen';

FlowPilot.registerCustomScreen('native_paywall_screen', {
  inputs: {
    plan: 'string',
    is_trial: 'boolean',
  },
  outputs: {
    purchased: { description: 'User purchased', payload: { product_id: 'string' } },
    skipped: { description: 'User skipped' },
  },
  component: ({ props, context }) => {
    const plan = (props.inputs.plan as string) ?? 'monthly';
    return (
      <MyNativeScreen
        plan={plan}
        onPurchased={(id) => context.emit('purchased', { product_id: id })}
        onSkip={() => context.emit('skipped')}
      />
    );
  },
});

What a custom screen would receive

Custom screens use CustomScreenRenderProps, which extends the custom-component context with screen-level capabilities:
interface CustomScreenRenderProps {
  props: {
    inputs: Record<string, VariableValue>;
  };
  context: {
    // Inherited from the custom-component context
    containerSize: { width: number; height: number };
    containerConstraints: { /* min/max width/height, supportsIntrinsicSize */ };
    emit(eventName: string, payload?: Record<string, unknown>): void;

    // Screen-specific
    screenId: string;
    setZonesVisible(visible: boolean): void; // show/hide persistent nav bar, footer, overlay
  };
}
setZonesVisible(false) would let a custom screen take over the full surface by hiding the flow’s persistent zones; emit(...) would fire output events the editor wires to navigation, exactly like custom components.

Use a custom component instead, for now

Anything you would build as a custom screen can be built today as a custom component that fills the screen. Place it as the only component on an otherwise-empty screen node, bind its inputs, and wire its emitted events in the editor.
FlowPilot.registerCustomComponent('full_screen_paywall', {
  inputs: { plan: 'string' },
  outputs: { purchased: { payload: { product_id: 'string' } }, skipped: {} },
  component: ({ props, context }) => (
    <MyNativeScreen
      plan={(props.inputs.plan as string) ?? 'monthly'}
      onPurchased={(id) => context.emit('purchased', { product_id: id })}
      onSkip={() => context.emit('skipped')}
    />
  ),
});
See Custom components for the full, working pattern.

Common mistakes

  • Expecting registerCustomScreen to render. It stores the definition but nothing mounts it in this build. Use a custom component.
  • Building around setZonesVisible today. It is part of the not-yet-active custom-screen context. To control zones now, configure them in the editor.