camunda-forms · diff
git:20261022.8802b5d to git:20261028.976917c
1 added, 1 removed. Audit A to A.
---
name: camunda-forms
description: |
Use this skill when a person has to enter, review, or approve data as a step in a process — task screens, approval or start forms with validation, conditional fields, or dropdowns — even if forms or user tasks aren't mentioned. Creates Camunda Form JSON (.form) for BPMN user tasks and start events; not for forms in standalone web apps.
---
# Camunda Forms
Create Camunda Form JSON schemas for user tasks and start events in Camunda 8.8+.
## Prerequisites
- Camunda 8.8+ cluster
## Cross-References
- **camunda-bpmn**: Use when linking forms to user tasks via `<zeebe:formDefinition formId="..."/>`
- **camunda-feel**: Use for form validation expressions and conditional visibility logic
## Instructions
### Form Basics
- Camunda Forms are JSON files with `.form` extension (not `.json`). Link to BPMN user tasks via the form's `id` field, which must match the `formId` attribute on `<zeebe:formDefinition formId="..."/>` inside the user task. The user task itself must also include `<zeebe:userTask/>` (the Camunda user task implementation). Don't link via the older `formKey` attribute — that's the deprecated job-worker user task, removed in Camunda 8.10. See **camunda-bpmn** § Form Definition for the BPMN side.
+ Camunda Forms are JSON files with `.form` extension (not `.json`). Link to BPMN user tasks via the form's `id` field, which must match the `formId` attribute on `<zeebe:formDefinition formId="..."/>` inside the user task. The user task itself must also include `<zeebe:userTask/>` (the Camunda user task implementation). Don't link via the older `formKey` attribute — that's the deprecated job-worker user task, removed in Camunda 8.10. See **camunda-bpmn** § Form Definition for the BPMN side; if you edited the BPMN, run its lint loop until clean.
### Form Structure
**Example** — minimal form skeleton:
```json
{
"components": [],
"executionPlatform": "Camunda Cloud",
"executionPlatformVersion": "8.8.0",
"exporter": { "name": "Camunda Modeler", "version": "5.34.0" },
"schemaVersion": 18,
"id": "my-form-id",
"type": "default"
}
```
All metadata fields are required. Use `schemaVersion: 18` and `executionPlatformVersion: "8.8.0"`.
### Components
Every component requires `type`, `id`, and `layout`. Input components also need `key` (maps to process variable name) and `label`.
```json
{
"type": "textfield",
"id": "Field_Name",
"key": "customerName",
"label": "Customer Name",
"layout": { "row": "row_0", "columns": null }
}
```
**Input components**: `textfield`, `textarea`, `number`, `checkbox`, `checklist`, `radio`, `select`, `taglist`, `datetime`
**Display components**: `text` (markdown), `html`, `image`, `separator`, `button`
**Layout components**: `group`, `spacer`
Use the schema property names exactly:
- Set an input's initial value with `defaultValue`. Do not use `value` on a component; `value` is only used inside option objects such as `{ "label": "High", "value": "high" }`.
- A submit control is a `button` component with `action: "submit"`. Do not use `type: "submit"` or add a `key` to a button.
```json
{
"type": "textfield",
"id": "Field_Name",
"key": "customerName",
"label": "Customer Name",
"defaultValue": "Ada",
"layout": { "row": "row_0", "columns": null }
}
```
```json
{
"type": "button",
"id": "Button_submit",
"label": "Submit",
"action": "submit",
"layout": { "row": "row_1", "columns": null }
}
```
See `references/component-reference.md` for complete properties of each component type.
### Layout
Components are arranged in rows. Place components in the same row to display them side-by-side:
```json
{ "type": "textfield", "id": "F1", "key": "firstName", "label": "First Name", "layout": { "row": "row_0", "columns": null } },
{ "type": "textfield", "id": "F2", "key": "lastName", "label": "Last Name", "layout": { "row": "row_0", "columns": null } }
```
### Variable Binding
The `key` property maps a form field to a process variable:
- On load: field is pre-populated from the variable if it exists
- On submit: field value is written back to the variable
### Validation
```json
{
"validate": {
"required": true,
"minLength": 2,
"maxLength": 100,
"min": 0,
"max": 10000,
"pattern": "^[A-Z]{2}[0-9]+$",
"validationExpression": "=amount <= budget"
}
}
```
Custom error messages: `requiredErrorMessage`, `patternErrorMessage`.
### Conditional Visibility
Hide or show fields based on other field values using FEEL:
```json
{
"conditional": {
"hide": "=approved = false"
}
}
```
The expression is evaluated against all form field values and process variables.
### Select / Radio / Checklist Options
Static values:
```json
{
"type": "select",
"values": [
{ "label": "Low", "value": "low" },
{ "label": "Medium", "value": "medium" },
{ "label": "High", "value": "high" }
]
}
```
Dynamic values from process variable:
```json
{
"type": "select",
"valuesExpression": "=departments"
}
```
The variable should be a list of `{label, value}` objects.
### Read-Only Fields
Display data without allowing edits:
```json
{
"type": "textfield",
"key": "orderId",
"label": "Order ID",
"readonly": true
}
```
### Text / Markdown
Display static text or instructions using markdown:
```json
{
"type": "text",
"text": "### Review Details\n\nPlease review the information below.",
"id": "Text_Header",
"layout": { "row": "row_0", "columns": null }
}
```
### Groups
Organize related fields:
```json
{
"type": "group",
"label": "Contact Information",
"components": [
{ "type": "textfield", "id": "F1", "key": "email", "label": "Email", "layout": { "row": "row_g0", "columns": null } },
{ "type": "textfield", "id": "F2", "key": "phone", "label": "Phone", "layout": { "row": "row_g1", "columns": null } }
],
"id": "Group_Contact",
"layout": { "row": "row_1", "columns": null }
}
```
### Output Format
Generate complete `.form` JSON files. Ensure:
- All `id` values are unique within the form
- `key` values match expected process variable names
- Input defaults use `defaultValue`; submit controls use `type: "button"` and `action: "submit"` without a `key`
- `layout.row` values increment sequentially (`row_0`, `row_1`, ...)
- Metadata fields are present and correct
### Schema Validation Loop (Lint Before Deploy)
After creating or editing `.form` files, validate them against the official Camunda form schema before presenting or deploying them. Use `ajv` with the `ajv-errors` plugin directly. The official schema permits component extension properties, so the helper below also rejects the known invalid component shapes (`value`, `type: "submit"`, and a button `key`). A plain `ajv-cli` invocation fails on this schema because it uses the `errorMessage` keyword; if you use `ajv-cli`, load `ajv-errors` via `--require`. Run all commands from the **project root**:
```bash
npm install --save-dev ajv ajv-errors @bpmn-io/form-json-schema@1.18.0
# Create the validation helper and commit it as a project tool
cat > validate-form.cjs << 'EOF'
const Ajv = require('ajv');
const addErrors = require('ajv-errors');
const fs = require('fs');
const schema = require('@bpmn-io/form-json-schema/resources/schema.json');
const file = process.argv[2];
if (!file) { console.error('Usage: node validate-form.cjs <path-to-form.form>'); process.exit(1); }
let form;
try { form = JSON.parse(fs.readFileSync(file, 'utf8')); }
catch (e) { console.error(`Cannot read/parse ${file}: ${e.message}`); process.exit(1); }
const ajv = new Ajv({ allErrors: true, strict: false });
addErrors(ajv);
const validate = ajv.compile(schema);
if (!validate(form)) { console.error(JSON.stringify(validate.errors, null, 2)); process.exit(1); }
function invalidComponentShapes(components, path = 'components') {
if (!Array.isArray(components)) return [];
const errors = [];
for (const [index, component] of components.entries()) {
if (!component || typeof component !== 'object' || Array.isArray(component)) continue;
const location = `${path}[${index}]`;
if (Object.prototype.hasOwnProperty.call(component, 'value')) {
errors.push(`${location}: use defaultValue instead of value on a component`);
}
if (component.type === 'submit') {
errors.push(`${location}: use type "button" with action "submit"`);
}
if (component.type === 'button' && Object.prototype.hasOwnProperty.call(component, 'key')) {
errors.push(`${location}: buttons must not define key`);
}
errors.push(...invalidComponentShapes(component.components, `${location}.components`));
}
return errors;
}
const shapeErrors = invalidComponentShapes(form.components);
if (shapeErrors.length) { console.error(JSON.stringify(shapeErrors, null, 2)); process.exit(1); }
console.log('Valid ✓');
EOF
node validate-form.cjs path/to/form.form
```
For multiple forms (including nested directories), loop over every file and collect all failures before exiting:
```bash
failed=0
# Use find for recursive discovery; replace with *.form for flat directories
while IFS= read -r f; do
node validate-form.cjs "$f" || { echo "FAILED: $f"; failed=1; }
done < <(find . -name '*.form' -not -path '*/node_modules/*')
exit $failed
```
Common schema keywords and fixes:
| Keyword | Typical meaning | Typical fix |
|---|---|---|
| `required` | Required field missing | Add the missing property named in the error message |
| `additionalProperties` | Unknown property for this object | Remove the unsupported property |
| `type` | Wrong JSON value type | Change value to the expected type (string/number/boolean/object/array) |
| `enum` / `const` | Value not in allowed set | Replace with one of the allowed values |
| `pattern` | String format invalid (often `id`/`key`) | Rename to match the required regex (usually alnum/underscore style) |
## Troubleshooting
- **Form doesn't appear in Tasklist** — verify the user task in BPMN includes `<zeebe:userTask/>` and `<zeebe:formDefinition formId="..."/>` matching the form's `id` field. The legacy `formKey` attribute was removed in Camunda 8.10.
- **`schemaVersion` mismatch error** — set `schemaVersion: 18` and `executionPlatformVersion: "8.8.0"`; older values are rejected by 8.8+ clusters.
- **Conditional field always hidden** — `conditional.hide` is a FEEL expression that returns true to hide. Forgetting the `=` prefix turns it into a literal string. See **camunda-feel** § Common Patterns for the FEEL side.
## References
For detailed reference material, read from `references/`:
- [component-reference.md](references/component-reference.md) — complete properties and examples for every component type (textfield, number, select, datetime, etc.)