@camunda/linting-autofix
Autofix and autofill affordances for linting, shared by Camunda Desktop and Web Modeler.
The module decorates the properties panel of elements inside an agentic ad-hoc sub-process with one-click affordances that wire up both sides of an AI Agent tool contract: the fromAi() inputs the agent supplies, and the toolCallResult output it reads back. One rule governs every affordance: seed a blank field, never overwrite a value.
Both modelers register the same module, so the two stay in lockstep by construction rather than by builders remembering to change both.
Usage
Register the module:
import { agentConfigAutofillModule } from '@camunda/linting-autofix';
const modeler = new BpmnModeler({
additionalModules: [
agentConfigAutofillModule
]
});
The module is editor-only (it mutates the model) and needs the properties panel to be registered too. Host integration is a single line.
Breaking change:
@camunda/linting-autofix/assets/linting-autofix.cssno longer exists — drop anyimport '@camunda/linting-autofix/assets/linting-autofix.css';line. The module now publishes plain diagnostics through the properties panel's ownpropertiesPanel.setDiagnosticsAPI, which renders and styles everything (text colors, action buttons, group dots, list badges) natively; there is no styling of this package's own left to ship.
Peer dependencies
bpmn-js, diagram-js, bpmn-js-properties-panel and @bpmn-io/properties-panel (>= 3.57.1) are peer dependencies, so the module always works with the host's own instances and stays in sync with however the panel styles a diagnostic.
bpmn-js-element-templates (>= 2.38.0) and @camunda/linting (>= 4.1.0) are optional peers. The module adds its actions to the diagnostics linting publishes through propertiesPanel.setDiagnostics. Where linting has flagged a field, the action rides that diagnostic; everywhere else it is a bare action with its explanation in the tooltip.
Affordances
-
Input autofill (
fromAi()), on the tool flow's entry element only — blank io-mapping inputs and blank FEEL-capable element-template fields bound tozeebe:inputget anInput from agentaction, seeding=fromAi(toolCall.<key>, "<description>", "<type>"). No offer when the derived key collides with afromAi()key already declared on the element. -
Result autofill (
toolCallResult), anOutput to agentaction on any tool-flow element that does not already produce a contract-readable result upstream on its own execution path, writing into the element's native result field — a templated connector's result expression, a FEEL script task's result variable, a multi-instance output collection, or an output parameter's process variable name on a plain task.On an output mapping row this action sits on the process variable name and writes that and nothing else, so the row's variable assignment value never affects it.
Withdrawn where clicking could not help: on an output row already targeting
toolCallResult, the click would write the value already there; on a row targeting a sub-path such astoolCallResult.statusCode, it would collapse a deliberate partial contribution back to the root. -
Autofix actions, offered on populated fields: malformed
fromAi()keys (wrong prefix, bracket notation, quoted, over-long path, wrong casing, missing entirely) and near-miss result-channel names are detected by re-parsing the element's own fields. A well-formedfromAi()call sitting on a non-entry element is offered asMove to "<entry element>", which declares it on the actual entry element and rewrites only thefromAi(...)span, leaving the rest of that field's expression intact.Every action is part of the field's own diagnostic: a correction rides the validation message it resolves, and an offer on a field that nothing flags is a bare action with its explanation in the tooltip. Its visible label describes the builder's goal independently of whether the implementation classifies the mutation as a fix or autofill:
What the field holds What accepting gives you Label A description or type you wrote Your text kept exactly; only the key is corrected, and anything you left out is filled in FixNothing but a broken key ( fromAi(),fromAi(42))A complete call: key, description, type Input from agentA call that will not parse at all A reset to a working default; what you wrote is replaced, and the adjacent validation text and accessible tooltip say so Input from agentThree rules govern this, stated canonically in
agent-tool-corrections.ts: keep every decision you made that is still readable; reset only when nothing can be salvaged; and say which of the two just happened, because the button previews nothing. A type is written from a template's declared property but never from a keyword guess into a call you already had.
Fixes are span-scoped, never whole-field replacement, so an expression like =concat("prefix-", fromAi(), "-suffix") is never discarded. Every applied fix, including a multi-element move, is a single undo unit.
The seeded fromAi() type is a guess, not a fact
Every seeded call carries a type argument computed by inferFromAiType in from-ai-autofill.ts. Only propertyType === 'Boolean' on an element-template field is a confident fact; everything else is a keyword heuristic over the field's own description/label text, defaulting to 'string'.
If you hit a runtime type mismatch on a tool call, check this before assuming the connector or the model is at fault: the third argument of a seeded call is exactly what inferFromAiType returned, never hand-authored. The fix is a one-word edit to the seeded "<type>" argument in the field itself, not a change to that function.
Eligibility
An ad-hoc sub-process counts as agentic once it carries the io.camunda.agenticai.toolContainer zeebe:Properties boolean, or the io.camunda.connectors.agenticai.aiagent.jobworker.v1 element template id — matching isAgenticAdHocSubProcess in bpmnlint-plugin-camunda-compat. A bare zeebe:AdHoc extension is deliberately not treated as a fallback: plain ad-hoc sub-processes using output collection carry it too.
The shared predicates (isAgenticAdHocSubProcess, isToolFlowElement, isToolEntryElement) are exported so every host surface agrees on where the affordances belong.
Build and Run
# install/setup
npm install
## spin up visual playground
npm start
# lint + build + test, the same as CI
npm run all
Local development against a host modeler
Build the library and link it into the host:
npm run build && npm link
cd ../camunda-modeler/client && npm link @camunda/linting-autofix
Re-run npm run build after each change; the host picks up dist/ directly.
License
MIT