Skip to content

Forms

Production-ready

The Forms capability is Lukeflow's end-to-end form platform: a headless design/render/validate library (@lukeflow/form-*), a versioned form-management data layer inside the core engine, and public embed/respond surfaces that turn a browser submission into a running BPMN process. It spans authoring (draft → check-in → sign-off → publish), inbound collection (embed → submit → durable process start), and outbound delivery (prefill → send → OTP-gated respond), all tenant-scoped and driven by transactional outboxes so no submission or process-start intent is ever lost.

Components at a glance

Data model

All tables live in the core engine's capability schema (com.luke.engine.capability.form), tenant-scoped by tenantId.

Entity (table)Key fieldsPurpose
FormDefinition (luke_form_definitions)id, @Version version, code (FM-XXXX-DDMMMYY, unique per tenant), name, kind (INBOUND/OUTBOUND), submissionHandling, outboundRolesJson, status (DRAFT/PUBLISHED/RETIRED), publishedVersion, draftSchema, allowedEmbedOrigins, embedKeyVersion, lockedBy/lockedAt, deletedAt, lastTestedAtThe versioned, tenant-scoped form template. Editable working draft in draftSchema; publishedVersion is what consumers resolve. JPA @Version gives optimistic locking (concurrent draft saves → 409).
FormVersion (luke_form_versions)id, formId, version (unique per form), schema (immutable), checkedInBy/checkedInAt, signedOffAt/signedOffByAn immutable checked-in snapshot — the artifact a renderer/process resolves and never changes underneath them. Publish is gated on signedOffAt being set.
FormInstance (luke_form_instances)id, token (unique, opaque URL handle), definitionCode + version (pinned), state, prefill/data/recipient/context (JSON maps), expiresAt, submittedAtA concrete runtime occurrence: hosted submission, prefilled invitation, or task-bound fill. context links back to processInstanceId/taskId/businessKey.
FormSubmissionOutbox (luke_form_submission_outbox)id, businessKey (= instance id, unique/idempotent), formInstanceId, processBusinessKey (SM-…), formDataJson, formMetaJson, status (QUEUED/PUBLISHED/FAILED), processInstanceId, retryCountTransactional outbox for submit → Camunda process start. Written in the same tx as the SUBMITTED state; drained off-thread.
FormEventOutbox (luke_form_event_outbox)id, eventType (lower-cased state, e.g. submitted), formCode, instanceId, payloadJson, state (QUEUED/SENT/SKIPPED/FAILED), retryCountTransactional outbox for the forms → workflow event rail. A lifecycle change enqueues a QUEUED row; the correlator starts subscribed workflows / advances waiting message catches.
FormAuditEvent (luke_form_audit)id, formId, action (created/checked_in/tested/published/archived/…), detail, actor, atImmutable activity feed for a definition's lifecycle.
FormRecipientOtp (luke_form_recipient_otp)id, instanceId (unique), codeHash, codeSalt, attempts, expiresAtOne salted-SHA-256 OTP challenge for an outbound recipient; attempt-capped, expiring, never stored in the clear.

Design-time flow

Function / endpointWhat it does
FormDefinitionController.create (POST /api/form-definitions)Mints a new definition with a generated code and kind (INBOUND/OUTBOUND), status DRAFT.
FormDefinitionController.checkout (POST /{id}/checkout)Acquires the advisory edit lock (lockedBy/lockedAt); stale locks can be taken over.
FormDefinitionController.saveDraft (PUT /{id}/draft)Persists the editable draftSchema (coltorapps-style JSON). @Version guards concurrent saves.
FormDefinitionController.checkIn (POST /{id}/versions)Snapshots the draft into a new immutable FormVersion (version = max+1), releases the lock; status untouched.
FormDefinitionController.signOff (POST /{id}/sign-off)Marks the latest version signedOffAt/signedOffBy and mirrors lastTestedAt for the "Tested" badge.
FormDefinitionController.publish (POST /{id}/versions/{v}/publish)Promotes a version to live — gated: rejects with 409 unless the version is signed off; sets publishedVersion + status PUBLISHED.
FormDefinitionController.restoreVersion / retire / unretire / cloneRestore an old version to draft, retire/unretire the definition, or clone it.
FormDefinitionController.release / discardRelease the lock, or discard draft changes back to the last checked-in version.

Runtime flow (inbound embed → submit → task complete)

Function / endpointWhat it does
createEmbed (form-embed)Injects a sandboxed iframe pointed at /embed/{token}, wires postMessage auto-resize (connectEmbedFrame), nonce-guards the channel; autoEmbed() scans embed script tags.
EmbedPageController.page (GET /embed/{token})Serves the engine-hosted embed HTML shell with per-form frame-ancestors CSP (from allowedEmbedOrigins, sanitized via FrameAncestors).
FormEmbedController.render (GET /api/public/embed/{token})Resolves the token (EmbedFormResolver) to the published FormVersion.schema + title; the signed embed token carries embedKeyVersion for revocation.
FormEmbedController.submit (POST /api/public/embed/{token}/submit)Per-IP + per-token rate limits, Honeypot.tripped bot trap, SubmissionValidator.clean server backstop, then creates a SUBMITTED FormInstance and calls FormSubmissionService.submit.
SubmissionValidator.cleanStrips unknown fields against the schema, enforces required, bounds payload size (413), removes C0/C1 control chars — the public endpoint cannot trust the client.
FormSubmissionService.submitIn ONE transaction: sets state SUBMITTED, merges data, writes a QUEUED FormSubmissionOutbox row (business key = instance id → idempotent), marks processStartStatus=QUEUED on context.
FormSubmissionOutboxConsumer.drain@Scheduled poller (luke.forms.outbox-poll-ms, default 2s) starts the Camunda intake process via InternalProcessService.start, mirrors attachments, flips the row PUBLISHED/FAILED with retry.
FormEventPublisher.emitEnqueues a FormEventOutbox row (lower-cased state as eventType) so subscribed workflows start / waiting message catches advance.
FormInstanceWriteBackDelegate.executeBPMN JavaDelegate at the intake process's write-back task — marks the originating instance PROCESSED and records processInstanceId; failures are logged, never rethrown.

