---
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; 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.)
