@inploi/forms
Visual form renderer for inploi application flows. It takes the same flow JSON that @inploi/plugin-chatbot renders sequentially and presents it as a classic, scrollable form. Forms can be rendered inline with the page or opened in a drawer, and are themed through the standard appearance options.
Deprecated (October 2026): use the Candidate widget's form mode.
@inploi/formsis superseded and will receive no new features. Form-style applications are now delivered through the unified candidate widget (@inploi/plugin-candidate, Apply layoutform), where conversational stays the default. For a new integration, use that.The package stays in this repository and on npm as a design reference, and its CDN files stay up. It is still wired into Studio's SDK Form block and Form button action, which a few demo pages use, so it is not safe to remove yet. It has never been sold, and no customer domain was seen loading it in the Worker logs for the week to 28 Oct 2026 (the logs only reach back about 7 days).
Recent changes here (raw-error copy in PROD-7504, and shared-engine alignment in PROD-7448) came along with work on the shared flows engine and the widget. They aren't evidence of customer impact. Nothing else is fixed in place here: improvements go into the widget's form mode. The risk of its frame running on
sdk.inploi.comis accepted while it stays up, and is revisited if a non-internal tenant starts submitting through it (PROD-7342 ADR, "Forms decision").
Architecture
- Host plugin (
forms.ts) creates and supervises an iframe, manages drawer open/close, scroll lock, theme propagation, submit transport and Talent Bank duplicate checks. It follows the same iframe scaffolding pattern as@inploi/plugin-kin. - Frame runtime (
forms.runtime.tsx) is a Preact app rendered inside the iframe. It owns form state, validation, draft persistence, user interactions and the host RPC (submit, duplicate-check and address lookuprequestIdcorrelation, inlined — no separate bridge module). - Flow projection (
form.projector.ts) walks the raw flow graph and produces renderable items plus a terminal state. It evaluatesif-block, followsjump, appliesadd-submission, collects submit chains, exposes readycheck-duplicatecandidates, and gathers post-submit text/image/link content. - Shared logic is consumed from
@inploi/flowsfor validation-adjacent flow helpers, conditions, interpolation, submit-response shapes, session keys and duplicate-check request building.
Forms is intentionally a projection-first renderer with a small forms executor, not a copy of the chatbot service interpreter.
- Component library (
src/library.ts): the unified candidate widget's form mode (PROD-7448) reuses Forms'Calendar,Comboboxand floating primitives. It bundles them from source, so they share its Preact, and styles them through their markup (roles, aria state,data-active). The candidate widget draws its form from the conversation's engine (@inploi/flows/form), not fromform.projector.ts.library.tsis workspace-internal and is not a published export. Forms also bundles identical icons from Candidate’s isolated kit through a workspace-only source alias.
Public API
import { formsPlugin } from '@inploi/forms';
const forms = formsPlugin({ appearance: { placement: 'drawer' } });
await forms.open({ flow, title: 'Apply now', context: { jobId: '...' } });
Placement
formsPlugin({
appearance: { placement: 'embedded' },
container: '#application-form',
});
drawer is the default placement. embedded mounts the iframe into the provided container and resizes it to fit the form content.
Submit
By default, the host plugin submits to /flow/submit using the SDK apiClient. Consumers can override this with onSubmit:
const forms = formsPlugin({
onSubmit: async payload => {
await submitApplication(payload);
return { success: true };
},
});
FormsSubmitPayload contains only the public submit data: integrationId, optional job, and filtered submissions.
See forms.protocol.ts for the full set of types (OpenFormsParams, FormsAppearance, FormsSubmitPayload, ...).
Runtime Behaviour
- Drafts are persisted by flow identity, flow keys and context hash. Closing, minimising, failed submit and successful submit preserve the draft. The header reset action is the explicit way to start over.
- A candidate who opens a form with a saved draft that has an answer and was never submitted (the first time it is opened since the frame loaded) sees a banner above the form: "You have an unfinished application for this role. Continue where you left off?", with Continue (keep the answers) and Start again (clear the draft, as the header reset does). It sits in a live region that is always on the page, so its arrival is announced. A drawer moves focus to Continue; an embedded form never takes focus from the host page. Answering a question also dismisses it. A successful submit writes a
submitted:<state key>marker to the same store, and the header reset or Start again removes it, so a submitted application is never offered for resuming (PROD-7338). Drafts live in this browser's IndexedDB only: another device or browser, a private window, cleared site data or Safari's seven-day storage expiry starts afresh. - Submit is disabled while required fields are missing, values are invalid, a duplicate check is pending, a submit is already in progress, or a submit cooldown is running.
- Sequential
integration-application-submitnodes are executed as a chain —FormAppawaits each step in order and short-circuits on the first failure or redirect. Redirect responses show the provider message and replace the footer submit button with a redirect CTA that opens the ATS in a new tab. There is no persistent submit cursor: before a successful submit, any field edit resets submit state, and the next click reruns the whole chain from step 0 (relies on integration idempotency). - A submit the API settles as a duplicate application, a closed role or an unknown verdict (the structured
error.datafrom/flow/submit) shows our own informational message instead of the provider's text and the error tone. Submit stays available after these; only a success removes it. A duplicate within an hour of this candidate's own failed attempt on the same application most likely is that attempt landing, so it showssubmission_duplicate_earlier_attemptand FLOW_SUBMIT recordsoutcome: 'duplicate_after_own_attempt'. The host keeps that attempt history for the page's life, not across reloads. - Recovery after other failures follows the API's
error.data(PROD-6846), decoded host-side with the chatbot's shared helpers from@inploi/flows/utils. A failure before the provider's submit step that the API marksretryablewith verdictnot_submittedshowssubmission_retryable. A failure markedretryable: falsekeeps the API's message and holds submit until the candidate changes an answer. An unreadable document (code: 'unreadable_document'with afield) clears that file answer, putssubmission_unreadable_documenton the file input and starts no wait. A thrown submit with no outcome to read (lost connection, gateway timeout, unreadable body, 5xx without an integration response) is an unconfirmed receipt and showssubmission_unknown. - A failure the API labels
reason: provider_maintenance(a provider's scheduled maintenance window, api#1713, PROD-7336) showssubmission_provider_maintenancewith the window's end (maintenance_until) in the candidate's local time, orsubmission_provider_maintenance_laterwhen there is no usable end. Submit waits until that end (at most six hours), shown as "Submit will be available again at {time}." An unknown verdict inside the window may have reached the provider, so it keepssubmission_unknown, though it still waits for the window to end. - Every other unsuccessful submit starts a cooldown with a countdown: consecutive throttles and temporary outages wait 20 s, 40 s, then 60 s (or the provider's longer Retry-After, capped at 120 s); anything else waits 30 s. The cooldown survives answer changes and the header reset, and is cleared when a different flow or job opens.
- These fields are optional on
forms:submit-result, so a CDN frame behind an older host behaves as before, and an older frame ignores them. - A submit the API accepts for later delivery (HTTP 202 with
integration_response.data.outcome: 'pending') is a success: it shows the API'sdata.message, orsubmission_pendingwhen there is none, in place of the flow's success message, and FLOW_SUBMIT recordsoutcome: 'pending'. In a submit chain it continues to the next step like any success, and the latest pending message is kept when a later step plainly succeeds. - Post-submit
text,imageandlinknodes are rendered after a successful final submit. - A successful final submit locks the inputs: they are wrapped in a disabled
<fieldset>, so every control becomes non-editable and visibly muted, while labels, descriptions, informational nodes and links stay fully legible and clickable. The footer CTA and post-submit content remain interactive; the header reset clears the lock (and the draft). end-flowbefore submit is treated as a terminal dead-end and hides the submit button.check-duplicateruns through the host-side Talent Bank client and writes the result back as an integration submission for downstream conditions.question-addresssupports manual entry plus host-side Mapbox suggestions. The regular address fields stay visible; selecting a suggestion fills them and users can still edit manually. When the question is required, every mapped part is required except line 2, which is required only when the node carriesline2Required: true(the api sets it for ATSs that refuse a blank line 2); a required part holding only spaces counts as missing.integration-workflow-getis expected to be materialized by the backend before runtime; if it reaches forms, the form fails closed instead of skipping integration-specific fields.