Skip to main content
A custom component lets you drop your own native SwiftUI view into a FlowPilot flow. You define the component once in the editor (its inputs and the events it can emit), then register a matching native factory in the SDK. When a flow renders a custom component, the SDK builds your view, hands it the resolved inputs, and routes the events your view emits back into the flow’s actions. This is the supported path for native code inside a flow: a paywall, a date picker, a chart, a camera view, anything SwiftUI can draw.

How it works

The editor and the SDK meet at a contract keyed by component key + version:
  1. In the editor you define a custom component: a stable Component Key, a version, typed inputs, and named outputs (events). See Custom components in the editor.
  2. A flow places an instance of that component and binds its inputs (to constants or variables) and its outputs (to actions).
  3. In your app you call registerCustomComponent(key:version:definition:) with a factory that returns a SwiftUI view.
  4. At render time the SDK looks up your factory by key and version, resolves the bound inputs, and calls your factory. Your view emits events through a context object, and the SDK runs the actions the editor wired to those events.
The SDK treats your view as a “dumb renderer”: it receives inputs and emits intent. The flow (defined in the editor) decides what each emitted event does.

Register a component

Register after the SDK is configured and before you present any flow that uses the component.
CustomComponentDefinition carries the input types, the output schemas, and the factory:
  • inputs: declared input keys and their types (.string, .number, .boolean, .list). Used for validation; the keys must match the input keys you defined in the editor.
  • outputs: a map of event name to OutputSchema, declaring which events the view can emit and the shape of each event’s payload.
  • factory: a @MainActor closure that returns your view wrapped in AnyView. It receives the resolved inputs (CustomComponentProps) and an emit context (CustomComponentContext).
OutputSchema declares one event:

Read inputs

The factory’s first argument, CustomComponentProps, gives typed, read-only access to the resolved inputs. Each accessor has an optional form and a form with a default:
Prefer the default: form so your view always has a value. If a key is missing or has the wrong type, the SDK logs a warning in debug builds and returns your default. The keys you read must match the input keys defined in the editor.

Emit events

The factory’s second argument, CustomComponentContext, is how your view tells the flow that something happened:
The eventName must match an output you declared in the editor (and in the definition’s outputs). When you emit, the SDK runs whatever actions the editor wired to that event in the component’s Interactions. The payload is available to those actions as {{payload.<key>}} (for example to write a value into a variable). See Interactions and actions. If you emit an event name that is not declared, the SDK still forwards it but logs a warning. If a payload does not match the declared schema, it is still forwarded with a warning. Treat both warnings as a sign your editor definition and SDK code have drifted.

Container info

The context also exposes the space the component was given:
Current-build limitation: containerSize is a fixed placeholder (300 x 200), not the measured layout size. Do not rely on it for precise sizing yet. Size your view from the layout the flow gives the component, or read the geometry yourself with GeometryReader inside your view.

Versioning

Registration is keyed by key + version so a component can evolve without breaking older flows. The version defaults to 1. When a flow references a component, the SDK looks up key at that exact version. For version 1, the SDK also registers the component under the bare key for backward compatibility, so older flows that omit a version still resolve. The version you register must match CustomComponentRef.version in the flow. If you ship a v2 of a component, register both versions while flows that pin v1 are still live.

Example

A date_picker component with one input (label) and one output (date_selected) carrying a date string and a timestamp number.
In the editor, a button or action wired to the date_selected event can read the payload, for example writing {{payload.date}} into a variable with a Set Variable action. To remove a registration (for example in tests):

The contract

The editor definition and the SDK registration must agree on four things. A mismatch on any of them breaks rendering or events:
Custom components (this page) are different from custom actions. A custom component emits editor-defined events, which is fully supported. A custom action (an action of kind custom defined in the editor) has no public native registration API on the SDK today. If you need native behavior, model it as a custom component that emits an event, and wire the action to that event. See Error handling for the current state of custom actions.

Common mistakes

  • Key or version mismatch. The flow asks for date_picker v2 but you registered v1 (or a different key). The SDK cannot find your factory and shows a placeholder instead.
  • Registering after presenting. Register the component before you call presentPlacement or createSession. A flow that renders before the factory is registered shows the placeholder.
  • Reading an input key the editor did not define. props.string("titel") (typo) returns your default and logs a warning. Match the editor’s input keys exactly.
  • Emitting an event the editor did not define. The event is forwarded but nothing is wired to it, so nothing happens. Declare the output in the editor and in your outputs.
  • Trusting containerSize. It is a fixed placeholder in the current build (see the warning above). Use GeometryReader if you need the real size.

Troubleshooting