> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flow-board.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Retrieve flow data

> Use Flowboard form data during the flow and when the flow finishes.

Flowboard returns collected user input as `formData`.

Each key in `formData` matches the `id` of an input component in your flow. If your flow has inputs with IDs like `email`, `plan`, and `accept_terms`, Flowboard returns data in this shape:

```json theme={null}
{
  "email": "ada@example.com",
  "plan": "pro",
  "accept_terms": true
}
```

You can read that data in three places:

| API                                        | When to use it                                                      | What you receive                                       |
| ------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------ |
| `onOnboardEnd(formData)`                   | When the flow is completed                                          | The final collected data                               |
| `onStepChange(pageId, pageStep, formData)` | When you want progress-aware analytics or autosave                  | The current data snapshot at that step                 |
| `ctx.formData` / `ctx.values`              | Inside a custom screen or custom action, where supported by the SDK | The latest data collected before that screen or action |

## Simple example

Start with `onOnboardEnd` when you only need the final values.

<CodeGroup>
  ```tsx Expo theme={null}
  import { Flowboard } from 'flowboard-react';

  export async function startFlow() {
    await Flowboard.launchOnboarding({
      onOnboardEnd: (formData) => {
        const email =
          typeof formData.email === 'string' ? formData.email.trim() : '';
        const selectedPlan =
          typeof formData.plan === 'string' ? formData.plan : 'free';

        console.log('Flow completed', {
          email,
          selectedPlan,
        });
      },
    });
  }
  ```

  ```tsx React Native theme={null}
  import { Flowboard } from 'flowboard-react';

  export async function startFlow() {
    await Flowboard.launchOnboarding({
      onOnboardEnd: (formData) => {
        const email =
          typeof formData.email === 'string' ? formData.email.trim() : '';
        const selectedPlan =
          typeof formData.plan === 'string' ? formData.plan : 'free';

        console.log('Flow completed', {
          email,
          selectedPlan,
        });
      },
    });
  }
  ```

  ```dart Flutter theme={null}
  import 'package:flutter/material.dart';
  import 'package:flowboard_flutter/flowboard_flutter.dart';

  Future<void> startFlow(BuildContext context) async {
    await Flowboard.launchOnboarding(
      context,
      onOnboardEnd: (formData) {
        final email = (formData['email'] as String? ?? '').trim();
        final selectedPlan = formData['plan'] as String? ?? 'free';

        debugPrint('Flow completed: email=$email, plan=$selectedPlan');
      },
    );
  }
  ```

  ```swift iOS theme={null}
  import FlowboardSwiftCore
  import FlowboardSwiftUIKit

  func startFlow() {
    Task {
      do {
        try await Flowboard.launchOnboarding(
          from: self,
          options: .init(
            onOnboardEnd: { formData in
              let email =
                formData.string("email")?
                .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
              let selectedPlan = formData.string("plan") ?? "free"

              print("Flow completed: email=\(email), plan=\(selectedPlan)")
            }
          )
        )
      } catch {
        print("Failed to launch Flowboard:", error)
      }
    }
  }
  ```
</CodeGroup>

<Tip>
  Keep your Flowboard input IDs stable. Those IDs become your `formData` keys, so changing them changes the data contract in your app.
</Tip>

## Use data before the flow ends

You do not have to wait until completion.

Use `onStepChange` for analytics and autosave. On SDKs that expose custom screens or custom actions, you can also read the current answers from `ctx.formData` or `ctx.values`.

<CodeGroup>
  ```tsx Expo / React Native theme={null}
  await Flowboard.launchOnboarding({
    onStepChange: (pageId, pageStep, formData) => {
      analytics.track('flow_step_viewed', {
        pageId,
        pageStep,
        selectedPlan:
          typeof formData.plan === 'string' ? formData.plan : undefined,
      });
    },
    customScreenBuilder: (ctx) => {
      if (ctx.screenData.id !== 'profile_summary') {
        return null;
      }

      return (
        <ProfileSummaryCard
          name={typeof ctx.formData.fullname === 'string' ? ctx.formData.fullname : ''}
          email={typeof ctx.formData.email === 'string' ? ctx.formData.email : ''}
          onContinue={ctx.onNext}
        />
      );
    },
  });
  ```

  ```dart Flutter theme={null}
  await Flowboard.launchOnboarding(
    context,
    onStepChange: (pageId, pageStep, formData) {
      analytics.track('flow_step_viewed', {
        'pageId': pageId,
        'pageStep': pageStep,
        'selectedPlan': formData['plan'],
      });
    },
    customScreenBuilder: (ctx) {
      if (ctx.screenData['id'] != 'profile_summary') {
        return const SizedBox.shrink();
      }

      return ProfileSummaryCard(
        name: ctx.formData['fullname'] as String? ?? '',
        email: ctx.formData['email'] as String? ?? '',
        onContinue: ctx.onNext,
      );
    },
  );
  ```

  ```swift iOS theme={null}
  import FlowboardSwiftCore
  import FlowboardSwiftUIKit

  func startFlow() {
    Task {
      do {
        try await Flowboard.launchOnboarding(
          from: self,
          options: .init(
            onStepChange: { pageId, pageStep, formData in
              analytics.track(
                "flow_step_viewed",
                properties: [
                  "pageId": pageId ?? "",
                  "pageStep": pageStep,
                  "selectedPlan": formData.string("plan") ?? ""
                ]
              )
            }
          )
        )
      } catch {
        print("Failed to launch Flowboard:", error)
      }
    }
  }
  ```
