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

> Render your own native SwiftUI view wherever a flow references a custom component, and wire its events back into the flow.

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](/editor/custom-components).
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.

```swift theme={null}
public func registerCustomComponent(
    key: String,
    version: Int = 1,
    definition: CustomComponentDefinition
)
```

`CustomComponentDefinition` carries the input types, the output schemas, and the factory:

```swift theme={null}
public init(
    inputs: [String: VariableType]? = nil,
    outputs: [String: OutputSchema]? = nil,
    factory: @escaping @Sendable @MainActor (CustomComponentProps, CustomComponentContext) -> AnyView
)
```

* `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:

```swift theme={null}
public init(description: String? = nil, payload: [String: VariableType]? = nil)
```

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

```swift theme={null}
func string(_ key: String) -> String?
func string(_ key: String, default: String) -> String

func number(_ key: String) -> Double?
func number(_ key: String, default: Double) -> Double

func bool(_ key: String) -> Bool?
func bool(_ key: String, default: Bool) -> Bool

func int(_ key: String) -> Int?
func int(_ key: String, default: Int) -> Int
```

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:

```swift theme={null}
func emit(_ eventName: String, payload: [String: Any]? = nil)
func emit(_ eventName: String)   // convenience, no payload
```

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](/editor/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:

```swift theme={null}
public let containerSize: CGSize
public let containerConstraints: ContainerConstraints
```

<Warning>
  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.
</Warning>

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

```swift theme={null}
import SwiftUI
import FlowPilotSDK

let datePicker = CustomComponentDefinition(
    inputs: ["label": .string],
    outputs: [
        "date_selected": OutputSchema(
            description: "Fires when the user picks a date",
            payload: ["date": .string, "timestamp": .number]
        )
    ],
    factory: { props, context in
        let label = props.string("label", default: "Pick a date")

        return AnyView(
            MyDatePicker(label: label) { date in
                let iso = ISO8601DateFormatter().string(from: date)
                context.emit("date_selected", payload: [
                    "date": iso,
                    "timestamp": date.timeIntervalSince1970
                ])
            }
        )
    }
)

// Register once, after configure() and before presenting a flow that uses it.
FlowPilot.shared?.registerCustomComponent(
    key: "date_picker",
    version: 1,
    definition: datePicker
)
```

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):

```swift theme={null}
FlowPilot.shared?.unregisterCustomComponent(key: "date_picker", version: 1)
```

## The contract

The editor definition and the SDK registration must agree on four things. A mismatch on any of them breaks rendering or events:

| Must match    | Editor side     | SDK side                                    |
| ------------- | --------------- | ------------------------------------------- |
| Component key | Component Key   | `key:` in `registerCustomComponent`         |
| Version       | version         | `version:` in `registerCustomComponent`     |
| Input keys    | defined Inputs  | keys you read with `props.string(...)` etc. |
| Event keys    | defined Outputs | names you pass to `context.emit(...)`       |

<Note>
  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](/ios-sdk/error-handling) for the current state of custom actions.
</Note>

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

| Symptom                                | Likely cause                                                 | Fix                                                                                                                                                     |
| -------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Component renders as a placeholder box | Not registered, or key/version mismatch                      | Register the exact key and version the flow uses, before presenting. Watch for the `custom_component_not_found` error code (`customComponentNotFound`). |
| View renders but events do nothing     | Emitted event name not wired in the editor, or name mismatch | Declare the output in the editor, wire an action to it, and emit the exact name.                                                                        |
| Inputs are empty or wrong              | Input key mismatch or wrong type                             | Match the editor's input keys and types; read with the `default:` accessors and check debug-log warnings.                                               |
| Payload value missing in an action     | Payload key not declared or `{{payload.key}}` typo           | Declare the payload field in `OutputSchema` and reference the exact key in the action.                                                                  |

## Related pages

* [Custom components in the editor](/editor/custom-components)
* [Custom screens](/ios-sdk/custom-screens)
* [Interactions and actions](/editor/interactions-and-actions)
* [Error handling](/ios-sdk/error-handling)
