Skip to main content
Subflows are the concrete building blocks inside a flow and are auto-discovered by Corbado Observe based on the events you send. They represent a specific authentication method, for example passkey login, password enrollment, or email OTP verification.
Install and set up the Corbado Observe SDK, including your first tracked event, in Getting started. This page explains the subflow concept and documents all available subflow types.
On SDK level, every subflow type has a dedicated class that helps with tracking. Each class defines the default steps you typically want to track for that subflow. If you need to track additional steps beyond the defaults, you can do so by using the generic step events with a custom stepName.

How subflows work

A subflow groups related events for one authentication method. Every subflow has a type (e.g., passkey-login or password-login) and consists of a trigger and one or more steps.

Trigger

The trigger (subflow_trigger) marks the moment of interaction with the subflow. Usually the actor is the user (e.g., clicking “Sign in with passkey”), but in some cases the system can be the actor too (e.g., during passkey enrollment with conditional mediation). Note that steps can run before the trigger fires. For example, during passkey login the get-options step may complete in the background while the UI renders a “Sign in with passkey” button. The trigger fires only when the user clicks that button. If the user never clicks, there is no trigger — meaning there was no interaction with this subflow. Corbado Observe uses the trigger to calculate how long it takes the user to interact with a subflow and to determine if a subflow should be included in certain dashboards or not. The trigger carries:
FieldDescription
actorWho initiated the interaction: user (e.g., clicked a button) or system (e.g., auto-started passkey enrollment with conditional mediation).
explicitSpecTypeOptional. A variant label that distinguishes different flavors of the same subflow type (see the spec type tables for each subflow below).

Steps

Each step represents one phase of the subflow. A step is identified by its stepName (e.g., get-options, ceremony, post-response) and tracked with three generic events:
EventDescription
subflow_step_startedThe step began.
subflow_step_finishedThe step completed successfully.
subflow_step_errorThe step failed.
Every subflow type defines a set of predefined steps (documented in the sections below). You can also define custom steps by passing any stepName string to the generic step events.

Example: passkey login

A successful passkey login subflow where assertion options are pre-fetched in the background:
#EventStepDescription
1subflow_step_startedget-optionsSDK requests assertion options from your server in the background.
2subflow_step_finishedget-optionsServer returns assertion options. UI can now show a “Sign in with passkey” button.
3subflow_triggerUser clicks the button (actor: user).
4subflow_step_startedceremonyBrowser WebAuthn ceremony begins.
5subflow_step_finishedceremonyUser completes the ceremony.
6subflow_step_startedpost-responseSDK sends the assertion response to your server for verification.
7subflow_step_finishedpost-responseServer confirms the login.
The position of the trigger is not fixed — it depends on your implementation. If assertion options are only fetched after the user clicks the button, the trigger would come first:
#EventStepDescription
1subflow_triggerUser clicks “Sign in with passkey” (actor: user).
2subflow_step_startedget-optionsSDK requests assertion options from your server.
3subflow_step_finishedget-optionsServer returns assertion options.
If any step fails, a subflow_step_error replaces the corresponding subflow_step_finished.

Subflow types

1. Provide identifier

SDK class: provideIdentifierOperationFull(inputHtmlField) Tracks when users enter and submit their identifier (e.g., an email address). Because passkey Conditional UI (CUI) is always bound to the same input field as the identifier, CUI passkey login events are tracked together with the provide-identifier subflow. This means a single integration point covers both the identifier input and an optional CUI-initiated passkey login.

Steps

StepSubflow typeDescription
post-responseprovide-identifierServer-side identifier check (e.g., does user exist, which login methods are available).
get-optionspasskey-loginCUI requests assertion options from the server.
ceremonypasskey-loginBrowser CUI ceremony (auto-fill prompt).
post-responsepasskey-loginServer verifies the CUI assertion response.

Spec types

explicitSpecTypeSubflow typeDescription
emailprovide-identifierUser typed into the identifier field (auto-detected by the SDK).
passkey-cuipasskey-loginUser interacted with the Conditional UI auto-fill prompt (auto-detected by the SDK).

Code example

The operation is bound to the identifier input field. This enables the SDK to automatically detect user interaction: when the user starts typing, the provide-identifier trigger fires; when the user interacts with the browser’s CUI autofill prompt, the CUI trigger and ceremony start fire automatically. The SDK also auto-detects CUI cancellation.
import { getTracker } from "@corbado/observe";

