Skip to content

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.

/bpmnkit:agent add a support-triage agent with tools: search KB (http), escalate to human (user task), post summary to slack

Or, from the CLI directly: write an aiAgent plan step (see below), then casen synth.

  • A bpmn:AdHocSubProcess carrying a zeebe:taskDefinition of type io.camunda.agenticai:aiagent-job-worker:1 — the presence of zeebe:taskDefinition is 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 (default toolCallResults); the agent’s own final answer lands in a process variable (default agent).

None of this is hand-authored — it’s generated by 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:

Terminal window
casen synth support-triage.plan.json --output support-triage.bpmn
casen lint lint support-triage.bpmn --profile deploy

Key points:

  • Every tool needs an id and a description written 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 to jobType (with a scaffolded worker) only when no connector exists.
  • A params[].target matching a connector value’s key overrides it — the standard way to let the LLM control one field of an otherwise-fixed connector call, as in notify_slack above: the channel is fixed, but the message text is supplied by the agent at call time. Give the overridden key a placeholder value in connector.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.

casen lint --profile deploy includes agentic/* rules specific to this pattern:

RuleCatches
agentic/tool-not-rootA tool with an incoming sequence flow (tools must be root nodes)
agentic/tool-no-descriptionA tool with no documentation for the LLM to read
agentic/fromai-bad-refA fromAi() call whose first argument doesn’t reference toolCall.*
agentic/no-output-collectionMissing tool-result aggregation
agentic/limits-missingNo 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.

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" } } }
}
Terminal window
casen test support-triage.bpmn

This 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.