About camunda-ai-agents
camunda-ai-agents is a skill for modeling agentic AI processes in Camunda 8.8 and later. It explains how to configure the AI Agent connector, define BPMN tools and prompts, and manage agent context and responses.
Key features
- 01Model agents in ad-hoc subprocesses
- 02Configure AI Agent connector templates
- 03Define tools with BPMN activities
- 04Declare tool inputs with fromAi()
- 05Configure prompts and response handling
- 06Support v1 and v2 template migration
Use cases
- 01Build a process that invokes an LLM agent
- 02Expose REST calls as agent tools
- 03Pass generated parameters to BPMN activities
- 04Upgrade an AI Agent template from v1 to v2
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ camunda/ skills --skill camunda-ai-agentsAfter installing, type/camunda-ai-agentsin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
portability.jsonreferences/ai-agent-task.mdfromai.mdtool-modeling.mdv1-to-v2-upgrade.mdDetails
From the project READMECamunda 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 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-templateworkflow 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
- AI Agent Sub-process (recommended variant)
- AI Agent Task
- Tool Definitions — how tool name, description, and
inputSchemaare derived fromAi()FEEL function
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.
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:
# 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 regularbpmn:subProcessor 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
toolCallResultmust 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:
-
Tool name — the activity ID in the ad-hoc subprocess (e.g.,
LookupCustomer,GetCurrentWeather). The connector uses the BPMNid, not the human-facingnameattribute, as the tool name passed to the LLM. Pick descriptive IDs. -
Tool description — the value of
<bpmn:documentation>on the activity. If documentation is missing, the connector falls back to the activity'snameattribute, 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 applydoes not write<bpmn:documentation>; hand-edit it in after applying a tool connector template. -
Input schema — derived automatically from
fromAi()calls inside the activity's input mappings. NofromAi()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.
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}.
<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.
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 =.
<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— whendata.response.format.type=text(the default).agent.responseJson— whenformat=json, or whenformat=textwithdata.response.format.parseJson=true.agent.context— whendata.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 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), orcustom. 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 thenameattribute. 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:
c8ctl bpmn lint 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:adHocSubProcesswith 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 (applydoesn't write it — set it via a direct edit). - Every tool's flow ends with
toolCallResultset in scope. - Both prompts start with
=. data.limits.maxModelCallsis 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 — worked XML for the four tool shapes (REST connector, script task, user task, sub-flow), including async-callback sub-flows
- fromai.md — full
fromAi()signature, all calling variants (positional, named, enum schemas, optional params, multi-call JSON bodies) - 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 — the older Task variant (audit/intercept every tool call) — niche use only