const identifierInput = document.getElementById("email") as HTMLInputElement;
const op = getTracker().provideIdentifierOperationFull(identifierInput);

// --- CUI passkey login (runs in background on page load) ---

const startConditionalUI = async () => {
  try {
    op.cui.getOptions.start({ explicitSpecType: "passkey-cui" });
    const options = await fetchConditionalUIOptions();
    op.cui.getOptions.finished({ assertionOptions: JSON.stringify(options) });
  } catch (e) {
    op.cui.getOptions.error(e);
    return;
  }

  try {
    // cui.trigger() and cui.ceremony.start() are auto-detected by the SDK
    const response = await startWebAuthnAuthentication(options, { useBrowserAutofill: true });
    op.cui.ceremony.finished({ assertionResponse: JSON.stringify(response) });

    op.cui.postResponse.start({});
    const result = await verifyOnServer(response);
    op.cui.postResponse.finished({}, { userReference: { userId: result.userId } });
  } catch (e) {
    op.cui.ceremony.error(e);
  }
};

// --- Provide identifier (user submits the form) ---

const handleIdentifierSubmit = async (email: string) => {
  op.provideIdentifier.postResponse.start({ explicitSpecType: "email" });

  try {
    const result = await checkUserOnServer(email);
    op.provideIdentifier.postResponse.finished({}, {
      userReference: { userId: result.userId },
    });
  } catch (e) {
    op.provideIdentifier.postResponse.error(e);
  }
};

// Clean up when the component unmounts
op.destroy();

2. Passkey login

SDK class: passkeyLoginFullOperation() Tracks passkey-based sign-in attempts where the relying party explicitly triggers the WebAuthn ceremony (not via Conditional UI — see Provide identifier for CUI).

Steps

StepDescription
get-optionsRequest assertion options from the server.
ceremonyBrowser WebAuthn authentication ceremony.
post-responseServer verifies the assertion response.

Spec types

explicitSpecTypeDescription
passkey-known-identifierUser’s identifier is already known (e.g., post-identifier screen). The server can scope the allowed credentials to this user.
passkey-no-identifierNo identifier is known. The server returns a broad set of allowed credentials.
passkey-cuiConditional UI initiated passkey login (typically tracked via the provide-identifier integration).

Code example

The user’s identifier is already known. A “Sign in with passkey” button is shown, and the user clicks it to start. The trigger fires on click with actor: "user".
import { getTracker } from "@corbado/observe";

const handlePasskeyLogin = async (email: string) => {
  const passkeyOp = getTracker().passkeyLoginFullOperation();
  passkeyOp.trigger({ actor: "user" });

  try {
    passkeyOp.getOptions.start({ explicitSpecType: "passkey-known-identifier" });
    const options = await fetchAssertionOptions(email);
    passkeyOp.getOptions.finished({ assertionOptions: JSON.stringify(options) });
  } catch (e) {
    passkeyOp.getOptions.error(e);
    return;
  }

  try {
    passkeyOp.ceremony.start({});
    const response = await startWebAuthnAuthentication(options);
    passkeyOp.ceremony.finished({ assertionResponse: JSON.stringify(response) });
  } catch (e) {
    passkeyOp.ceremony.error(e);
    return;
  }

  try {
    passkeyOp.postResponse.start({});
    await verifyOnServer(response);
    passkeyOp.postResponse.finished({});
  } catch (e) {
    passkeyOp.postResponse.error(e);
  }
};

3. Passkey enrollment

SDK class: passkeyEnrollmentFullOperation() Tracks passkey registration and setup after authentication (e.g., during a post-login enrollment prompt).

Steps

StepDescription
get-optionsRequest attestation options from the server.
ceremonyBrowser WebAuthn registration ceremony.
post-responseServer verifies the attestation response and stores the credential.

Spec types

explicitSpecTypeDescription
conditional-auto-manualSystem first attempts conditional (auto-register) enrollment, then falls back to a regular prompt if it fails, and finally allows the user to trigger it manually.
auto-manualSystem auto-triggers the enrollment prompt, with a manual fallback.
manualUser explicitly triggers enrollment (e.g., clicks “Create passkey”).

Code example

The system first attempts conditional mediation (auto-register) enrollment. If the user dismisses the conditional prompt, it falls back to a regular auto-triggered prompt.
import { getTracker } from "@corbado/observe";

