camunda-ai-agents · git:20261028.976917c · 2026-10-28 · sha256 ebb8b0f1c9faf3e1
camunda-ai-agents git:20261028.976917cA
Immutable. This exact content never changes and is served at /api/v1/blob/ebb8b0f1c9faf3e1.
---
name: camunda-ai-agents
description: |
Use this skill when an LLM should act inside a business process — choosing tools or next steps, calling systems, handing off to people — with guardrails, human review, or an audit trail, even if Camunda isn't mentioned, and before hand-coding an agent loop. Models Camunda 8 AI Agent sub-processes: tools as BPMN tasks, fromAi() parameters, prompts, and limits.
Also covers v1/v2 element-template selection and upgrading v1 elements to v2.
---
# Camunda AI Agent
Build agentic AI processes in Camunda 8.8+: an LLM driver (the AI Agent connector, **Sub-process variant**) applied to an ad-hoc subprocess with tools modeled as BPMN activities. Covers shape, prompts, tool modeling with `fromAi()`, sub-flow tools, and multi-turn agent context.
The older **Task variant** (AI Agent connector on a service task paired with an external multi-instance ad-hoc subprocess and explicit feedback loop) is documented in [references/ai-agent-task.md](references/ai-agent-task.md) for the niche cases where you need to audit or intercept every tool call. The Sub-process variant is the recommended choice for everything else, and is what the rest of this skill teaches.
## Prerequisites
- Camunda 8.8+ cluster (the AI Agent connector ships in 8.8+)
- c8ctl CLI installed and a profile configured — see **camunda-c8ctl**
- A provider, exact model identifier, and names of its existing connector secrets,
supplied by the user. Store secret values as Camunda cluster secrets, never in
the BPMN file. For local c8run, see **camunda-c8ctl** for the secrets bootstrap
flow.
## Provider Configuration
Before creating or editing BPMN, confirm the provider, exact model identifier,
and every required existing connector-secret name. If any are missing, ask the
user and stop until they are confirmed. Do not choose a default provider or
model, invent a secret name, or ask for secret values. Inspect the selected
template for provider-specific model and authentication fields.
For Camunda SaaS with Camunda-hosted connectors, configure secret values in
Console. Use the confirmed secret names in BPMN as `{{secrets.NAME}}`
references.
## Cross-References
- **camunda-bpmn**: BPMN basics, ad-hoc subprocess element, namespaces, the mandatory lint loop.
- **camunda-connectors**: The underlying `c8ctl element-template` workflow used to apply the AI Agent template (and the REST connector commonly used as a tool).
- **camunda-feel**: FEEL syntax for prompts, `fromAi()` calls, result expressions, type coercion.
- **camunda-process-mgmt**: Deploying the process, starting an instance, inspecting incidents from agent invocations.
## Authoritative References
- [AI Agent connector overview](https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/agentic-ai-aiagent/)
- [AI Agent Sub-process](https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/agentic-ai-aiagent-subprocess/) (recommended variant)
- [AI Agent Task](https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/agentic-ai-aiagent-task/)
- [Tool Definitions](https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/agentic-ai-aiagent-tool-definitions/) — how tool name, description, and `inputSchema` are derived
- [`fromAi()` FEEL function](https://docs.camunda.io/docs/components/modeler/feel/builtin-functions/feel-built-in-functions-miscellaneous/#fromaivalue)
## Template Generations: v1 and v2
Camunda 8.10 introduced **v2** AI Agent element templates (ID suffix `.v2`, e.g. `io.camunda.connectors.agenticai.ai-agent-subprocess.v2`) *(8.10+)* and deprecated **v1** (e.g. `io.camunda.connectors.agenticai.aiagent.jobworker.v1`). They are separate templates with separate job types. v1 keeps working for now but is deprecated and gets no new provider capabilities.
v2 adds reasoning/thinking settings, prompt caching, decoupled provider + backend choices (e.g. Anthropic on AWS Bedrock Mantle or Foundry), and a custom chat model provider.
- **New agent, 8.10+ cluster**: v2.
- **Cluster below 8.10, or Desktop Modeler below 5.51.0**: v1 — v2 needs an 8.10 engine and shows as **Not found** in older Modeler.
- **Existing v1 element, 8.10+ cluster**: upgrade to v2 — v1 is deprecated. v1 keeps working, so the upgrade can wait; updating within v1 (no `--to`) is only a stopgap, see the reference.
Switching takes effect per redeploy: running instances keep executing on the v1 job worker.
`c8ctl element-template search "ai agent"` returns both generations. The provider `--set` paths in the apply example below are v1-shaped; v2 nests credentials under `backend.<type>` (e.g. `provider.anthropic.backend.anthropic.apiKey`) — read `c8ctl element-template get-properties <id>` for the chosen template.
To migrate v1 → v2, read [references/v1-to-v2-upgrade.md](references/v1-to-v2-upgrade.md).
## Applying the AI Agent Connector
An ad-hoc subprocess alone is not an AI agent. Apply the AI Agent **Sub-process**
connector template to it; do not substitute a generic subprocess or service task.
**Example** — apply the template via c8ctl rather than hand-writing the many provider/prompt/memory fields:
```bash
# 0. One-time per environment: sync the OOTB catalog. search,
# get-properties, and OOTB-ID apply need this to have run at
# least once. Skip on subsequent runs.
c8ctl element-template sync
# 1. Find the current template ID and version — they evolve
c8ctl element-template search "ai agent"
# 2. Inspect the selected provider's model and authentication fields.
c8ctl element-template get-properties <id>
c8ctl element-template get-properties <id> --detailed data.systemPrompt.prompt
# 3. Replace placeholders with user-supplied values and selected-template fields.
c8ctl element-template apply -i <id> AgentTools process.bpmn \
--set 'provider.type=<provider>' \
--set 'provider.<provider>.authentication.<field>={{secrets.<secret-name>}}' \
--set 'provider.<provider>.model.<field>=<model>' \
--set data.systemPrompt.prompt='="You are a customer support agent. Use the available tools to look up customers and orders, and escalate to a human only when needed."' \
--set data.userPrompt.prompt='="Customer " + customerId + " reports: " + issue' \
--set data.limits.maxModelCalls='=10'
```
The template handles `zeebe:taskDefinition`, the `zeebe:adHoc` collection bindings, default input mappings, and the model-provider-specific fields — they change across template versions, don't hand-code them.
Supported providers:
- v1 `provider.type`: `anthropic`, `bedrock`, `azureOpenAi`, `google-vertex-ai`, `openai`, `openaiCompatible`.
- v2 `provider.type`: `anthropic`, `bedrock`, `openai`, `google-gemini`, `mistral`, `custom`. Azure/Foundry, Vertex AI, and OpenAI-compatible are backends of these.
## The BPMN Shape
The host is `bpmn:adHocSubProcess` — not a service task and not a regular sub-process. Tools live inside it.
```
bpmn:adHocSubProcess (template applied here)
└── <root activities, each one a tool>
bpmn:serviceTask — e.g., REST connector call
bpmn:scriptTask — FEEL computation
bpmn:userTask — human-in-the-loop
bpmn:subProcess — multi-step or async tool
```
Hard rules that the lint loop does NOT catch — verify by reading the BPMN:
- The element type must be `bpmn:adHocSubProcess`. A regular `bpmn:subProcess` or a service task will not host the connector.
- A tool's **root node** is the entry activity that the LLM picks. A root node has **no incoming sequence flow** and is **not a boundary event**. An incoming flow turns the node into a regular flow step and the agent never sees it.
- The ad-hoc subprocess must contain **at least one activity** — BPMN semantics; an empty agent is rejected.
- Somewhere in the tool's execution flow, the variable `toolCallResult` must be set — for a single-activity tool that's the activity itself; for a sub-flow tool, it can be any activity inside the sub-flow.
## Defining Tools
For a multi-part review, model each independent check as a focused root tool
rather than a catch-all task.
Three things determine whether the LLM picks a tool correctly:
1. **Tool name** — the **activity ID** in the ad-hoc subprocess (e.g., `LookupCustomer`, `GetCurrentWeather`). The connector uses the BPMN `id`, not the human-facing `name` attribute, as the tool name passed to the LLM. Pick descriptive IDs.
2. **Tool description** — the value of `<bpmn:documentation>` on the activity. If documentation is missing, the connector falls back to the activity's `name` attribute, but **always set documentation explicitly**. Strong descriptions say what the tool does, when to use it, when not to, and what it returns:
> "Look up a customer by ID. Returns the customer's name, tier, and account status. Call this when the user mentions a customer ID or name. Do not call for anonymous queries."
The LLM sees the description verbatim — keep it free of vendor names, internal URLs, or anything you wouldn't want quoted back. `c8ctl element-template apply` does not write `<bpmn:documentation>`; hand-edit it in after applying a tool connector template.
3. **Input schema** — derived automatically from `fromAi()` calls inside the activity's input mappings. No `fromAi()` calls → empty schema → the LLM can't pass parameters.
A tool can be a **single activity** (service task, script task, user task) or a **sub-flow** rooted at a `bpmn:subProcess` containing further activities. In both cases the LLM only sees the root node — descriptions, inputs, and schema are read from there. The internal sub-flow steps are invisible to the LLM; they execute in sequence per normal BPMN semantics and propagate variables up when the sub-process completes.
For a sub-flow tool this is literal: put `fromAi()` calls on the `bpmn:subProcess` root's own `zeebe:ioMapping`, not on a descendant activity — a `fromAi()` on a child task yields an empty schema, so the LLM calls the tool with no arguments and every downstream value (form fields included) comes back `null`.
Worked XML for each of the four shapes (REST, script, user task, sub-flow) is in [references/tool-modeling.md](references/tool-modeling.md).
## fromAi() — Declaring AI-Generated Parameters
`fromAi()` tags a value as "the LLM will provide this at runtime". The first argument must be a reference to `toolCall.<paramName>` — the last segment becomes the LLM-visible parameter name. The function takes an optional description, type (`"string"` default, plus `"number"`, `"boolean"`, `"array"`, `"object"`), a JSON-Schema fragment, and an options context for things like `{required: false}`.
```xml
<zeebe:input
source='="https://api.example.com/customers/" + fromAi(toolCall.id, "Customer ID", "string")'
target="url" />
```
Full signature, all 6 calling variants (positional, named, enum schemas, optional params, multi-call JSON bodies) in [references/fromai.md](references/fromai.md).
## toolCallResult — Returning Output to the Agent
When a tool completes, the connector reads the variable named `toolCallResult` from the tool's scope and forwards it to the LLM as the tool-call response. The rule is about **scope**, not which activity sets it:
- **Single-activity tool** — the activity itself sets `toolCallResult` (via a result expression / result variable / output mapping / script result variable).
- **Sub-flow tool** — any activity inside the sub-flow can set `toolCallResult`; BPMN variable scoping propagates the value to the sub-process scope when it completes.
Ways to set it depending on the activity type:
- **Connector with result expression**: `value="={toolCallResult: response.body}"`.
- **Connector with result variable**: name the result variable `toolCallResult`.
- **Output mapping**: `<zeebe:output source="=someValue" target="toolCallResult" />`.
- **Script task**: `<zeebe:script expression="..." resultVariable="toolCallResult" />`.
The value can be primitive (string, number) or a complex FEEL context — it'll be serialized to JSON before being sent to the LLM. If `toolCallResult` is missing or empty when the tool completes, the connector sends a generic "tool succeeded without returning a result" message to the LLM and the next turn degrades. Always set it meaningfully.
## Prompts
Both `data.systemPrompt.prompt` and `data.userPrompt.prompt` are FEEL strings — they start with `=`.
```xml
<zeebe:input
source='="You are a customer support agent. Use available tools to look up customers and orders. Escalate to a human only when needed."'
target="data.systemPrompt.prompt" />
<zeebe:input
source='="Customer " + customerId + " (priority: " + priority + ") reports: " + issue'
target="data.userPrompt.prompt" />
```
- **Static prompt**: `="You are ..."`. The `=` is mandatory even for plain text.
- **Variable interpolation**: `="Customer " + customerId + " reports: " + issue`. `+` coerces scalars to string.
- **Feedback-loop prompt**: `=if (is defined(followUpInput)) then followUpInput else initialUserInput`. Used when looping back into the agent with user follow-up — see "Response Interaction" below.
For long, structured prompts, build the string in a script task upstream and pass it in via a variable.
## Tool-Call Feedback Loop
The tool feedback loop is **internal**: the agent job worker repeatedly calls the LLM, activates the tools it chose, collects results, and re-prompts until the LLM produces a final response or `data.limits.maxModelCalls` is reached. You don't model the loop — only the tools.
## Reading the Agent Response
The agent writes its output to a single context variable named by the connector's **Result Variable** field — default `agent`. With multiple agents in the same process, give each a unique result variable name (e.g. `mySecondAgent`) and re-align the agent-context input field accordingly (`mySecondAgent.context`) to avoid interference between agents.
Which fields the result context contains depends on the response settings (examples below use the default `agent`):
- `agent.responseText` — when `data.response.format.type=text` (the default).
- `agent.responseJson` — when `format=json`, or when `format=text` with `data.response.format.parseJson=true`.
- `agent.context` — when `data.response.includeAgentContext=true`. Needed for the Response Interaction pattern below.
**Provider × `format=json` compat.** OpenAI supports `data.response.format.type=json` natively (JSON mode; a schema gives structured outputs). Anthropic and AWS Bedrock support it only with a `data.response.format.schema`. For other providers, and for v1 templates, use `format=text` + `data.response.format.parseJson=true` and read `agent.responseJson`.
**Schema is a FEEL context literal.** When `format=json`, `data.response.format.schema` is `feel: required` — write it as `={ type: "object", properties: { ... } }`, not a quoted JSON string.
**When JSON parsing fails.** Behavior differs between the two paths: `parseJson=true` on `format=text` omits `responseJson` and still populates `responseText` with the raw (unparseable) text — no incident. Native `format=json` raises an incident with error code `FAILED_TO_PARSE_RESPONSE_CONTENT`; handle it with an error boundary event if you need graceful fallback.
## Response Interaction (User Feedback Loop)
After the agent produces its final response, you may want a user (or another agent acting as a judge) to review or amend it and bounce it back in. The pattern is to route from the ad-hoc subprocess to a user task that collects `followUpInput`, then back to the same agent ad-hoc subprocess. The user-prompt FEEL switches between the initial and the follow-up input:
```
data.userPrompt.prompt = =if (is defined(followUpInput)) then followUpInput else initialUserInput
```
The agent preserves conversation context across re-entries; see the [Sub-process docs](https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/agentic-ai-aiagent-subprocess/) for the current context-handling field names, since these have evolved across template versions.
## Limits and Memory
- `data.limits.maxModelCalls` — caps the number of LLM calls per agent execution. Always set it (5–20 is a typical starting range); without a sensible cap a misbehaving prompt can rack up cost. There is no time-based limit.
- `data.memory.contextWindowSize` — caps how many prior messages the agent replays to the LLM (default 20). Smaller saves tokens, larger preserves more context.
- `data.memory.storage.type` — `in-process` (default), `camunda-document` (offload to the Camunda Document store when context grows past variable size limits), or `custom`. Use the hyphenated form.
## Troubleshooting
Non-obvious failure modes the lint loop will not catch.
- **Tool has an incoming sequence flow** — it stops being a tool and becomes a regular flow step. The tool's ROOT node must have no incoming flow. Internal activities inside a sub-flow tool can (and do) have incoming flows — that's how the sub-flow works.
- **Tool name confusion** — the LLM-visible tool name is the BPMN **`id`**, not the `name` attribute. Use descriptive PascalCase IDs.
- **Bare-string prompts** — both system and user prompts are FEEL. Even literals must be `="..."`.
- **Number-in-string FEEL** — concatenating a number into a URL or message requires `string(x)`; `+` between a string and an un-coerced number fails. Cross-ref **camunda-feel** § type coercion.
- **Hyphenated memory storage type** — `in-process`, `camunda-document`, `custom`. Not camelCase.
## Closing Step
Run the BPMN lint loop (see **camunda-bpmn**) before declaring the agent process done — fix and re-lint until zero errors and zero warnings:
```bash
c8ctl bpmn lint --json process.bpmn
```
Lint now catches `fromAi()` misplacement on sub-flow tools — the `agent-fromai-contract` rule in `bpmnlint-plugin-camunda-compat` flags a `fromAi()` call declared on a descendant activity instead of the tool's entry element (for a sub-flow tool, the `bpmn:subProcess` root's own `zeebe:ioMapping`). Run lint first; if it's clean but a sub-flow tool's params still come back `null`, re-check against "Defining Tools" above.
Lint catches structural BPMN problems but does not validate connector-template inputs. After lint is clean, verify by reading the BPMN:
- Host element is `bpmn:adHocSubProcess` with the AI Agent **Sub-process**
template applied, not an unconfigured ad-hoc subprocess.
- Every tool's root node has no incoming sequence flow and has a `<bpmn:documentation>` element (`apply` doesn't write it — set it via a direct edit).
- Every tool's flow ends with `toolCallResult` set in scope.
- Both prompts start with `=`.
- `data.limits.maxModelCalls` is set.
- Provider, model, and `{{secrets.NAME}}` references are user-confirmed; never
put secret values in BPMN.
## References
For detailed reference material, read from `references/`:
- [tool-modeling.md](references/tool-modeling.md) — worked XML for the four tool shapes (REST connector, script task, user task, sub-flow), including async-callback sub-flows
- [fromai.md](references/fromai.md) — full `fromAi()` signature, all calling variants (positional, named, enum schemas, optional params, multi-call JSON bodies)
- [v1-to-v2-upgrade.md](references/v1-to-v2-upgrade.md) — moving v1 elements to the v2 templates with `c8ctl element-template update`: ID pairs, dry-run/report workflow, recipe vs manual provider mapping, post-upgrade steps
- [ai-agent-task.md](references/ai-agent-task.md) — the older Task variant (audit/intercept every tool call) — niche use only