# Embedded Schedulers



`mountScheduler` reads the record's scheduler configuration, renders a working scheduler, tracks the booking lifecycle, and writes the confirmed booking into the response. It works for every provider native Surface forms support.

## The default path [#the-default-path]

```js
import { mountScheduler } from "@surface-labs/forms-sdk";

const teardown = mountScheduler({
  form, // the SurfaceForms.init handle
  container: document.querySelector("#booking"),
  onEvent(e) {
    if (e.type === "booking_confirmed") form.submit(); // or next()/goToStep
  },
});
```

That is the whole integration: persistence is automatic, and events also arrive on `form.on("scheduler", handler)`. Never bind a scheduler question in HTML, and never iframe the hosted booking page.

<Note>
  Mount once, as soon as `form` resolves, into the step container even while it is still `hidden`. Do not mount on step entry.
</Note>

## Options [#options]

| Option       | Default     | What it does                                                                                                                                                                        |
| ------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `form`       | required    | The `init` handle; config, transport, identity, and persistence come from it.                                                                                                       |
| `container`  | required    | The embed mounts here; teardown removes it.                                                                                                                                         |
| `questionId` | auto        | Only needed with more than one scheduler question (the error names the candidates). `stepId` works too.                                                                             |
| `styled`     | `true`      | Surface provider: injects a stylesheet scoped under `.surface-scheduler` on this container. Override with `--surface-scheduler-*` custom properties; `false` keeps everything bare. |
| `persist`    | `true`      | Auto-persist the booking into the response. `false` still emits events, never writes.                                                                                               |
| `prefill`    | from record | `{ name?, lastName?, email? }` overrides the record's linked answers.                                                                                                               |
| `formatters` | locale      | Surface provider only: `{ month?, date?, time? }` label formatters.                                                                                                                 |
| `calendarId` | auto        | `DynamicScheduler` with multiple calendars: pick one by name. Ambiguous routing throws.                                                                                             |
| `onEvent`    | none        | Same events as `form.on("scheduler", ...)`.                                                                                                                                         |
| `onComplete` | none        | Fires alongside `booking_confirmed`. Not needed for persistence.                                                                                                                    |

The returned teardown is idempotent and independent of the engine; call it before or after `form.destroy()`.

## Lifecycle events [#lifecycle-events]

Every event carries `{ questionId, stepId, provider }` plus:

```ts
| { type: "viewed" }
| { type: "date_selected"; date? }        // "YYYY-MM-DD"
| { type: "slot_selected"; slot? }        // { start, end? }
| { type: "form_opened" }
| { type: "booking_confirmed"; booking }  // SchedulerBookingSummary
| { type: "booking_cancelled"; reason? }
```

`SchedulerBookingSummary` is `{ provider, uri, meetingTime?, meetingEndTime?, inviteeName?, guestEmails?, ownerEmails?, joinLink?, raw? }`; `uri` is the provider's native booking ID, `raw` the untouched payload.

## Supported providers [#supported-providers]

| Provider    | Embed                                 | Booking detection                                                           |
| ----------- | ------------------------------------- | --------------------------------------------------------------------------- |
| Surface     | SDK booking widget, styled by default | Widget booking, full sub-step lifecycle                                     |
| Calendly    | iframe                                | postMessage, plus a server-poll fallback that also reports cancellations    |
| Cal.com     | Vendor script                         | Booking event                                                               |
| SavvyCal    | Vendor script                         | Booking event                                                               |
| Chili Piper | iframe                                | postMessage                                                                 |
| HubSpot     | Vendor script                         | postMessage                                                                 |
| RevenueHero | Vendor script                         | postMessage                                                                 |
| Zoom        | iframe                                | postMessage, plus a server poll that backfills host email, times, join link |
| Reclaim.ai  | iframe                                | Server poll only                                                            |
| Clari       | iframe                                | None; nothing is tracked or persisted, drive navigation yourself            |

All postMessage listeners are strictly origin-checked.

## Persistence [#persistence]

* A confirmed booking is merged into the scheduler question's answer (`eventScheduled`, `uri`, meeting fields) and saved; a finished form is amended instead. Surface bookings also stamp `meta.bookingId` and fire the `meeting_booked` ad conversion trigger.
* A poll-reported cancellation writes `eventCancelled: true` and saves.
* Calendly, Zoom, and Reclaim embeds carry the `responseId` in their URL, so mounting them creates the response if none exists. The Surface widget resolves identity lazily at book time; visitors who never book create nothing.
* With `preview: true`, every write and poll is blocked and the Surface widget books synthetically.

## Prefill [#prefill]

Name and email prefill comes from the record's `linkNameToStep`, `linkLastNameToStep`, and `linkEmailToStep` references to earlier answers. Set them at record creation, or pass `prefill: { name, email }` yourself.

## Custom booking UI [#custom-booking-ui]

Building your own booking interface for a Surface scheduler: mount the unstyled widget directly and write the answer yourself.

```js
import {
  createSchedulerClient,
  createTransport,
  mountBookingWidget,
  toSchedulerAnswerState,
} from "@surface-labs/forms-sdk";

const scheduler = createSchedulerClient({ transport: createTransport({ apiBaseUrl }) });
mountBookingWidget({
  container: document.querySelector("#booking"),
  scheduler,
  eventTypeId: "evt_xxx", // from the record's scheduler content
  identity: () => ({ responseId: form.state().responseId ?? undefined }),
  prefill: { name, email },
  onComplete(booking) {
    form.setAnswer("q_booking", { ...toSchedulerAnswerState(booking), type: "CalendlyScreen" });
    form.submit();
  },
});
```

The widget walks date, time, name and email, booked. It injects no CSS; style its stable `surface-booking-*` class tree. Month, date, and slot elements carry raw values in `data-value`. `toSchedulerAnswerState` emits `type: "DynamicScheduler"`; override `type` to the record's component type as shown.

<Note>
  A persistent "No availability" means the event type really has no bookable slots (host schedule, maximum advance, no connected host). `list_schedulers` with `includeAvailability: true` returns each scheduler's `nextAvailableDate`.
</Note>