const enrollOp = getTracker().passkeyEnrollmentFullOperation();

const handleEnrollment = async (userId: string, isConditional: boolean, isAuto: boolean) => {
  const specType = isConditional ? "conditional-auto-manual" : "auto-manual";
  enrollOp.trigger({ actor: isConditional || isAuto ? "system" : "user" });

  try {
    enrollOp.getOptions.start({ explicitSpecType: specType });
    const options = await fetchAttestationOptions(userId);
    enrollOp.getOptions.finished({ attestationOptions: JSON.stringify(options) });
  } catch (e) {
    enrollOp.getOptions.error(e);
    return;
  }

  try {
    enrollOp.ceremony.start({ mediation: isConditional ? "conditional" : "required" });
    const response = await startWebAuthnRegistration(options, { useAutoRegister: isConditional });
    enrollOp.ceremony.finished({ attestationResponse: JSON.stringify(response) });
  } catch (e) {
    enrollOp.ceremony.error(e);
    if (isConditional) {
      // Conditional prompt dismissed — fall back to auto
      await handleEnrollment(userId, false, true);
    }
    return;
  }

  try {
    enrollOp.postResponse.start({});
    await verifyOnServer(response);
    enrollOp.postResponse.finished({});
  } catch (e) {
    enrollOp.postResponse.error(e);
  }
};

// Start with conditional enrollment
await handleEnrollment(userId, true, true);

4. Password login

SDK class: passwordLoginFullOperation(autoTrackConfig?) Tracks password-based login attempts. The trigger is auto-detected by the SDK when the user starts typing into the password input field.

Steps

StepDescription
post-responseServer verifies the submitted password.

Spec types

explicitSpecTypeDescription
password-known-identifierIdentifier was already submitted in a previous step (e.g., post-identifier screen with a pre-filled email).
password-with-identifierIdentifier and password are submitted together on the same screen.

Typed errors

In addition to passing raw errors, the password login subflow supports typed error codes:
CodeDescription
invalid_passwordThe submitted password is incorrect.
user_not_foundNo user exists for the given identifier.
account_lockedThe account has been locked.

5. Password enrollment

SDK class: passwordEnrollmentFullOperation(autoTrackConfig?) Tracks password creation or password reset during authentication journeys. The trigger is auto-detected by the SDK when the user starts typing into the password input field.

Steps

StepDescription
post-responseServer processes the new or updated password.

Spec types

explicitSpecTypeDescription
password-setUser sets a password for the first time (e.g., during signup).
password-resetUser resets an existing password (e.g., via a recovery flow).

Typed errors

CodeDescription
requirements_not_fulfilledThe password does not meet the required complexity rules.

6. Email OTP

SDK class: emailOtpOperationFull() Tracks one-time-password verification sent by email.

Steps

StepDescription
sendServer sends the OTP email to the user.
post-responseServer verifies the OTP code the user submitted.
resendUser requests a new OTP code.

Spec types

explicitSpecTypeDescription
email-otp-loginOTP is used as a login method.
email-otp-enrollmentOTP is used for enrollment or account activation.

SDK class: emailLinkOperationFull() Tracks authentication flows where users sign in or verify their identity through a link sent by email.

Steps

StepDescription
sendServer sends the email containing the magic link.
post-responseServer verifies the token from the clicked link.
resendUser requests a new email link.

Spec types

explicitSpecTypeDescription
email-link-loginEmail link is used as a login or recovery method.
email-link-enrollmentEmail link is used for enrollment or account activation.

8. Social login

SDK class: socialLoginOperationFull() Tracks authentication with social identity providers (e.g., Google, Apple, Facebook).

Steps

StepDescription
get-redirect-urlApplication initiates the OAuth redirect to the social provider.
exchange-codeApplication exchanges the authorization code for tokens after the user returns from the provider.

Spec types

explicitSpecTypeDescription
pre-identifierSocial login button is shown before the user enters any identifier (e.g., on the initial login screen).
post-identifierSocial login is offered after the user has already provided an identifier.

9. Coming soon

The following subflow types are planned but not yet available:
Subflow typeDescription
sms-otpOTP verification sent via SMS.
provide-dataCollecting additional user data during a flow (e.g., name, address).
totpTime-based one-time password (authenticator app).