Forms SDK

Dos and Don'ts

The rules that keep SDK forms correct

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

  • 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.
  • 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.
  • If you call 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

  • 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

  • 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

  • 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.
  • Walk your own branching logic in a real browser, both branches plus a boundary case; no Surface validator sees business logic in your code.

Ready to Get Started?

See it on your own site, or open the app and build the first form.

Need Help?

Setup problems, blocked domains and CRM push failures live in the Help Center.

Feature Requests?

Tell us what is missing. We would rather hear it than guess.

On this page