AI Agents
Camunda 8’s AI Agent Sub-process lets a single process step delegate to an LLM that can
call tools (other BPMN activities) in a loop until it produces a final answer. BPMNKit
generates this pattern the same way it generates everything else: deterministically, from a
ProcessPlan — never by hand-writing the underlying ad-hoc-sub-process XML.
Quick start
Section titled “Quick start”/bpmnkit:agent add a support-triage agent with tools: search KB (http), escalate to human (user task), post summary to slackOr, from the CLI directly: write an aiAgent plan step (see below), then casen synth.
How it’s modeled
Section titled “How it’s modeled”- A
bpmn:AdHocSubProcesscarrying azeebe:taskDefinitionof typeio.camunda.agenticai:aiagent-job-worker:1— the presence ofzeebe:taskDefinitionis itself what marks it as a job-worker implementation. - Each tool is a root-node activity inside the sub-process (no incoming sequence flow). The tool’s element id is its name as the LLM sees it; its documentation is the description the LLM reads.
- Parameters the LLM must supply are
fromAi(toolCall.<param>, "<description>", "<type>"?, <jsonSchema>?, {required: false}?)FEEL expressions. - Each tool’s result is aggregated into an
outputCollection(defaulttoolCallResults); the agent’s own final answer lands in a process variable (defaultagent).
None of this is hand-authored — it’s generated by the aiAgent plan step.
The aiAgent plan step
Section titled “The aiAgent plan step”{ "kind": "aiAgent", "id": "triage_agent", "name": "Triage agent", "provider": "anthropic", "model": "claude-sonnet-5", "providerInputs": { "provider.anthropic.authentication.apiKey": "{{secrets.ANTHROPIC_API_KEY}}" }, "systemPrompt": "You triage support tickets and post updates to Slack.", "userPrompt": "=ticketText", "tools": [ { "id": "notify_slack", "description": "Post a status update to the #support-escalations Slack channel.", "connector": { "template": "io.camunda.connectors.Slack.v1", "values": { "method": "chat.postMessage", "token": "{{secrets.SLACK_OAUTH_TOKEN}}", "data.channel": "#support-escalations", "data.text": "placeholder" } }, "params": [ { "name": "message", "description": "The status update to post", "type": "string", "target": "data.text" } ] }, { "id": "escalate", "description": "Escalate the ticket to a human agent.", "jobType": "escalate:1", "params": [{ "name": "reason", "description": "Why this needs a human" }] } ], "errorBoundary": { "errorCode": "AGENT_FAILED", "steps": [{ "kind": "end", "id": "agent_failed", "errorCode": "AGENT_FAILED" }] }}Compile it like any other plan:
casen synth support-triage.plan.json --output support-triage.bpmncasen lint lint support-triage.bpmn --profile deployKey points:
- Every tool needs an
idand adescriptionwritten for the LLM to read, like briefing a teammate. - A tool is either connector-backed (
connector) or a plain job-worker (jobType) — never both. Search the connector catalog first (casen connector search), fall back tojobType(with a scaffolded worker) only when no connector exists. - A
params[].targetmatching a connector value’s key overrides it — the standard way to let the LLM control one field of an otherwise-fixed connector call, as innotify_slackabove: the channel is fixed, but the message text is supplied by the agent at call time. Give the overridden key a placeholder value inconnector.values, not a real one. - Always give the agent step an error boundary — an unhandled agent/tool failure needs somewhere to go.
- Secrets always use
{{secrets.NAME}}— never a literal API key or token.
Deploy-grade validation
Section titled “Deploy-grade validation”casen lint --profile deploy includes agentic/* rules specific to this pattern:
| Rule | Catches |
|---|---|
agentic/tool-not-root | A tool with an incoming sequence flow (tools must be root nodes) |
agentic/tool-no-description | A tool with no documentation for the LLM to read |
agentic/fromai-bad-ref | A fromAi() call whose first argument doesn’t reference toolCall.* |
agentic/no-output-collection | Missing tool-result aggregation |
agentic/limits-missing | No maxModelCalls safety limit |
The data-flow category can report spurious “variable never set” findings against the
agent’s own provider/model/prompt bindings (a hyphenated model id like claude-sonnet-5
can be misread as an arithmetic expression) — this is a known limitation of that heuristic,
not a real issue, and doesn’t affect the deploy profile.
Testing without a real LLM call
Section titled “Testing without a real LLM call”Scenarios mock the whole agent sub-process as a single job — the dispatcher routes any job-worker-backed ad-hoc sub-process (this pattern included) through the same job-mock mechanism as any other task:
{ "name": "Agent resolves the ticket", "mocks": { "io.camunda.agenticai:aiagent-job-worker:1": { "outputs": { "agent": { "status": "resolved" } } } }, "expect": { "path": ["triage_agent"], "variables": { "agent": { "status": "resolved" } } }}casen test support-triage.bpmnThis verifies the process routes correctly through and around the agent step. It does not
exercise the agent’s actual tool-calling behavior, prompt quality, or model choice — those
require a real (or sandboxed) LLM run, which is out of scope for casen test.
See also
Section titled “See also”- Building Processes with AI — the overall plan/synth pipeline
casen connector— searching the catalog for connector-backed tools