Overview
@bpmnkit/connectors answers two questions about Camunda 8 connectors: which ones exist,
and what happens to a task when you apply one.
It bundles the 133 out-of-the-box Camunda connector templates as data, plus the
templates BPMN Kit maintains, so a catalog, a search box
or an AI tool call can work offline. And it resolves a template plus a set of values into
the zeebe:taskDefinition, zeebe:ioMapping and zeebe:modelerTemplate bookkeeping the
Modeler would write — deterministically, so the same template and values always produce the
same XML.
The package root is browser-safe. Everything that touches the filesystem lives behind
@bpmnkit/connectors/node.
The catalog and application logic live in
@bpmnkit/core/connectors, whose
templates leave out icons, groups, tooltips and placeholders. This package re-exports that API
and adds those parts back: getTemplate, applyConnectorTemplate and
CAMUNDA_CONNECTOR_TEMPLATES answer with the full templates, so an applied element carries its
icon. Use it in an editor or property panel. Use the core subpath where a bundle should stay
small, for example in a worker that generates diagrams.
Installation
npm install @bpmnkit/connectors
Browsing the catalog
import { listConnectors, searchConnectors, getTemplate } from "@bpmnkit/connectors";
listConnectors().length; // 134 — Camunda's 133, plus BPMN Kit's
searchConnectors("slack"); // 11 matches, inbound and outbound
const template = getTemplate("io.camunda.connectors.Slack.v1");
listConnectors and searchConnectors return ConnectorSummary — the shape a picker needs,
without the full template:
interface ConnectorSummary {
id: string;
name: string;
description?: string;
taskType: string; // e.g. "io.camunda:http-json:1"
appliesTo: string[]; // e.g. ["bpmn:Task"]
direction: ConnectorDirection; // "outbound" | "inbound"
keywords: string[];
requiredInputs: ConnectorInputSpec[];
optionalInputs: ConnectorInputSpec[];
}
Each ConnectorInputSpec carries what a form needs to render the field — label,
description, default, choices, whether it is isSecret or isFeel, and the condition
that decides whether it applies at all. summarizeTemplate produces the same shape from a
template you hold yourself.
CAMUNDA_CONNECTOR_TEMPLATES is the raw bundled array if you would rather work with the
templates directly.
Templates maintained by BPMN Kit
CAMUNDA_CONNECTOR_TEMPLATES mirrors Camunda’s marketplace and is regenerated from it.
Templates for services Camunda does not ship one for live beside it in
BPMNKIT_CONNECTOR_TEMPLATES, and join the same catalog: listConnectors,
searchConnectors, getTemplate and casen connector find them like any other. Each runs on
a connector the Camunda runtime already has — the REST connector, io.camunda:http-json:1 —
so a cluster needs no extra job worker.
| Template id | What it does | Guide |
|---|---|---|
io.bpmnkit.connectors.CloudflareClef.v1 | Asks a Cloudflare Clef decision model typed questions and returns calibrated answers to route on | AI Decisions |
Connector cards
A summary lists every input of a template. Many templates hold several operations, so GitHub’s
lists owner five times, once per operation that uses it. A card is one operation with only
the inputs it uses. It is the shape to hand a language model:
import { findConnectorCards, formatConnectorCard } from "@bpmnkit/connectors";
const [card] = findConnectorCards("create a github issue");
card.alias; // "github"
card.operation; // "createIssue"
card.values; // { operationGroup: "issues", issueOperationType: "createIssue" }
card.required; // owner, repo, issueTitle
formatConnectorCard(card);
// github createIssue — GitHub Outbound Connector: Issues / Create an issue | owner* repo* issueTitle* | optional: …
Pass card.values together with the inputs to applyConnectorTemplate. They select the
operation.
- Modes. Dropdowns that change what an operation needs without making it a different
operation, such as an authentication type, are
card.modes. Each choice lists the required inputs it adds. - Advanced inputs. Inputs marked
advanced(retries, timeouts, TLS, saved credentials) are left out offormatConnectorCardunless you pass{ advanced: true }. - Aliases. Every bundled template has a short, fixed alias, such as
http,slackorsqs-message-start, inCONNECTOR_ALIASES. That table also lists which dropdowns choose the operation.connectorAlias(id)andtemplateIdForAlias(alias)map between the two.
Picking cards for a diagram
selectConnectors({ text, tasks }) picks the cards a model should see when it connects a
diagram, per task and in code. text is what the person asked for; tasks are the diagram’s
nodes. It returns only tasks with a candidate, so an empty answer means there is nothing to
connect.
A card is a candidate for a task when:
- the task’s name names the system (“Post summary to Slack”);
- the request names the system, the task is a task rather than an event, and no other task’s name claims that system; or
- the task’s name shares a word with the connector’s name, other than a common verb.
Beyond those rules:
- REST fallback. The REST connector is offered for a task that asks for an HTTP call (“Fetch …”, “Call endpoint”) when nothing else fits. It is also offered for a task with no connector of its own when the request asks for a REST call (“check the stock with a REST call”).
- Synonyms. A few words requests use for what templates call something else, such as “notify” for sending a message, change the ranking only.
- Variety. Each further card of one template ranks lower. The Email connector’s IMAP operations do not crowd out SendGrid when the request names it.
- Systems nobody named. A connector that names a system the task and request do not ranks lower: “Azure OpenAI” for a request that says OpenAI.
- Generic words. A word every process has (request, document, file, data, report, record, form) never makes a connector a candidate by itself: “Request rework” is no SOAP request.
- Named systems only. A task whose name names a system gets no connector that only shares a word with it: “Send message to SQS” gets SQS, not Camunda’s Send message connector.
- The request wins. A task whose name names a system the request does not (“Post summary to Slack” for a request that says Teams) gets the request’s system first, ranked by what the request asks of it.
- Deprecated templates are never offered.
- Caps. At most three cards per task and eight in all. Every task keeps its best card before any task gets a second.
API cards
Most systems have no dedicated connector. The REST connector can still call them, but only
with the right base URL, path and authentication. The
API index has these for about 80 HTTP APIs, built
offline from their OpenAPI specs. Pass the services the request names as apis, and a task
that names one of them gets the REST connector first, plus an API card with its
best-fitting endpoints:
import { API_SERVICES, loadApiServices } from "@bpmnkit/connector-gen/api-index";
import { apiServicesIn, formatConnectorSelection, selectConnectors } from "@bpmnkit/connectors";
const text = "When someone signs up, create a Stripe customer";
const apis = await loadApiServices(apiServicesIn(text, API_SERVICES));
const selection = selectConnectors(
{ text, tasks: [{ id: "customer", name: "Create customer", type: "serviceTask" }] },
{ apis },
);
formatConnectorSelection(selection);
// customer (Create customer):
// http — Send REST Request | url* | …
// api stripe — Stripe API https://api.stripe.com auth=bearer secret=STRIPE_TOKEN
// POST /v1/customers — Create a customer | form body: name email description address …
- A dedicated connector comes first. The API card is left out when a dedicated
connector for the system has an operation that fits as much of the task’s name. An operation
fits a word when it does what the task’s verb says and the word is what it is about:
createIssuefits “Create issue”, but neither it norgetIssuefits “List open issues”. GitHub’s connector creates issues, so “Create GitHub issue” gets it. It has nothing for workflow runs, so “List GitHub workflow runs” gets the API card. The request counts too: the API card is also left out when the connector covers every word of the request that the best endpoint has. “Send a confirmation email with SendGrid” gets SendGrid’s connector, whatever the task is called. - Tasks only. Events get no API card.
- Ranking.
findApiOperations(service, text)ranks endpoints by the words of their summary, then of their path. The service’s own name never counts, nor its parts (“git” and “hub” of GitHub), and a word a tenth of its endpoints share counts a third: “GitHub Actions” is in hundreds of summaries, “runs” in a few. A verb picks the method (“Create” → POST, “List” → GET), and a path that ends in a word of the task wins over a deeper one. - Services in text.
apiServicesIn(text, summaries)finds the services a text names by their brand. A brand that is also an everyday word, like “box” or “square”, counts only as “Box API”.
import { formatConnectorSelection, selectConnectors } from "@bpmnkit/connectors";
const selection = selectConnectors({
text: "Every hour, list open GitHub issues and post a summary to Slack",
tasks: [
{ id: "list", name: "List open issues", type: "serviceTask" },
{ id: "post", name: "Post summary to Slack", type: "serviceTask" },
],
});
formatConnectorSelection(selection); // the prompt block, one task per paragraph
with lines
In the line format, a model configures a connector
with a with line:
with post: slack postMessage | channel=#ops | text== "Order " + orderId | token=xoxb-123
applyConnectorLines(definitions, lines) applies the lines from parseProcessText or a change
script. Each line goes through applyTemplateToElement, so a plain task becomes the connector’s
service task and inbound templates work on events.
resolveConnectorLine repairs what a model gets nearly right and reports the rest:
-
A misspelt alias. One within two letters of a real alias is read as that one (
slak→slack). -
The operation. It is matched exactly, or by its last dotted part (
postMessage→chat.postMessage), or as the start of an SDK call (chat.completions.create→chat). An operation the connector lacks is read as the one sharing the longest start with it, of five letters or more (sendEmailImap→sendEmailSmtp). It may also be given as the input that selects it. -
A short key. A key the operation lacks, but that is the end of one it has, is read as that one (
channel→data.channel). -
Values without a key.
http POST https://…setsmethodandurl. A URL written where the alias goes is a REST call to it, andapi=github GETnames the method too. -
Inputs in one part.
| region=eu-west-1 functionName=resizeis two inputs, as the cards list them. A FEEL value is never split, and a*copied from a card (token*=) is dropped. -
Results.
result=namesets the result variable;result=name: exprsets the result expression={name: expr}, under whatever key the operation uses for it. -
Credentials. A credential written as a value becomes a
{{secrets.…}}placeholder: the diagram never carries one. -
Variables in another syntax.
{{orderId}},{{variables.orderId}}and${orderId}become the FEEL=orderId, and${orderId}inside a FEEL expression becomesorderId. Secrets are left as they are. -
API index calls. With
{ apis }as the last argument, anhttpline that names a service (http POST /v1/customers | api=stripe), or calls a URL under its base URL, is completed from the index:- the base URL goes before the path;
- each
{param}of the path reads the variable of the same name; - the service’s authentication is set with a
{{secrets.STRIPE_TOKEN}}placeholder, unless the line sets its own; - the headers the endpoint needs are added, such as Notion’s
Notion-Versionor a form body’sContent-Type.
A call the index does not have is kept, and becomes a question to check the method and URL. A line written like an API card’s head (
api github GET /issues) is read ashttp GET /issues | api=github. A service written where the alias goes (stripe POST /v1/refunds) is the same call; a connector of that name wins. A path alone, with one service loaded, is that service’s. A FEELurl=beside the path is ignored: the index completes the path. A word of the path is spelt as the service spells it (Actions→actions). An endpoint’s summary written where the path goes (http GET — List workflow runs for a repository) is read as that endpoint. A{{param}}in a path is the parameter{param}.
A required input the line left out becomes a question (AppliedConnectorLines.questions),
with a line to finish, and the rest of the line is still applied. A line for a node that already
carries the same connector changes only the inputs it names, so the answer to a question keeps
everything else.
- Writing back.
connectorLineFor(element, definitions)writes an element’s connector back as a line. Applying that line again gives the same element. - Prompting.
CONNECT_GUIDEis the system prompt that teaches the format to the pass that configures a finished diagram’s connectors.
Applying a template
Applying does not mutate an element. It turns a template plus your values into the builder
options for whichever element kind the template applies to, which you hand to
@bpmnkit/core:
import { applyConnectorTemplate } from "@bpmnkit/connectors";
import { Bpmn } from "@bpmnkit/core";
const { serviceTask, problems } = applyConnectorTemplate("io.camunda.connectors.Slack.v1", {
token: "=secrets.SLACK_TOKEN",
"data.channel": "#alerts",
"data.text": "=message",
});
// serviceTask = {
// name: "Slack connector",
// taskType: "io.camunda:slack:1",
// ioMapping: { inputs: [...], outputs: [] },
// modelerTemplate: "io.camunda.connectors.Slack.v1",
// modelerTemplateVersion: "1",
// modelerTemplateIcon: "data:image/svg+xml;utf8,...",
// }
Bpmn.createProcess("alerting").startEvent("in").serviceTask("notify", serviceTask).build();
applyElementTemplate(template, values) is the same operation against a template object
rather than a catalog id — the path a workspace template takes.
The result carries exactly one populated element key, so the template decides what it applies to rather than the caller guessing:
interface ApplyResult {
serviceTask?: ServiceTaskOptions;
adHocSubProcess?: Partial<AdHocSubProcessOptions>;
startEvent?: Partial<StartEventOptions>;
boundaryEvent?: Partial<BoundaryEventOptions>;
intermediateEvent?: Partial<IntermediateCatchEventOptions>;
problems: ApplyProblem[];
}
Nothing throws — “this value does not belong to this template” and “this required value is
missing” are answers a UI has to show, so they come back as problems alongside whatever
could be built:
applyConnectorTemplate("io.camunda.connectors.Slack.v1", {}).problems;
// [{ key: "token", kind: "missing-required",
// message: 'Missing required value for "OAuth token" (token)' }, …]
applyConnectorTemplate("nope.does.not.exist", {}).problems;
// [{ message: 'Unknown connector template "nope.does.not.exist"' }]
Application is deterministic: property order and the emitted bindings depend only on the template and the values, never on iteration order or the clock. That is what lets a diagram be rebuilt in CI and diffed.
Builder options have no field for some bindings. A message start event’s correlation key and
zeebe:linkedResource come back as problems rather than being dropped, and an inbound
template’s message name — which Camunda generates per element — must be passed as
message.name. To write every binding, apply to an element instead.
Applying to an element — inbound connectors and linked resources
An inbound connector does not live on its element alone. Its message name and correlation key
belong to a root bpmn:message the event references, and an RPA task’s scripts to
zeebe:linkedResources. applyTemplateToElement writes a template onto an element of a parsed
model, all of it:
import { applyTemplateToElement, getTemplate } from "@bpmnkit/connectors";
import { Bpmn } from "@bpmnkit/core";
const webhook = getTemplate("io.camunda.connectors.webhook.WebhookConnectorIntermediate.v1")!;
const { definitions, problems } = applyTemplateToElement(
Bpmn.parse(xml),
"payment-received",
webhook,
{
"inbound.context": "payments",
"message.correlationKey": "=orderId",
correlationKeyExpression: "=request.body.orderId",
},
);
Bpmn.export(definitions);
// <bpmn:message id="Message_…" name="…">
// <bpmn:extensionElements>
// <zeebe:subscription correlationKey="=orderId" />
// </bpmn:extensionElements>
// </bpmn:message>
// <bpmn:intermediateCatchEvent id="payment-received"
// zeebe:modelerTemplate="io.camunda.connectors.webhook.WebhookConnectorIntermediate.v1" …>
// <bpmn:extensionElements>
// <zeebe:properties>
// <zeebe:property name="inbound.type" value="io.camunda:webhook:1" /> …
// <bpmn:messageEventDefinition messageRef="Message_…" />
What it does, binding by binding:
| Binding | Written to |
|---|---|
bpmn:Message#property (name) | The root bpmn:message the event definition or receive task references |
bpmn:Message#zeebe:subscription#property (correlationKey) | That message’s zeebe:subscription — where Camunda reads it. A copy on the event itself is removed |
zeebe:property (inbound.type, …) | The element’s zeebe:properties |
zeebe:linkedResource | The element’s zeebe:linkedResources, one zeebe:linkedResource per linkName |
| everything else | As applyElementTemplate resolves it, on the element |
- The element’s type follows the template.
elementTypeconverts the element, keeping its id, name and flows;elementType.eventDefinitionmakes an event a message event. A template whoseappliesTodoes not cover the element is refused and the model comes back unchanged —bpmn:Taskcovers every task type, as it does in the Modeler. - Messages are reused, not multiplied. A message already carrying the name is referenced; the element’s own message is renamed when nothing else uses it; otherwise a new one is created.
- A generated message name is deterministic. Camunda generates an inbound message’s name as a UUID. Here it keeps the name of the message the element already references, or is derived from the template and element ids — never from a clock or random source.
- Re-applying is safe. Each extension kind the template declares is replaced whole, so
switching a dropdown off removes what it wrote, and applying twice gives the model applying
once does.
zeebe:modelerTemplate,…Versionand…Iconare stamped the same way. - The input is never mutated. The result is a copy.
The keys for these properties, where the template gives no id, are message.name,
message.correlationKey and linkedResource.<linkName>.<property> — for example
linkedResource.RPAScript.resourceId. listConnectors reports them like any other input.
Workspace templates
A project can ship its own .camunda/element-templates/. The filesystem half lives behind its
own entry point so importing the catalog in a browser never pulls in node:fs:
import { discoverElementTemplates, collectElementTemplates } from "@bpmnkit/connectors/node";
discoverElementTemplateswalks upward from a diagram to the project root, nearest winning — the resolution a modeler needs.collectElementTemplateswalks downward from a root — the sweep a CI check needs, so a broken template in a sub-folder is reported rather than skipped because the root looked fine.
registerElementTemplates merges what you found into the catalog, later registration winning
on an id collision, so listConnectors, searchConnectors and getTemplate then see a
project’s own templates alongside the bundled ones. clearRegisteredTemplates undoes it.
Which host resolves how
Per-file resolution is what Camunda Desktop Modeler does: a diagram in a/ sees
a/.camunda/element-templates/ and every folder above it up to the project root, and never
b/’s. When two folders define the same id, the one nearer the diagram wins.
| Host | Resolution |
|---|---|
casen lint, casen dev checks | Per file. Each diagram’s connector/* findings use the templates from its folder up to the project root (casen lint: the current directory; casen dev: the served folder), then the bundled catalog. |
| VS Code extension | Per file. The extension host calls discoverElementTemplates directly — no proxy — with the workspace folder as the root, and the Problems panel checks connector inputs against them. |
| Studio (project opened from disk) | Per file. The connector-catalog plugin asks the proxy for GET /element-templates?root=<project>&file=<model path> and swaps the set when you open another model. |
casen connector list/search/show | Upward from the current directory. |
casen connector validate | Downward: every template in the project (collectElementTemplates). |
| bpmnkit.com/editor, Drop | Bundled templates only. A browser with no filesystem has no path to resolve from. |
In a browser host, the connector-catalog plugin does the swapping:
const catalog = createConnectorCatalogPlugin(configPanelBpmn, palette, {
proxyUrl: "http://localhost:3033",
workspaceRoot: "/home/me/project",
diagramPath: "processes/orders/order.bpmn", // absolute, or relative to workspaceRoot
});
// The user opened another diagram:
await catalog.setDiagramPath("processes/billing/invoice.bpmn");
The proxy serves /element-templates only for a workspaceRoot it accepts as a workspace: a
folder passed with casen proxy start --root, or a project folder that is not your home
directory, the filesystem root or a hidden folder. The diagram path must resolve inside that
root. See Local proxy.
The previous diagram’s templates are unregistered before the next diagram’s are registered, and
a bundled template they shadowed comes back, so the properties panel’s connector picker lists
only the current diagram’s templates plus the bundled and built-in ones. A host that resolves
templates itself (with its own filesystem access) passes them with
catalog.setWorkspaceTemplates(templates) instead. Without diagramPath, workspaceRoot keeps
the project-wide merge (GET /element-templates?root=…).
Validating a template
import { validateElementTemplate } from "@bpmnkit/connectors";
validateElementTemplate(template);
// {
// valid: false,
// problems: [
// { path: 'properties[0].type',
// message: 'unknown property type "nope" — expected one of String, Text, Hidden, Dropdown, Boolean, Number' },
// { path: 'properties[0].binding', message: 'binding is required' },
// ],
// warnings: [],
// }
Problems are reported by path rather than as a JSON-schema oneOf dump, and every problem
is collected rather than stopping at the first. Warnings are kept separate from problems, so a
CI gate can fail on one and not the other. readTemplateDocument parses a file that may hold
one template or an array of them.
The CLI wraps this as casen connector validate, which exits non-zero
for CI and takes --format json.
API Reference
| Export | Description |
|---|---|
listConnectors() | Every connector in the catalog, as summaries |
searchConnectors(query) | Summaries matching name, description or keywords |
getTemplate(id) | The full ElementTemplate for an id |
summarizeTemplate(template) | ConnectorSummary from a template you hold |
propertyKey(property) | The variable name a template property binds to |
applyConnectorTemplate(id, values) | Catalog template → builder options + problems |
applyElementTemplate(template, values) | Template object → builder options + problems |
applyTemplateToElement(definitions, elementId, template, values) | Template written onto an element of a parsed model → { definitions, problems } |
validateElementTemplate(template) | { valid, problems, warnings } |
readTemplateDocument(text) | Parse a file holding one template or many |
registerElementTemplates(templates) | Merge templates into the catalog |
clearRegisteredTemplates() | Drop everything registered |
CAMUNDA_CONNECTOR_TEMPLATES | The 133 bundled Camunda templates, raw |
BPMNKIT_CONNECTOR_TEMPLATES | The templates BPMN Kit maintains, raw |
findConnectorCards(query, { limit }) | Operation cards matching a request, best first |
connectorCards(id) / listConnectorCards() | The cards of one template / of all |
formatConnectorCard(card, { advanced }) | A card as one prompt line |
connectorAlias(id) / templateIdForAlias(alias) | Template id ↔ short alias |
CONNECTOR_ALIASES | Alias and operation dropdowns of every bundled template |
applyConnectorLines(definitions, lines, { apis }) | Apply with lines → { definitions, problems, fixes, questions } |
resolveConnectorLine(line, { apis }) | One with line → template, card, values, problems, fixes, questions |
connectorLineFor(element, definitions) | An element’s connector as a with line |
CONNECT_GUIDE | System prompt teaching with lines |
selectConnectors({ text, tasks }, { perTask, total, apis }) | The cards, and API cards, to show a model for each task of a diagram |
formatConnectorSelection(selection) | Picked cards as a prompt block |
apiServicesIn(text, summaries) | Ids of the API-index services a text names |
findApiOperations(service, text, { limit }) / rankApiOperations | A service’s endpoints that fit a task, best first |
formatApiCard({ service, operations }) | An API card for a prompt |
findApiOperation(service, method, path) | The indexed endpoint a call names |
apiUrl(baseUrl, path) / apiAuthValues(service) / apiSecretNames(service) | REST connector inputs for an indexed call |
From @bpmnkit/connectors/node: discoverElementTemplates, collectElementTemplates,
DEFAULT_CONFIG_FOLDER, TEMPLATES_SUBFOLDER.
Stability
@bpmnkit/connectors carries the 1.0 stability promise:
its exports will not change shape without a major version.
Two things the promise does not freeze, because both track Camunda rather than this package:
- The catalog’s contents. Connectors are added, and upstream templates gain versions and
properties. A changed
CAMUNDA_CONNECTOR_TEMPLATESis a minor; the types describing it are covered as usual. - Template validation being strict about new upstream shapes. A template that validates today will not start failing in a minor — that direction is covered.