@camunda/linting-autofix
v0.1.0
Published
Autofix and autofill affordances for Camunda linting, shared by Camunda Desktop and Web Modeler
Readme
@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';
import '@camunda/linting-autofix/assets/linting-autofix.css';
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.
Peer dependencies
bpmn-js, diagram-js, bpmn-js-properties-panel and @bpmn-io/properties-panel are peer dependencies, so the module always renders through the host's own instances. In particular, its entries are authored with htm bound to @bpmn-io/properties-panel's vendored preact copy — the panel renders its tree through that copy, and hooks from a second preact instance track a different currentComponent than the panel sets during render.
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 agentpill, in its own entry directly below the field, 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 agentpill 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, or a new output parameter on a plain task.Withdrawn where clicking could not help: on an output row already targeting
toolCallResultwith no source to fill in, 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.A correction that resolves a validation message is painted onto that message rather than taking a row of its own, and carries one of two labels:
| 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 |
Fix| | Nothing but a broken key (fromAi(),fromAi(42)) | A complete call: key, description, type |Autofill| | A call that will not parse at all | A reset to a working default; what you wrote is replaced, and the message says so |Autofill|Three 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.
Development
npm install
npm start # playground for testing library code in isolation
npm run all # lint + build + test, the same as CILocal 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-autofixRe-run npm run build after each change; the host picks up dist/ directly.
License
MIT