</CodeGroup>

<Note>
  The current Swift package exposes `onOnboardEnd`, `onStepChange`, and `ctx.values` inside action callbacks for native data access. Native custom-screen builders are not part of the Swift package surface today.
</Note>

## Recommended production setup

In production, do not pass raw `formData` directly to the rest of your app.

Map it into a small, typed payload first. This gives you one place to validate keys, apply defaults, and keep your backend contract stable even if the flow grows.

<CodeGroup>
  ```tsx Expo theme={null}
  // src/flowboard/flowResult.ts
  type FlowboardSignupPayload = {
    email: string;
    fullName: string;
    plan: 'free' | 'pro' | 'team';
    acceptedTerms: boolean;
  };

  function readString(
    formData: Record<string, unknown>,
    key: string
  ): string {
    const value = formData[key];
    return typeof value === 'string' ? value.trim() : '';
  }

  function readPlan(
    formData: Record<string, unknown>
  ): FlowboardSignupPayload['plan'] {
    const value = formData.plan;
    return value === 'pro' || value === 'team' ? value : 'free';
  }

  export function buildFlowboardSignupPayload(
    formData: Record<string, unknown>
  ): FlowboardSignupPayload {
    return {
      email: readString(formData, 'email'),
      fullName: readString(formData, 'fullname'),
      plan: readPlan(formData),
      acceptedTerms: Boolean(formData.accept_terms),
    };
  }

  // src/features/onboarding/launchSignupFlow.ts
  import { Flowboard } from 'flowboard-react';
  import { buildFlowboardSignupPayload } from '../../flowboard/flowResult';

  async function submitOnboardingResult(payload: FlowboardSignupPayload) {
    await api.post('/onboarding/complete', payload);
  }

  export async function launchSignupFlow() {
    await Flowboard.launchOnboarding({
      onStepChange: (pageId, pageStep, formData) => {
        analytics.track('flow_step_viewed', {
          pageId,
          pageStep,
          plan: buildFlowboardSignupPayload(formData).plan,
        });
      },
      onOnboardEnd: (formData) => {
        const payload = buildFlowboardSignupPayload(formData);
        void submitOnboardingResult(payload);
      },
    });
  }
  ```

  ```tsx React Native theme={null}
  // src/flowboard/flowResult.ts
  type FlowboardSignupPayload = {
    email: string;
    fullName: string;
    plan: 'free' | 'pro' | 'team';
    acceptedTerms: boolean;
  };

  function readString(
    formData: Record<string, unknown>,
    key: string
  ): string {
    const value = formData[key];
    return typeof value === 'string' ? value.trim() : '';
  }

  function readPlan(
    formData: Record<string, unknown>
  ): FlowboardSignupPayload['plan'] {
    const value = formData.plan;
    return value === 'pro' || value === 'team' ? value : 'free';
  }

  export function buildFlowboardSignupPayload(
    formData: Record<string, unknown>
  ): FlowboardSignupPayload {
    return {
      email: readString(formData, 'email'),
      fullName: readString(formData, 'fullname'),
      plan: readPlan(formData),
      acceptedTerms: Boolean(formData.accept_terms),
    };
  }

  // src/features/onboarding/launchSignupFlow.ts
  import { Flowboard } from 'flowboard-react';
  import { buildFlowboardSignupPayload } from '../../flowboard/flowResult';

  async function submitOnboardingResult(payload: FlowboardSignupPayload) {
    await api.post('/onboarding/complete', payload);
  }

  export async function launchSignupFlow() {
    await Flowboard.launchOnboarding({
      onStepChange: (pageId, pageStep, formData) => {
        analytics.track('flow_step_viewed', {
          pageId,
          pageStep,
          plan: buildFlowboardSignupPayload(formData).plan,
        });
      },
      onOnboardEnd: (formData) => {
        const payload = buildFlowboardSignupPayload(formData);
        void submitOnboardingResult(payload);
      },
    });
  }
  ```

  ```dart Flutter theme={null}
  // lib/flowboard/flow_result.dart
  class FlowboardSignupPayload {
    final String email;
    final String fullName;
    final String plan;
    final bool acceptedTerms;

    const FlowboardSignupPayload({
      required this.email,
      required this.fullName,
      required this.plan,
      required this.acceptedTerms,
    });

    Map<String, dynamic> toJson() {
      return {
        'email': email,
        'fullName': fullName,
        'plan': plan,
        'acceptedTerms': acceptedTerms,
      };
    }
  }

  String _readString(Map<String, dynamic> formData, String key) {
    final value = formData[key];
    return value is String ? value.trim() : '';
  }

  String _readPlan(Map<String, dynamic> formData) {
    final value = formData['plan'];
    if (value == 'pro' || value == 'team') {
      return value as String;
    }
    return 'free';
  }

  FlowboardSignupPayload buildFlowboardSignupPayload(
    Map<String, dynamic> formData,
  ) {
    return FlowboardSignupPayload(
      email: _readString(formData, 'email'),
      fullName: _readString(formData, 'fullname'),
      plan: _readPlan(formData),
      acceptedTerms: formData['accept_terms'] == true,
    );
  }

  // lib/features/onboarding/launch_signup_flow.dart
  Future<void> submitOnboardingResult(FlowboardSignupPayload payload) async {
    await api.post('/onboarding/complete', payload.toJson());
  }

  Future<void> launchSignupFlow(BuildContext context) async {
    await Flowboard.launchOnboarding(
      context,
      onStepChange: (pageId, pageStep, formData) {
        final payload = buildFlowboardSignupPayload(formData);

        analytics.track('flow_step_viewed', {
          'pageId': pageId,
          'pageStep': pageStep,
          'plan': payload.plan,
        });
      },
      onOnboardEnd: (formData) {
        final payload = buildFlowboardSignupPayload(formData);
        submitOnboardingResult(payload);
      },
    );
  }
  ```

  ```swift iOS theme={null}
  import FlowboardSwiftCore
  import FlowboardSwiftUIKit
  import UIKit

  struct FlowboardSignupPayload: Encodable {
    let email: String
    let fullName: String
    let plan: String
    let acceptedTerms: Bool
  }

  func readString(_ formData: [String: FlowboardJSONValue], _ key: String) -> String {
    formData.string(key)?
      .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
  }

  func readPlan(_ formData: [String: FlowboardJSONValue]) -> String {
    switch formData.string("plan") {
    case "pro", "team":
      return formData.string("plan") ?? "free"
    default:
      return "free"
    }
  }

  func buildFlowboardSignupPayload(
    from formData: [String: FlowboardJSONValue]
  ) -> FlowboardSignupPayload {
    FlowboardSignupPayload(
      email: readString(formData, "email"),
      fullName: readString(formData, "fullname"),
      plan: readPlan(formData),
      acceptedTerms: formData.bool("accept_terms") ?? false
    )
  }

  func submitOnboardingResult(_ payload: FlowboardSignupPayload) async throws {
    try await api.post("/onboarding/complete", body: payload)
  }

  func launchSignupFlow(from host: UIViewController) {
    Task {
      do {
        try await Flowboard.launchOnboarding(
          from: host,
          options: .init(
            onStepChange: { pageId, pageStep, formData in
              let payload = buildFlowboardSignupPayload(from: formData)

              analytics.track(
                "flow_step_viewed",
                properties: [
                  "pageId": pageId ?? "",
                  "pageStep": pageStep,
                  "plan": payload.plan
                ]
              )
            },
            onOnboardEnd: { formData in
              let payload = buildFlowboardSignupPayload(from: formData)
              Task {
                try? await submitOnboardingResult(payload)
              }
            }
          )
        )
      } catch {
        print("Failed to launch Flowboard:", error)
      }
    }
  }
  ```
</CodeGroup>

<Note>
  Replace `analytics`, `api`, and any helper components in these examples with the services and UI primitives used in your app.
</Note>

## Summary

Use this rule:

* Read final data in `onOnboardEnd`
* Read progress snapshots in `onStepChange`
* Read live values in `ctx.formData` or `ctx.values`, depending on the SDK surface you use
* Normalize the payload before sending it to your backend
