- Decisions, the checkpoints where a user or your system picks a method or a route
- Subflows, one concrete auth method attempt each
- Nested flows, a complete journey inside another one, such as a recovery started from the login page
1. Flow names
Use one of these predefined flow names when you send flow events:| Flow name | Outcome states | Description |
|---|---|---|
login | complete or incomplete | Existing user authentication. Typically contains the pre-identifier and post-identifier decisions, subflows such as provide-identifier, passkey-login, password-login, social-login, email-otp or email-link, and possibly a nested signup or recovery. |
signup | complete or incomplete | New user registration. Similar subflows to login, usually with the enrollment spec types, and possibly a nested recovery. |
recovery | complete or incomplete | Regaining access to an account, for example a password reset. Usually nested inside a login flow. |
enrollment | complete, skipped, visible-auto-skip, invisible or incomplete | Authenticator enrollment, for example a passkey setup prompted after login. Typically contains an enrollment-user decision and a passkey-enrollment subflow. |
2. Flow events
2.1 flowStarted()
Emit flowStarted() at the flow’s own entry screens: the login page, the sign-up page, the “forgot password” screen, the enrollment prompt. Use flowName when the flow is known from the start.
Use flowNames when several flow names are possible at entry. This is common for identifier-first journeys where login and sign-up share one form: list both, optionally with defaultFlowName to indicate which one Corbado Observe should assume if the journey is never resolved.
Set touchpoint to describe where the flow was initiated. The same flow started from checkout behaves very differently from the same flow started from the account page, and the funnels are segmented by it.
If the user is already known when the flow starts, which is normal for enrollment, include userId in flowStarted().
flowStarted(data, tags?, experiments?, contexts?) accepts structured context as its fourth argument. Context is written to the event’s contexts field without redaction; use it for non-sensitive technical metadata, and use tags for analytical dimensions. Omit unused arguments with undefined.
2.2 flowDecided()
Emit flowDecided() when a journey that started with flowNames resolves to one concrete flow, typically after your backend has checked whether the identifier belongs to an existing user. If it is emitted more than once, the last flow name wins. A flow name provided by a later flowFinished() also wins.
2.3 flowFinished()
Emit flowFinished() when the flow reaches its explicit end state. For login, that is the moment the user is authenticated. For sign-up, the moment the account exists and the user is authenticated. For recovery, the moment access is regained. For enrollment, the moment the authenticator is set up.
Include userId and identifier so the completed flow links to a user.
Do not model non-completion. A flow the user abandons is classified as incomplete from the absence of a flowFinished(). The one exception is an explicit skip, which is semantically different from abandoning: use explicitOutcome.
A skip carries the user reference whenever identity is already known.
2.4 flowAutoFinished()
Only login and signup can contain nested flows. A flow that itself establishes the session, such as a sign-up or a recovery started from the login page, nests inside login. When the nested flow’s terminal fires, finish it with flowFinished() and complete the parent with flowAutoFinished():
flowAutoFinished() without an open parent is dropped silently. A flow that runs after the session already exists, typically an enrollment prompted after login, is not nested. It is a sibling flow started after the login finished, with its own flowStarted() and flowFinished().
When the user leaves a nested flow without finishing it, close it with flowFinished({ flowName, explicitOutcome: "skipped" }). That records the right outcome and keeps the parent’s subsequent events out of the nested flow.
2.5 flowReset()
flowReset() exists for an explicit restart by the user, for example navigating back to the start of a login. It is rarely needed: whether a user restarted is inferable later from revisited decisions and subflows. Do not emit it just to be tidy.
3. Ordering
Ordering requirements are causal, not temporal:flowStarted()before any event of that flow. AflowFinished()orflowDecided()without an open flow invalidates the whole session’s classification.- When a screen renders, the decision
startedcomes before the operation helpers are created. - Settle the previous screen before opening the next: a navigational decision
finishedprecedes the next screen’s decisionstarted.
explicitTimestamp on steps and decisions to back-date an event when the semantic moment precedes the tracking call.
A flow carried across page loads also needs the same Observe session. The SDK checks inactivity at initialization, so an expired session can split a cross-load flow. Ensure the project’s inactivity window spans the journey and initialize the same project on each page. See session lifetime.
4. Tags on flows
Flows are the natural carrier for tags. Configuration known at entry, such as product, variant or device class, goes onflowStarted(). Values only known on success go on flowFinished(). The last value per key wins across a flow’s events. Never put identity into tags.
5. Conversions
conversion() records a business conversion outside authentication, such as a completed purchase, so that authentication outcomes can be related to what happened next:
6. Examples
In a real application, flow tracking is spread across pages, components, server actions and callback routes. The examples keep the relevant snippets together to show the complete sequence for each journey.6.1 Recovery with automatic login
A login flow is started, the user cannot log in and switches to recovery. After completing recovery through an email link and setting a new password, the user is logged in automatically. Recovery nests inside the surrounding login. When recovery completes, finishrecovery explicitly and complete login with flowAutoFinished(). Because the email link can be opened in a different browser or device, pass a stable crossEnvironmentTransactionID from the send step to the verification step.
6.2 Recovery without automatic login
Same journey, but the user is not logged in after the reset and has to log in again with the new password. Recovery is still nested inside login. The difference is thatlogin stays open after the nested recovery finished and contains a further password-login subflow before it finishes explicitly.