@camunda/linting-autofix
v0.2.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.On an output mapping row this pill writes the process variable name 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.A correction that resolves a validation message is painted onto that message rather than taking a row of its own. 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 |
Fix| | Nothing but a broken key (fromAi(),fromAi(42)) | A complete call: key, description, type |Input from agent| | A 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 agent|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
