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

# Custom tracking

> Send Flowboard events to your own MMP, ads SDK, or product analytics stack.

Flowboard does not force a tracking vendor.

Use `onStepChange` and `onOnboardEnd` to send events to your own tools (MMP, ads SDK, product analytics).

## What you can track from Flowboard callbacks

| Callback                                   | When it fires                       | Data you receive                                                                                |
| ------------------------------------------ | ----------------------------------- | ----------------------------------------------------------------------------------------------- |
| `onStepChange(stepId, pageStep, formData)` | Every time the user lands on a step | `stepId` (screen id), `pageStep` (0-based step index), `formData` (current snapshot of answers) |
| `onOnboardEnd(formData)`                   | When the flow finishes              | `formData` (final collected answers)                                                            |

`formData` keys match your Flowboard input `id` values.

Example:

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

<Tip>
  Keep input `id` values stable in Flowboard. They are your analytics data contract.
</Tip>

## Simple custom tracking examples

Use this pattern when you only need to forward events to your own tracking layer.

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

  export async function launchTrackedFlow() {
    await Flowboard.launchOnboarding({
      onOnboardEnd: (formData) => {
        console.log('Flow finished:', formData);
        myTracking.track('flow_completed', {
          plan: typeof formData.plan === 'string' ? formData.plan : undefined,
          acceptedTerms: Boolean(formData.accept_terms),
        });
      },
      onStepChange: (stepId, pageStep, formData) => {
        console.log('User moved to step:', stepId, pageStep, formData);
        myTracking.track('flow_step_viewed', {
          stepId,
          pageStep,
          plan: typeof formData.plan === 'string' ? formData.plan : undefined,
        });
      },
    });
  }
  ```

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

  Future<void> launchTrackedFlow(BuildContext context) async {
    await Flowboard.launchOnboarding(
      context,
      onOnboardEnd: (formData) {
        debugPrint('Flow finished: $formData');
        myTracking.track('flow_completed', {
          'plan': formData['plan'],
          'acceptedTerms': formData['accept_terms'] == true,
        });
      },
      onStepChange: (stepId, pageStep, formData) {
        debugPrint('User moved to step: $stepId $pageStep $formData');
        myTracking.track('flow_step_viewed', {
          'stepId': stepId,
          'pageStep': pageStep,
          'plan': formData['plan'],
        });
      },
    );
  }
  ```

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

  func launchTrackedFlow(from host: UIViewController) {
    Task {
      do {
        try await Flowboard.launchOnboarding(
          from: host,
          options: .init(
            onOnboardEnd: { formData in
              print("Flow finished:", formData)
              myTracking.track(
                "flow_completed",
                properties: [
                  "plan": formData.string("plan") ?? "",
                  "acceptedTerms": formData.bool("accept_terms") ?? false
                ]
              )
            },
            onStepChange: { stepId, pageStep, formData in
              print("User moved to step:", stepId ?? "", pageStep, formData)
              myTracking.track(
                "flow_step_viewed",
                properties: [
                  "stepId": stepId ?? "",
                  "pageStep": pageStep,
                  "plan": formData.string("plan") ?? ""
                ]
              )
            }
          )
        )
      } catch {
        print("Failed to launch Flowboard:", error)
      }
    }
  }
  ```
</CodeGroup>

## Concrete example: Amplitude + Adjust

The example below sends:

* Product analytics events to Amplitude
* Attribution / marketing events to Adjust

<Warning>
  For Expo, use a development build or prebuild setup for native SDKs such as Adjust.
</Warning>

### React Native / Expo (development build)

```bash theme={null}
npm install @amplitude/analytics-react-native react-native-adjust
```

<CodeGroup>
  ```tsx React Native / Expo theme={null}
  import { Flowboard } from 'flowboard-react';
  import { init as amplitudeInit, track as amplitudeTrack } from '@amplitude/analytics-react-native';
  import { Adjust, AdjustConfig, AdjustEnvironment, AdjustEvent } from 'react-native-adjust';

  const ADJUST_STEP_TOKEN = 'abc123';
  const ADJUST_COMPLETE_TOKEN = 'xyz789';

  export function initTracking() {
    amplitudeInit('AMPLITUDE_API_KEY');

    const config = new AdjustConfig('ADJUST_APP_TOKEN', AdjustEnvironment.Production);
    Adjust.create(config);
  }

  function trackAdjustStep(stepId: string, pageStep: number) {
    const event = new AdjustEvent(ADJUST_STEP_TOKEN);
    event.addCallbackParameter('step_id', stepId);
    event.addCallbackParameter('page_step', String(pageStep));
    Adjust.trackEvent(event);
  }

  function trackAdjustComplete(plan: string | undefined) {
    const event = new AdjustEvent(ADJUST_COMPLETE_TOKEN);
    if (plan) event.addCallbackParameter('plan', plan);
    Adjust.trackEvent(event);
  }

  export async function launchTrackedFlow() {
    await Flowboard.launchOnboarding({
      onStepChange: (stepId, pageStep, formData) => {
        const plan = typeof formData.plan === 'string' ? formData.plan : undefined;

        amplitudeTrack('flow_step_viewed', {
          step_id: stepId,
          page_step: pageStep,
          plan,
        });

        if (typeof stepId === 'string' && stepId.length > 0) {
          trackAdjustStep(stepId, pageStep);
        }
      },
      onOnboardEnd: (formData) => {
        const plan = typeof formData.plan === 'string' ? formData.plan : undefined;

        amplitudeTrack('flow_completed', {
          plan,
          accepted_terms: formData.accept_terms === true,
        });

        trackAdjustComplete(plan);
      },
    });
  }
  ```
</CodeGroup>

### Flutter

```bash theme={null}
flutter pub add amplitude_flutter adjust_sdk
```

For native iOS, install your Amplitude and Adjust SDKs in the same app target that hosts Flowboard, then forward Flowboard callbacks into those SDKs from one small bridge object.

<CodeGroup>
  ```dart Flutter theme={null}
  import 'package:amplitude_flutter/amplitude.dart';
  import 'package:adjust_sdk/adjust.dart';
  import 'package:adjust_sdk/adjust_config.dart';
  import 'package:adjust_sdk/adjust_event.dart';
  import 'package:flutter/material.dart';
  import 'package:flowboard_flutter/flowboard_flutter.dart';

  const String adjustStepToken = 'abc123';
  const String adjustCompleteToken = 'xyz789';

  final Amplitude amplitude = Amplitude.getInstance();

  void initTracking() {
    amplitude.init('AMPLITUDE_API_KEY');

    final config = AdjustConfig('ADJUST_APP_TOKEN', AdjustEnvironment.production);
    Adjust.initSdk(config);
  }

  void trackAdjustStep(String stepId, int pageStep) {
    final event = AdjustEvent(adjustStepToken);
    event.addCallbackParameter('step_id', stepId);
    event.addCallbackParameter('page_step', pageStep.toString());
    Adjust.trackEvent(event);
  }

  void trackAdjustComplete(String? plan) {
    final event = AdjustEvent(adjustCompleteToken);
    if (plan != null && plan.isNotEmpty) {
      event.addCallbackParameter('plan', plan);
    }
    Adjust.trackEvent(event);
  }

  Future<void> launchTrackedFlow(BuildContext context) async {
    await Flowboard.launchOnboarding(
      context,
      onStepChange: (stepId, pageStep, formData) {
        final plan = formData['plan'] as String?;

        amplitude.logEvent('flow_step_viewed', eventProperties: {
          'step_id': stepId,
          'page_step': pageStep,
          'plan': plan,
        });

        if (stepId.isNotEmpty) {
          trackAdjustStep(stepId, pageStep);
        }
      },
      onOnboardEnd: (formData) {
        final plan = formData['plan'] as String?;

        amplitude.logEvent('flow_completed', eventProperties: {
          'plan': plan,
          'accepted_terms': formData['accept_terms'] == true,
        });

        trackAdjustComplete(plan);
      },
    );
  }
  ```

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

  let adjustStepToken = "abc123"
  let adjustCompleteToken = "xyz789"

  func trackAmplitude(_ event: String, properties: [String: Any]) {
    amplitude.track(event, properties: properties)
  }

  func trackAdjust(token: String, callbackParameters: [String: String]) {
    adjust.trackEvent(token: token, callbackParameters: callbackParameters)
  }

  func launchTrackedFlow(from host: UIViewController) {
    Task {
      do {
        try await Flowboard.launchOnboarding(
          from: host,
          options: .init(
            onStepChange: { stepId, pageStep, formData in
              let plan = formData.string("plan")

              trackAmplitude(
                "flow_step_viewed",
                properties: [
                  "step_id": stepId ?? "",
                  "page_step": pageStep,
                  "plan": plan ?? ""
                ]
              )

              if let stepId, !stepId.isEmpty {
                trackAdjust(
                  token: adjustStepToken,
                  callbackParameters: [
                    "step_id": stepId,
                    "page_step": String(pageStep)
                  ]
                )
              }
            },
            onOnboardEnd: { formData in
              let plan = formData.string("plan")

              trackAmplitude(
                "flow_completed",
                properties: [
                  "plan": plan ?? "",
                  "accepted_terms": formData.bool("accept_terms") ?? false
                ]
              )

              var params: [String: String] = [:]
              if let plan, !plan.isEmpty {
                params["plan"] = plan
              }
              trackAdjust(token: adjustCompleteToken, callbackParameters: params)
            }
          )
        )
      } catch {
        print("Failed to launch Flowboard:", error)
      }
    }
  }
  ```
</CodeGroup>

## Tracking recommendations

* Avoid sending raw PII to ad attribution tools unless your policy allows it.
* Keep event names stable (`flow_step_viewed`, `flow_completed`).
* Forward only the fields your downstream tools actually use.
* Version your event schema if you change `formData` keys.