Outbound forms

Outbound definitions (kind = OUTBOUND) are never embeddable — a preparer prefills fields and sends the form to a named recipient. Per-field roles live in FormDefinition.outboundRolesJson (a { fieldKey → role } map where role ∈ PREPARER | RECIPIENT | EITHER), configured via PUT /{id}/outbound-config.

  • OutboundSendController.send (POST /api/form-definitions/{id}/send) → OutboundSendService.send: creates a prefilled FormInstance with recipient identity, returns the opaque token + recipient link, and best-effort emails the recipient ("Please complete: …") — the send never fails on email.
  • Recipient access is OTP-gated: PublicFormInstanceService.requestOtp mails a FormRecipientOtp code; verify marks the instance OPENED and mints a short-lived access token (RecipientAccessTokens); render/save/submit require it. The public API (PublicFormInstanceController at /api/public/form-instances/**) never leaks whether a token exists.

Component reference

ComponentTypeResponsibilityKey methods
FormDefinitionControllercontrollerDesign-time CRUD + lifecycle over definitions/versionscreate, saveDraft, checkIn, signOff, publish, checkout, clone, auditTrail
FormInstanceControllercontrollerAuthenticated instance management (internal)create, list, submit, retryProcess, setState, send, cancel, processed
FormEmbedControllercontroller (public)Inbound embed render + submit with abuse guardsrender, submit, rateLimit
PublicFormInstanceControllercontroller (public)OTP-gated recipient respond flowotp, verify, GET/PATCH {token}, submit
OutboundSendControllercontrollerSend an outbound form to a recipientsend
EmbedPageController / RespondPageControllercontrollerServe engine-hosted /embed and /respond HTML shells with per-form CSPpage
FormSubmissionServiceserviceDurable submit → process-start bridge (outbox)submit, reEnqueue, enqueue
FormSubmissionOutboxConsumerserviceDrains submission outbox, starts Camunda processdrain, process
PublicFormInstanceServiceserviceRecipient OTP/verify/render/save/submitrequestOtp, verify, render, save, submit
OutboundSendServiceserviceCreate prefilled instance + email linksend
SubmissionValidatorutilServer-side validate/clean of untrusted submissionsclean
FormEventPublisherserviceEmit lifecycle events onto the workflow railemit
EmbedFormResolver / EmbedTokens / FrameAncestors / HoneypotsupportToken resolution, embed-key revocation, CSP sanitization, bot trapresolve, tripped
FormInstanceExpirySweeperschedulerSweep expired open instances to EXPIRED@Scheduled sweep
createFormEnginelib fn (form-core)Headless form engine: dependency graph, visibility/required, validation, defaults, gridsinit, update, validate, getState, serialize
FormRenderer / useFormEnginereact comp (form-react)Renders a schema against the engine; controls, i18n, theme, sanitized HTMLrender controls, MinionProvider
FormBuilder / useFormBuilderreact comp (form-builder)Drag-drop authoring: palette, canvas, settings, problems, previewFormBuilderHandle, SettingsPanel
createEmbed / connectEmbedFramelib fn (form-embed)Host-page iframe embed SDK + auto-resize bridgecreateEmbed, autoEmbed
FormBuilderPage / FormInbox / FormInstancesListreact page (consumer-ui)Authoring UI + inbox/instance managementlifecycle actions, trace panel

Endpoints

MethodPathPurposeAuth model
POST/api/form-definitionsCreate a definitionTenant (X-Tenant-Id)
GET/api/form-definitions · /{id} · /by-code/{code}List / fetch definitionsTenant
PUT/api/form-definitions/{id}/draftAutosave working draftTenant + X-User-Id
POST/api/form-definitions/{id}/versionsCheck in an immutable versionTenant + user
POST/api/form-definitions/{id}/sign-offSign off latest version (tested)Tenant + user
POST/api/form-definitions/{id}/versions/{v}/publishPublish (gated on sign-off)Tenant + user
POST/api/form-definitions/{id}/checkout · /release · /discardEdit-lock lifecycleTenant + user
PUT/api/form-definitions/{id}/outbound-configSet per-field preparer/recipient rolesTenant
POST/api/form-definitions/{id}/sendSend an outbound form to a recipientTenant
GET/api/form-definitions/{id}/embed-token · POST /embed-token/rotateFetch / revoke embed tokenTenant
POST/api/form-instances · GET / · PATCH /{id} · /{id}/submitInternal instance managementTenant
GET/embed/{token}Engine-hosted embed page (per-form CSP)Public (origin-gated)
GET/api/public/embed/{token}Public render of published schemaPublic (token)
POST/api/public/embed/{token}/submitInbound submission → process startPublic (token, rate-limited)
GET/respond/{token}Engine-hosted respond pagePublic
POST/api/public/form-instances/{token}/otp · /verifyRecipient OTP challenge / verifyPublic (token)
GET/PATCH/POST/api/public/form-instances/{token} · /submitRecipient render / autosave / submitPublic (token + access token)

Status & gaps

Production-ready across authoring, inbound embed, and outbound respond, all backed by transactional outboxes and per-tenant isolation. See the Completeness Scorecard for the fleet-wide status view.

Lukeflow Manual · documentation snapshot July 2026