1. Three passes
Work the mapping in three passes, global concerns first. Mistakes in early passes cost the most. Later passes are local problems with a small blast radius that you can solve pragmatically.1
Flow boundaries
For every flow, find the single best signal for when it starts, when it finishes successfully and, where your product has one, when it is skipped. Do this for all flows, top-level and nested, before anything else. The result of this pass is an exact definition of what is tracked. Precision matters most here: a
flowFinished() without a matching open flow invalidates the whole session’s classification.2
Decision structure
Assign every screen of the journey to a decision name, then find the simplest way to determine the selectable option set per screen. Look for one mechanism that yields options globally if your app offers it, for example a server response that already lists the rendered choices.
3
Subflows
Fill in one auth method attempt at a time. What matters is creating the operation helper at the right moment, then mapping your app’s signals onto the helper’s steps.
2. Flow boundaries
One declared opener per flow.flowStarted() fires at the flow’s own entry screens only. Every other handler drops its signals when no flow is open instead of opening one lazily. Never re-announce a parent flow from a nested page “to make sure it is open”: the classifier reads a repeated start of an outer flow as a restart and closes everything nested under it as incomplete.
Finish only on success or explicit skip. Do not model non-completion on the client. Incompleteness is classified from the absence of a flowFinished(). Skipping is the one exception, because it is semantically different from abandoning: send flowFinished({ flowName, explicitOutcome: "skipped" }) for “continue as guest” or “not now”, carrying the user reference whenever identity is already known.
Nesting versus chaining. 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 own terminal fires, complete the parent with flowAutoFinished(). A flow that runs after the session already exists, typically a passkey enrollment prompted after login, is chained: a sibling flow started after the login finished, never nested.
Events attribute to the innermost open flow. When the user leaves a nested flow without finishing it, close it explicitly with explicitOutcome: "skipped", for example when entering sign-up abandons an open recovery. That records the right outcome and keeps the parent’s subsequent events out of the nested flow.
Ambiguous entry. On a combined login and sign-up form, start with flowNames: ["login", "signup"] and resolve with flowDecided() once your backend knows whether the identifier exists.
See Flows for the event reference.
3. Decision structure
A decision name is a checkpoint of the journey: a semantic unit that takes a successful auth method to pass and navigation to leave. Several screens usually map to one name (progressive disclosure, explanatory screens, switching between verification methods). Typical names arepre-identifier for everything before the identifier is submitted, post-identifier for the methods shown after it and 2fa.
A screen’s option set mixes two kinds of options:
Realistic logins have many navigational options. They are the adaptive, per-application part of the model, not an edge case. Name them for reuse across decisions.
A screen can lead into the same subflow through several controls: two buttons that run the same ceremony, a primary tile plus an “other methods” list, a prefilled versus a typed identifier. That is one option and one subflow, because only one option string can resolve. Express the difference through an explicit spec type where the taxonomy has one, otherwise carry it as a tag or accept the loss and document it.
See Decisions for presentation rules and the option strings each subflow resolves.
4. Subflows
A subflow is one auth method attempt. Creating its operation helper emits the attempt’s start, so when you create the helper is the modeling question:- Input-bound methods (password, OTP, identifier, provide-data) start when the input renders. The field itself is the attempt surface, and the helper captures interaction on it.
- Action-bound methods (passkey button, social button, app confirmation) start on the action, not when the button becomes visible. A helper created for a button nobody pressed produces an attempt without interaction: not counted for most types, counted as an incomplete attempt for passkeys.
postResponse for almost every subflow. Track that step always. For passkeys, also track the ceremony step: it powers all passkey analytics and nothing in it is recoverable from the server response alone.
Instrumentation is additive by default. It observes your app’s existing lifecycle and adds no cancellation, timers or navigation rules of its own.
See Subflows for the helper reference.
5. Identity and tags
PassuserId and identifier as soon as identity is known: on the step that resolved it, and at minimum on flowFinished(). A skip carries identity too when the user is already known.
Tags ride flows. Configuration known at entry (product, variant, device class) goes on flowStarted(). Values only known on success go on flowFinished(). Do not re-fire the opener from reactive configuration such as store hydration or feature flags just to refresh its tags.
6. Journey catalog
The event series a correct integration produces for common journeys. Options are abbreviated andspec stands for explicitSpecType.
6.1 Identifier-first login, back, then sign-up
finished for the method choices, no subflow finishes and no explicit abandon events. Had the user left mid-journey, the missing finishes would have classified it. The second pre-identifier decision is deliberately a second occurrence: the user genuinely revisited that checkpoint. The password attempt that rendered but was never submitted has no step and is not counted; the back finish resolves the post-identifier occurrence it was rendered in. The step names differ per helper (pi-post-response for the identifier, post-response elsewhere); the helpers emit them, you never type them.
6.2 Direct sign-up, then a skipped enrollment prompt
login flow to auto-finish here; flowAutoFinished() belongs only to a parent that was actually started (6.1, 6.4). The enrollment starts after the sign-up finished, so it is a sibling.
6.3 Combined form resolved to login
6.4 Recovery nested in login with automatic login
recovery flow and calls setCrossEnvironmentTransactionId() with the same ID, which is what joins the two sessions. It has no open login to auto-finish.
7. What the classifier merges, repairs or drops
8. Next steps
Flows
Flow events, nesting and outcomes.
Decisions
Presentation rules and option strings.
Subflows
Operation helpers, steps and spec types.
Use with AI agents
Let an agent apply this method to your codebase.