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

# Custom components and actions

> Render your own React Native UI inside a flow, and extend the action chain with native handlers.

The 14 built-in components cover most flows, but sometimes you need native UI a flow cannot express, like a StoreKit paywall, a map, or a camera preview. Custom **components** let you render any React Native subtree inside a flow while the editor owns how it wires into the rest of the flow. Custom **actions** let a flow call into your native code.

## Custom components

A custom component is a renderer, not an action site. It receives typed inputs from the flow, renders your React Native UI, and emits named events. The editor decides what each event does (navigate, set a variable, close the flow, and so on).

### Register at startup

Register before any flow that uses the component is presented, normally right after `configure(...)`.

```tsx theme={null}
import { FlowPilot } from '@flowpilotjs/react-native-sdk';
import { MyPaywall } from './MyPaywall';

FlowPilot.registerCustomComponent('my_paywall', {
  // Declared input schema (validated by the editor when authoring a flow)
  inputs: {
    user_name: 'string',
    is_premium: 'boolean',
    show_annual: 'boolean',
  },
  // Declared output events the component can emit
  outputs: {
    purchase: {
      description: 'User completed a purchase',
      payload: { product_id: 'string', price: 'number' },
    },
    dismiss: {
      description: 'User dismissed the paywall',
    },
  },
  // Your React component
  component: ({ props, context }) => {
    const userName = (props.inputs.user_name as string) ?? '';
    const isPremium = (props.inputs.is_premium as boolean) ?? false;
    const showAnnual = (props.inputs.show_annual as boolean) ?? true;

    return (
      <MyPaywall
        userName={userName}
        isPremium={isPremium}
        showAnnual={showAnnual}
        onPurchase={(productId, price) =>
          context.emit('purchase', { product_id: productId, price })
        }
        onDismiss={() => context.emit('dismiss')}
      />
    );
  },
});
```

### Registration shape

```ts theme={null}
FlowPilot.registerCustomComponent(key: string, definition: {
  version?: number;                                  // default 1
  inputs?: Record<string, 'string' | 'number' | 'boolean' | 'list'>;
  outputs?: Record<string, {
    description?: string;
    payload?: Record<string, 'string' | 'number' | 'boolean' | 'list'>;
  }>;
  component: React.ComponentType<CustomComponentRenderProps>;
});
```

### What your component receives

Your `component` is rendered with `CustomComponentRenderProps`:

```ts theme={null}
interface CustomComponentRenderProps {
  props: {
    inputs: Record<string, VariableValue>; // resolved input values from the flow
  };
  context: {
    containerSize: { width: number; height: number };
    containerConstraints: {
      minWidth?: number;
      maxWidth?: number;
      minHeight?: number;
      maxHeight?: number;
      supportsIntrinsicSize: boolean;
    };
    emit(eventName: string, payload?: Record<string, unknown>): void;
  };
}
```

* **`props.inputs`** holds the resolved input values. Each is a `VariableValue` (string, number, boolean, or array), so cast to the type you declared.
* **`context.emit(eventName, payload?)`** fires one of your declared output events. The editor's interaction for that event runs its action chain. Output payloads are validated against the `outputs` schema before reaching the chain.
* **`context.containerSize` / `containerConstraints`** describe the space the flow gave your component, for laying out responsively.

### How the flow references it

The editor produces flow JSON that references the component by key and binds each input to a variable (`{ "bind": "user.name" }`) or a constant (`{ "value": true }`), and wires each emitted event to an action chain:

```json theme={null}
{
  "id": "paywall_1",
  "type": "custom",
  "props": {
    "componentType": "my_paywall",
    "inputs": {
      "user_name": { "bind": "user.name" },
      "is_premium": { "bind": "user.is_premium" },
      "show_annual": { "value": true }
    }
  },
  "interactions": [
    {
      "id": "on_purchase",
      "event": "purchase",
      "actions": [
        { "kind": "trackEvent", "eventKey": "paywall_purchase" },
        { "kind": "navigate", "targetNodeId": "success_screen" }
      ]
    },
    {
      "id": "on_dismiss",
      "event": "dismiss",
      "actions": [{ "kind": "closeFlow" }]
    }
  ]
}
```

### Unregister

To remove a registered component (for example on logout), call:

```ts theme={null}
FlowPilot.unregisterCustomComponent('my_paywall');         // version 1
FlowPilot.unregisterCustomComponent('my_paywall', 2);      // a specific version
```

### Principles

1. Custom components are **renderers**, not action sites. Emit events with `context.emit(...)` and let the editor decide what happens next.
2. Inputs are validated against the declared `inputs` schema by the editor when authoring a flow.
3. Output payloads are schema-validated against the declared `outputs` schema before being passed to action chains.

## Custom actions

If a flow needs to call native code that the built-in action kinds do not cover (open a native paywall, request a permission, start a purchase), register a custom action handler. The editor then references it by key with a `custom` action.

```ts theme={null}
import { FlowPilot, type ActionContext } from '@flowpilotjs/react-native-sdk';

FlowPilot.registerCustomAction('open_native_paywall', async (params, context: ActionContext) => {
  await NativeBilling.presentPaywall();
  // Optionally read/write flow variables via context.variableStore
});
```

The handler signature:

```ts theme={null}
type CustomActionHandler = (
  params: Record<string, unknown> | undefined,
  context: ActionContext,
) => Promise<void>;
```

`params` carries any `params` object the editor attached to the action. `context` gives you the flow's `variableStore`, `navigationController`, the current `flowId` / `screenId`, and `isFlowClosed`. The editor references the action like this:

```json theme={null}
{ "kind": "custom", "actionKey": "open_native_paywall", "params": { "plan": "annual" } }
```

Remove a handler with `FlowPilot.unregisterCustomAction('open_native_paywall')`.

<Note>
  Action chains run with timeout protection (about 5s per chain, 2s per async action). A custom action that hangs is abandoned so it cannot freeze the flow, and an unregistered `custom` action is skipped with a warning, not an error. Keep handlers fast, or kick off long work and return.
</Note>

## Common mistakes

* **Registering after presenting.** Register custom components and actions before the flow that uses them is presented, or the component shows a placeholder and the action is skipped.
* **Key mismatch.** The `key` you register must match the `componentType` / `actionKey` in the flow exactly (case-sensitive).
* **Version mismatch.** Components match on `key + version`. If the flow uses version `2`, register version `2`.
* **Treating a custom component as an action site.** Do not call navigation directly from inside the component. Emit an event and let the editor's interaction handle it, so flow logic stays in the editor.
* **Long-running custom actions.** A handler that exceeds the per-action timeout is abandoned. Start the work and return, or report progress through a custom component instead.

## Troubleshooting

* **Component renders a blank placeholder.** It was not registered, registered after presenting, or the key/version does not match the flow. Register the exact key and version at startup. Enable `logLevel: 'debug'` to see the lookup miss.
* **An emitted event does nothing.** The editor has no interaction wired for that event name, or the event name does not match the declared output. Confirm the `outputs` key matches what you `emit(...)`.
* **A custom action never runs.** The handler key does not match the flow's `actionKey`, or it was registered too late. Register before presenting, and check the warning in debug logs.

## Related pages

* [Custom screens](/expo-sdk/custom-screens)
* [Custom components (editor)](/editor/custom-components)
* [Variables and context](/expo-sdk/variables-and-context)
* [Actions reference](/reference/actions-reference)
* [API reference](/expo-sdk/api-reference)
