Home / Docs / Guides / AI Agents

AI Agents

Add a Camunda 8 AI Agent Sub-process to a BPMN process — an LLM-in-the-loop step that can call tools, generated deterministically like everything else in the pipeline.

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

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

How it’s modeled

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

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

Deploy-grade validation

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.

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

See also