Appearance
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 fields | Purpose |
|---|---|---|
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, lastTestedAt | The 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/signedOffBy | An 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, submittedAt | A 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, retryCount | Transactional 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), retryCount | Transactional 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, at | Immutable activity feed for a definition's lifecycle. |
FormRecipientOtp (luke_form_recipient_otp) | id, instanceId (unique), codeHash, codeSalt, attempts, expiresAt | One salted-SHA-256 OTP challenge for an outbound recipient; attempt-capped, expiring, never stored in the clear. |
Design-time flow
| Function / endpoint | What 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 / clone | Restore an old version to draft, retire/unretire the definition, or clone it. |
FormDefinitionController.release / discard | Release the lock, or discard draft changes back to the last checked-in version. |
Runtime flow (inbound embed → submit → task complete)
| Function / endpoint | What 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.clean | Strips unknown fields against the schema, enforces required, bounds payload size (413), removes C0/C1 control chars — the public endpoint cannot trust the client. |
FormSubmissionService.submit | In 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.emit | Enqueues a FormEventOutbox row (lower-cased state as eventType) so subscribed workflows start / waiting message catches advance. |
FormInstanceWriteBackDelegate.execute | BPMN 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 prefilledFormInstancewithrecipientidentity, 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.requestOtpmails aFormRecipientOtpcode;verifymarks the instance OPENED and mints a short-lived access token (RecipientAccessTokens);render/save/submitrequire it. The public API (PublicFormInstanceControllerat/api/public/form-instances/**) never leaks whether a token exists.
Component reference
| Component | Type | Responsibility | Key methods |
|---|---|---|---|
FormDefinitionController | controller | Design-time CRUD + lifecycle over definitions/versions | create, saveDraft, checkIn, signOff, publish, checkout, clone, auditTrail |
FormInstanceController | controller | Authenticated instance management (internal) | create, list, submit, retryProcess, setState, send, cancel, processed |
FormEmbedController | controller (public) | Inbound embed render + submit with abuse guards | render, submit, rateLimit |
PublicFormInstanceController | controller (public) | OTP-gated recipient respond flow | otp, verify, GET/PATCH {token}, submit |
OutboundSendController | controller | Send an outbound form to a recipient | send |
EmbedPageController / RespondPageController | controller | Serve engine-hosted /embed and /respond HTML shells with per-form CSP | page |
FormSubmissionService | service | Durable submit → process-start bridge (outbox) | submit, reEnqueue, enqueue |
FormSubmissionOutboxConsumer | service | Drains submission outbox, starts Camunda process | drain, process |
PublicFormInstanceService | service | Recipient OTP/verify/render/save/submit | requestOtp, verify, render, save, submit |
OutboundSendService | service | Create prefilled instance + email link | send |
SubmissionValidator | util | Server-side validate/clean of untrusted submissions | clean |
FormEventPublisher | service | Emit lifecycle events onto the workflow rail | emit |
EmbedFormResolver / EmbedTokens / FrameAncestors / Honeypot | support | Token resolution, embed-key revocation, CSP sanitization, bot trap | resolve, tripped |
FormInstanceExpirySweeper | scheduler | Sweep expired open instances to EXPIRED | @Scheduled sweep |
createFormEngine | lib fn (form-core) | Headless form engine: dependency graph, visibility/required, validation, defaults, grids | init, update, validate, getState, serialize |
FormRenderer / useFormEngine | react comp (form-react) | Renders a schema against the engine; controls, i18n, theme, sanitized HTML | render controls, MinionProvider |
FormBuilder / useFormBuilder | react comp (form-builder) | Drag-drop authoring: palette, canvas, settings, problems, preview | FormBuilderHandle, SettingsPanel |
createEmbed / connectEmbedFrame | lib fn (form-embed) | Host-page iframe embed SDK + auto-resize bridge | createEmbed, autoEmbed |
FormBuilderPage / FormInbox / FormInstancesList | react page (consumer-ui) | Authoring UI + inbox/instance management | lifecycle actions, trace panel |
Endpoints
| Method | Path | Purpose | Auth model |
|---|---|---|---|
| POST | /api/form-definitions | Create a definition | Tenant (X-Tenant-Id) |
| GET | /api/form-definitions · /{id} · /by-code/{code} | List / fetch definitions | Tenant |
| PUT | /api/form-definitions/{id}/draft | Autosave working draft | Tenant + X-User-Id |
| POST | /api/form-definitions/{id}/versions | Check in an immutable version | Tenant + user |
| POST | /api/form-definitions/{id}/sign-off | Sign off latest version (tested) | Tenant + user |
| POST | /api/form-definitions/{id}/versions/{v}/publish | Publish (gated on sign-off) | Tenant + user |
| POST | /api/form-definitions/{id}/checkout · /release · /discard | Edit-lock lifecycle | Tenant + user |
| PUT | /api/form-definitions/{id}/outbound-config | Set per-field preparer/recipient roles | Tenant |
| POST | /api/form-definitions/{id}/send | Send an outbound form to a recipient | Tenant |
| GET | /api/form-definitions/{id}/embed-token · POST /embed-token/rotate | Fetch / revoke embed token | Tenant |
| POST | /api/form-instances · GET / · PATCH /{id} · /{id}/submit | Internal instance management | Tenant |
| GET | /embed/{token} | Engine-hosted embed page (per-form CSP) | Public (origin-gated) |
| GET | /api/public/embed/{token} | Public render of published schema | Public (token) |
| POST | /api/public/embed/{token}/submit | Inbound submission → process start | Public (token, rate-limited) |
| GET | /respond/{token} | Engine-hosted respond page | Public |
| POST | /api/public/form-instances/{token}/otp · /verify | Recipient OTP challenge / verify | Public (token) |
| GET/PATCH/POST | /api/public/form-instances/{token} · /submit | Recipient render / autosave / submit | Public (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.
- Cross-links: Forms library · Core Engine · Consumer UI