# Navigation and Routing



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 [#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:

```html
<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 [#code-driven-navigation]

Dynamic branching calls the handle's methods from your own handlers:

```js
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 [#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](/docs/forms-sdk/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 [#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 [#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.
