Navigation and Routing
How visitors move through an SDK form: markers, branching, disqualification, and submit semantics
SDK form records carry no routing logic. Branching lives in your codebase; the SDK provides the movement primitives and keeps the response in sync.
Declarative markers
One marker per element; most specific wins:
| Marker | Action |
|---|---|
data-surface-goto-step="<stepId>" | Jump to a step. Forward jumps save; jumping back to a visited step rewinds without a write. |
class="surface-disqualify-button" | End the form as disqualified. |
class="surface-submit-button" | Finish and submit. |
class="surface-next-button" | Advance to the next step in record order. |
A <form> container's native submit event also finishes the form (the SDK calls preventDefault). A goto to an unknown step logs a console warning, never throws.
A static branch is two buttons:
<section data-step-id="step_role">
<label>Your role <input data-question-id="q_role" /></label>
<button type="button" data-surface-goto-step="step_company">I'm a buyer</button>
<button type="button" class="surface-disqualify-button">Just browsing</button>
</section>Code-driven navigation
Dynamic branching calls the handle's methods from your own handlers:
continueButton.addEventListener("click", () => {
const size = form.state().answers["step_company"]?.["q_size"]?.selected;
if (size === "1-10") form.disqualify();
else form.goToStep("step_booking");
});When navigation or submit is code-driven, put data-surface-nav="js" on any element (usually <body>) so validate_form_html does not warn about missing buttons.
Timing semantics
- Steps switch instantly.
next()andgoToStep()resolve on the step change; the partial save runs behind them and reports throughsavedanderror. Do not disable the continue button for a partial save, and do not insert async work before the step change unless the owner asked for a gate (see email validation). - Finishing is the one wait.
submit()anddisqualify()await the finishing write before the ending step shows. The binding layer disables the clicked button and marks the containersurface-busy; callingsubmit()without a marker, disable your own button until it resolves.
Endings
After the terminal write, submit() reveals the step with endStepKind: "thank_you" and disqualify() the "disqualified" one. So: put the submit button on the last question step, and give both endings a container, or the visitor finishes on a blank screen.
Delivery guarantees
- The finishing write is awaited; a failure emits
error. The server never demotes a finished response: a late partial save is merged, not un-finished. - Partial saves never block navigation. The first forward move creates the response (settling
responseId); the rest are fire-and-forget beacons. - Before programmatic teardown (test harness closing the browser),
await form.flush(). Ordinary visitors need nothing; beacons survive page unloads.