Forms SDK

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:

MarkerAction
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() and goToStep() resolve on the step change; the partial save runs behind them and reports through saved and error. 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() and disqualify() await the finishing write before the ending step shows. The binding layer disables the clicked button and marks the container surface-busy; calling submit() 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.

On this page