Rheo documentation
Developer Guide

Headless external surfaces

Render a host-owned React Native component as an External Surface Node and branch on complete, back, or dismiss.

Purpose

Use an External Surface Node when a step should leave Rheo screen chrome and show your UI — for example a custom quiz sheet, native settings picker, or third-party screen Rheo does not ship. Authors place a surf_* node on the canvas (provider: "headless"); your app registers a component under the node's Host key; the SDK renders it and advances when the host calls onComplete, onBack, or onDismiss.

External Surface Nodes do not require an App Settings integration toggle. RevenueCat and Superwall paywalls use a separate Integration Node — see RevenueCat paywall and Superwall paywall.

How to

1. Author the node

  1. In the flow editor, add an External Surface Node from the canvas add menu (Grow+).
  2. Set the Host key in the inspector (defaults to the node id, e.g. surf_custom_step). Use a stable identifier your app can register under — changing it does not rename the graph node id used for analytics and outcomes.
  3. Wire outcomes and a required Fallback:
OutcomeCallback / causeTypical use
completedHost calls onCompleteContinue to the next step
backHost calls onBackJump to a prior screen (or any target you wire)
dismissedHost calls onDismissSoft exit / skip path
failedNo host component registered for the host keySafety net → usually same as Fallback
FallbackAny unmapped outcomeRequired before save / publish

2. Register the host component

Pass externalSurfaces on the managed flow mount. Keys must match each node's Host key (config.hostKey when set, otherwise the surf_* node id).

React Native (Expo / bare)

import { Flow, type ExternalSurfaceHostProps } from '@getrheo/react-native-expo';
// bare: '@getrheo/react-native-bare'

const CustomStep = ({ onComplete, onBack, onDismiss }: ExternalSurfaceHostProps) => (
  <>
    {/* your UI */}
    <Button title="Continue" onPress={onComplete} />
    <Button title="Back" onPress={onBack} />
    <Button title="Skip" onPress={onDismiss} />
  </>
);

<Flow
  channelId="ch_live_xyz"
  externalSurfaces={{
    surf_custom_step: CustomStep,
  }}
/>;

With headless useFlow, read pendingExternalSurface and call reportExternalSurfaceOutcome(nodeId, 'completed' | 'back' | 'dismissed'), or render the same component yourself using those callbacks.

3. Publish and resolve

Publish the flow and assign it to a channel as usual. When the graph lands on the External Surface Node, Rheo clears the Rheo screen, emits surface_presented, renders your component, and waits for a callback. Each callback emits surface_outcome and jumps via the wired target (or Fallback).

Host props

PropTypeRole
surfaceIdstringHost key (config.hostKey or node id)
nodeexternal surface nodeFull manifest node (config, outcomes, fallback)
onComplete() => voidOutcome completed
onBack() => voidOutcome back
onDismiss() => voidOutcome dismissed

Call exactly one of the callbacks when the host UI is finished. Calling more than once after the flow has already advanced is ignored by the state machine.

Manifest shape

{
  "id": "surf_custom_step",
  "name": "Custom host UI",
  "config": { "provider": "headless", "hostKey": "onboardingQuiz" },
  "outcomes": {
    "completed": "scr_next",
    "back": "scr_welcome",
    "dismissed": "scr_skip",
    "failed": "scr_skip"
  },
  "fallback": "scr_skip"
}

Omit hostKey to use the node id as the registry key.

Analytics

EventWhen
surface_presentedFlow lands on the headless node (provider: "headless")
surface_outcomeHost callback or missing-component failure (completed / back / dismissed / failed)

See Event catalog.

Guardrails

  • Registry is required for success. If the flow hits an External Surface Node and externalSurfaces[hostKey] is missing, the SDK resolves failed and follows that branch / Fallback.
  • Not a modal paywall presenter. Unlike RevenueCat / Superwall Integration Nodes, headless UI is rendered in place by Flow (no promise-based externalSurfacePresenter).
  • Keep partner paywalls separate. Do not use an External Surface Node to wrap RevenueCat or Superwall paywalls when you need purchase commerce events — use an Integration Node with provider: "revenuecat" or provider: "superwall" instead.
  • Host key uniqueness. Each headless node's resolved host key (hostKey or node id) must be unique in the flow.
  • Host key vs node id. Changing the Host key in the inspector updates the registry lookup only; outcomes and analytics still use the stable surf_* node id.

Troubleshooting

  • Blank flash then Fallback: no component registered for that host key — check spelling and that you pass externalSurfaces into the same Flow instance that runs the channel.
  • Callback does nothing: the surface may already have advanced; ensure you call the callback only once from user action.
  • Publish blocked on Fallback: connect the Fallback handle in the inspector or canvas.