Install and set up the Corbado Observe SDK, including your first tracked event, in Custom events. This page explains the subflow concept and documents all available subflow types.
1. How subflows work
On SDK level, every subflow type has an operation helper on the tracker, for examplepasskeyLoginFullOperation(). The helper stamps the subflow type and step names for you, so your code only wraps the application logic of each step.
1.1 Start
Creating the helper emitssubflow_started: the attempt begins. Construction always emits the start, even without an input field or configuration. Do not add a manual subflowStart() on top of it. Repeated starts of the same subflow type with nothing in between are merged. A different, known spec type starts a new attempt.
When to create the helper depends on how the method is engaged:
Older SDK versions used a separate
subflow_trigger event to mark the moment of interaction. It is deprecated and carries no classification value. Interaction is read from the first step and from the bound input field.1.2 Steps
Each step is one phase of the attempt, identified by its step name, and tracked with three events:
Every helper defines its steps (section 3). Custom steps beyond the predefined ones are available through
op.customStep("my-step").
Never finish a subflow. There is no subflow-finished event. The classifier derives each attempt’s outcome from its steps. The outcome-bearing step is postResponse for almost every subflow (exchangeCode for social login, ceremony for app confirmation). Track it always. Earlier steps are enrichment you may skip when the effort outweighs the value, with one exception: the WebAuthn ceremony steps of the passkey subflows. Track those whenever passkeys are in play, because ceremony start, finish and error power all passkey analytics and none of it is recoverable from postResponse alone. A completed ceremony without a postResponse still classifies as incomplete, because only the backend confirmation proves the method worked.
On failure, call .error() on the step that failed and stop. A retry is simply a new sequence of step events.
1.3 Spec types
explicitSpecType distinguishes variants of one subflow type, for example a passkey login with a known identifier from one without. Supply it in the helper’s config whenever it is known. If it only becomes known mid-attempt, send it on a later step’s start data. The last spec type wins.
1.4 Errors
Error tracking is an optional investment. Classification never depends on it: an attempt that just stops already classifies as incomplete, and an explicit step error is a different, more diagnostic outcome. The backend groups every reported error by its exact signature (subflow type, step, code, message, spec type) into error flavours that are then curated into named errors with impact analysis. That yields four rules:- Platform errors go in raw. For failures the browser or operating system produces, such as a WebAuthn exception, pass the caught exception to
.error(e). The platform’s own vocabulary is bounded and groups well. - Your own errors deserve a deliberate shape. For failures from your API, pass a plain
{ code, message }object: a code naming what the client observed (invalid_password,invalid_otp,http_error), reused wherever the same observation recurs, and the server’s raw error label as the message. Where a helper predefines typed codes, prefererrorTyped()for the compile-time check. - Keep volatile tokens out of messages. Request IDs, timestamps and user data fragment the grouping.
- No fallback codes. A response you cannot classify is neither success nor failure. Leave the step open and it classifies as incomplete.
ceremony error with the raw browser error. The exception is an auto-offered method torn down because the user proceeded with another, for example a Conditional UI request aborted by a password submit. That is not an error.
1.5 Cleanup
Input-bound helpers and the passkey helpers hold event listeners. Callop.destroy() when the surface unmounts.
2. Example: passkey login
A “Sign in with passkey” button after the identifier is known:getOptions.finished takes assertionOptions and ceremony.finished takes assertionResponse. For enrollment, the same steps take attestationOptions and attestationResponse, and the enrollment ceremony.start additionally requires mediation ("conditional", "optional" or "required").
3. Subflow types
Helper signatures can move between releases. Read the installed package’s type definitions before applying a recipe.
Every config accepts
explicitSpecType. There is no autoStart option: create the helper when the attempt should begin. The input-bound helpers additionally accept inputHtmlField for input tracking; it does not control whether the attempt starts. For a subflow type without a helper, such as totp, use the low-level tracker methods trackSubflowStarted(), trackSubflowStepStarted(), trackSubflowStepFinished() and trackSubflowStepError() with the same shape.
3.1 Provide identifier
Tracks when users enter and submit their identifier, typically an email address. Bind the helper to the identifier input so the SDK captures interaction on it. Passkey Conditional UI (autofill) is bound to the same input field, so its events are tracked through this helper’scui steps. Conditional UI is not a passkey-login attempt: it resolves the same identifier-email option as a typed identifier, and a Conditional UI request torn down because the user submitted the identifier is neutral, not an error.
3.2 Passkey login
Tracks passkey sign-in attempts where the WebAuthn ceremony is started explicitly, by a button or by your code. Conditional UI on the identifier field belongs to provide identifier instead.
See section 2 for the button example. Immediate mediation looks the same with
explicitSpecType: "passkey-immediate" and mediation: "immediate" on the WebAuthn call. If the ceremony fails, continue with your regular login and start Conditional UI on the identifier field.
3.3 Passkey enrollment
Tracks passkey registration, for example after login or during sign-up.
The
ceremony.start data requires mediation, which tells Observe how the credential manager was invoked.
conditional-auto-manual, run the same steps with mediation: "conditional" first. If the conditional ceremony errors, a regular prompt is a new sequence of step events on the same helper with mediation: "required".
explicitSpecType also accepts conditional for a silent conditional-create attempt and auto for an automatically triggered regular prompt. Use a new operation for each attempt when using these values; keep manual for a user-triggered attempt. The combined values above remain supported for existing integrations that model one offer across its fallbacks.
The enrollment helper’s configuration also accepts ignoreAsInteraction: true for a programmatic start without a user gesture or visible surface, such as a conditional attempt. It marks the emitted start as non-interactive; it does not suppress the operation’s later step events. subflowStart() and trigger() data also accept this flag. Do not apply it to a user-triggered start.
3.4 Password login
Tracks password-based login attempts. Bind the helper to the password input so the SDK captures interaction on it. Create it when the password field renders.
Typed error codes:
invalid_password, user_not_found, account_locked.
When identifier and password share one screen, Conditional UI is bound to that screen too. This helper therefore carries the same cui steps as provide identifier. Conditional UI fired through it resolves the passkey-login-cui option.
3.5 Password enrollment
Tracks password creation during sign-up (password-set) or a password reset during recovery (password-reset). Bind it to the new-password input. Typed error code: requirements_not_fulfilled.
3.6 Email OTP and SMS OTP
Track one-time-password verification. Steps:send (email only), postResponse for the code the user submitted, resend when a new code is requested. Spec types distinguish the login variant from the enrollment variant, for example email verification during sign-up.
3.7 Email link
Tracks sign-in or verification through a link sent by email. Steps:send, postResponse for the token from the clicked link, resend.
Email links often continue in a different tab, browser or device. Pass the same crossEnvironmentTransactionID in the user reference of the send step and the later postResponse step so both sides are correlated. Generate one or reuse an existing transaction ID from your system. It does not replace userId, which you still provide as soon as it is known. See the recovery example.
3.8 Social login
Tracks authentication with social identity providers. Steps:getRedirectUrl when your app initiates the OAuth redirect, exchangeCode when the user returns and the code is exchanged. exchangeCode is the outcome-bearing step. Spec types: pre-identifier when the button is shown before any identifier, post-identifier when it is offered after one.
3.9 Provide data
Covers form fields that request user data but map to no deeper subflow concept, such as name, address, birth date or bank details. One screen, one provide-data subflow bound to the most important field. PassfieldName on postResponse.start for the semantic name of what is collected. The spec type names the surrounding flow.
3.10 App confirmation
Tracks a login approved in another app, for example by scanning a QR code.ceremony is the approval and the outcome-bearing step, with typed error codes declined and expired. retry covers a re-issued code.
3.11 Captcha
Tracks a captcha challenge inside an authentication journey. Steps:ceremony for the challenge, postResponse for the server-side check. Spec types visible and invisible.