# Dos and Don'ts



The short version: copy IDs and shapes from the binding map, let the SDK own what it owns, and verify by reading a response back.

## Boot [#boot]

* Take `apiBaseUrl` from `get_sdk_binding_map`; a wrong host 404s.
* Author every step container after the first with `hidden`, or they flash stacked during boot.
* Remove `preview: true` and `debug: true` before deploying; shipped preview mode silently stores nothing.
* One engine per page life: `destroy()` on teardown, never `init` again without it (duplicate responses).

## Binding [#binding]

* Copy every `questionId`, `stepId`, option key, and `answerShape` from the binding map; a typo is silent data loss.
* Set `data-question-type` on every choice group container, each control's `value` exactly the record's option key. No `data-field-name` on choice options.
* Give each control of a multi-field component an explicit `data-field-name` with the record's exact keys (`workEmailAddress`, `phoneNumber`); the set is closed, don't guess `email` or `phone`.
* Leave scheduler and file-upload questions unbound; their answers are written in code, and `validate_form_html` knows they are exempt.
* In a framework, keep bound inputs uncontrolled and mounted; the listener attaches once at `init`, so no conditional rendering, `key` remounts, or controlled values.

## Navigation [#navigation]

* Let `next()` and `goToStep()` switch steps instantly; don't disable the continue button for a partial save or insert async work before the step change unless the owner asked for a gate.
* Calling `submit()` without a marker? Disable your own button until it resolves. With the marker, `surface-busy` handles it.
* Submit button on the last question step; both endings get a `data-step-id` container. The SDK reveals endings itself.
* Declare `data-surface-nav="js"` when navigation is code-driven; don't add fake buttons to quiet the validator.
* Emit `hidden` as a constant in framework markup, never derived from state.

## Data and policies [#data-and-policies]

* Ask the form owner what an invalid email means (default: annotate only); never gate navigation on `validateEmail` on your own initiative. Verdicts fail open on vendor outages.
* Enforce required fields and selection limits in your page if the owner wants parity; the SDK never blocks a step.
* Caller-computed values go in `mergeMeta`, visitor input in answers; answers are what workflows and tables read.
* Check `form.capabilities` before promising enrichment, validation, or tracking; the record can declare features the environment does not back.

## Schedulers [#schedulers]

* Use `mountScheduler({ form, container })` and let it persist the booking; don't iframe the hosted booking page, bind the scheduler question in HTML, or hand-wire persistence unless building a custom UI on purpose.
* Mount once as soon as `form` resolves, even into a hidden step container; not on step entry.
* Drive navigation yourself for Clari; it exposes no booking signal.

## Verification [#verification]

* Validate the rendered HTML with `validate_form_html` and fix every blocker; never a hand transcription of framework source.
* Submit one real test response after publishing and read it back, confirming every answer and `meta.surfaceTagStatus: "sdk"`. The read-back is the only end-to-end truth.
* `await form.flush()` before a programmatic page close (test harnesses); ordinary visitors need nothing.
* Re-run `validate_form_html` after every record edit and follow the [editing sequence](/docs/forms-sdk/building-with-ai#editing-an-existing-sdk-form).
* Walk your own branching logic in a real browser, both branches plus a boundary case; no Surface validator sees business logic in your code.
