Write a Camunda 8 form (.form JSON) with @bpmnkit/core in one script: create, export, edit, validate. Run it with node --experimental-strip-types build.ts (Node 22.6+) or npx tsx build.ts. Never write form JSON by hand.
Minimal form build script: create, export, edit, validate
import { readFileSync, writeFileSync } from "node:fs"
import { Form, exportFormPreserving } from "@bpmnkit/core"
// 1. Create. "review-order" is the form id a userTask or startEvent references as formId.
const form = Form.create("review-order")
.text("## Review order")
.textfield("Customer", "customer", { validate: { required: true } })
.select("Decision", "decision", {
values: [
{ label: "Approve", value: "approve" },
{ label: "Reject", value: "reject" },
],
validate: { required: true },
})
.textarea("Comment", "comment")
.checkbox("Notify customer", "notify")
.build()
// 2. Export.
writeFileSync("review-order.form", Form.export(form))
// 3. Edit an existing file: parse, change, write with exportFormPreserving to keep key order and ids.
const original = readFileSync("review-order.form", "utf8")
const edited = Form.parse(original)
edited.components.push({
id: "Field_note",
type: "text",
text: "Reviewed orders ship the same day.",
})
writeFileSync("review-order.form", exportFormPreserving(original, edited))
// 4. Validate: it parses back with the id the process references.
const check = Form.parse(readFileSync("review-order.form", "utf8"))
if (check.id !== "review-order") {
console.error(`form id is ${check.id}, expected review-order`)
process.exit(1)
}
npm i @bpmnkit/core. The script writes review-order.form, re-opens it, adds a component without reordering the rest, and checks the form id.
Top builder options
Form.create(id)….build()returns aFormDefinition;Form.export(form)gives the JSON.- Fields take
(label, key, options):textfield,textarea,select(valuesorvaluesKey),checkbox;radio(label, key, values). text(markdown)is static text;group(label, (g) => g.textfield(...))nests fields.options.validate:{ required, minLength, maxLength };defaultValue;idfor a fixed component id.- The builder stamps
8.10.0; override with.executionPlatformVersion("8.8.0"). - Read or change an existing file:
Form.parse(json)→form.components.
Traps
- The process calls the form id.
userTask(id, { zeebeUserTask: true, formId: "review-order" })andstartEvent(id, { formId })must equalForm.create("review-order"), the JSONid, not the file name. - Deploy the
.formbefore an instance reaches the task.formIdbinds to the latest deployed form with that id:casen deploy deploy review-order.formfirst. WithformBindingType: "deployment"it must be in the same deployment as the process. keyis the process variable the field reads and writes. Keep keys equal to the variable names the process and its gateways use; a typo silently creates a new variable.- Use a Camunda user task (
zeebeUserTask: true) for a linked form; Camunda 8.10 deprecates job-worker user tasks. - Edit with the preserving writer. Write
exportFormPreserving(original, edited); it keeps key order and component ids, so the diff shows only your change.
Validate
The script re-parses the file and checks the form id. Before deploying, run casen check review-order.form order.bpmn --deploy: it lints the form and checks that every formId in the .bpmn names a form. Check by hand that each key is a variable the process expects.
Related cards: casen agent-card bpmn, casen agent-card dmn. More: npx bpmnkit-docs ask "form on a start event".