How to create a Custom Connector and Element Template using Camunda Connector SDK

Hello Community,

I am working with Camunda 8 and want to create my own Custom Connector using the Camunda Connector SDK (Java + Spring Boot).

I need guidance on the following points:

  1. How to properly implement a Custom Connector (request class, connector implementation, and execution logic) using the Connector SDK.
  2. How to create a JSON Element Template (element-templates/*.json) so that my connector shows up inside Camunda Modeler under the Service Task properties.
  3. How to handle errorExpression or error mapping in the element template.
  4. If possible, a small end-to-end example (connector code + element template JSON + usage in Modeler) would be really helpful.

Hello! I’d be happy to help you create a Custom Connector and Element Template for Camunda 8. Let me provide you with a comprehensive end-to-end example covering all your requirements.

1. Custom Connector Implementation

Project Setup

First, use the connector-template-outbound as a starting point and add the required dependency to your pom.xml:

<dependency>
  <groupId>io.camunda.connector</groupId>
  <artifactId>connector-core</artifactId>
  <version>${version.connectors}</version>
</dependency>

Request Class

Create a request class to handle input data:

package io.camunda.connector.example;

public class MyConnectorRequest {
    private String message;
    private String endpoint;
    private Authentication authentication;
    
    // getters and setters
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    
    public String getEndpoint() { return endpoint; }
    public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
    
    public Authentication getAuthentication() { return authentication; }
    public void setAuthentication(Authentication authentication) { this.authentication = authentication; }
}

class Authentication {
    private String token;
    private String user;
    
    // getters and setters
    public String getToken() { return token; }
    public void setToken(String token) { this.token = token; }
    
    public String getUser() { return user; }
    public void setUser(String user) { this.user = user; }
}

Connector Implementation

Create the main connector function:

package io.camunda.connector.example;

import io.camunda.connector.api.annotation.OutboundConnector;
import io.camunda.connector.api.outbound.OutboundConnectorContext;
import io.camunda.connector.api.outbound.OutboundConnectorFunction;

@OutboundConnector(
    name = "MY_CUSTOM_CONNECTOR",
    inputVariables = {"message", "endpoint", "authentication"},
    type = "io.camunda:my-custom-connector:1"
)
public class MyCustomConnectorFunction implements OutboundConnectorFunction {

    @Override
    public Object execute(OutboundConnectorContext context) throws Exception {
        // Deserialize input variables into your request class
        MyConnectorRequest request = context.bindVariables(MyConnectorRequest.class);
        
        // Execute your business logic
        return executeConnector(request);
    }

    private MyConnectorResult executeConnector(MyConnectorRequest request) {
        try {
            // Your custom logic here
            String result = processMessage(request.getMessage(), request.getEndpoint());
            
            MyConnectorResult response = new MyConnectorResult();
            response.setResult(result);
            response.setStatus("SUCCESS");
            return response;
            
        } catch (Exception e) {
            // Handle errors - these can be caught by errorExpression
            throw new RuntimeException("CONNECTOR_ERROR: " + e.getMessage(), e);
        }
    }
    
    private String processMessage(String message, String endpoint) {
        // Implement your connector logic
        return "Processed: " + message + " via " + endpoint;
    }
}

class MyConnectorResult {
    private String result;
    private String status;
    
    // getters and setters
    public String getResult() { return result; }
    public void setResult(String result) { this.result = result; }
    
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
}

Register the Connector

Create the SPI file at src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction:

io.camunda.connector.example.MyCustomConnectorFunction

2. JSON Element Template

Create element-templates/my-custom-connector.json:

{
  "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json",
  "name": "My Custom Connector",
  "id": "io.camunda.examples.MyCustomConnector",
  "description": "A custom connector example with error handling",
  "appliesTo": ["bpmn:ServiceTask"],
  "elementType": {
    "value": "bpmn:ServiceTask"
  },
  "groups": [
    {
      "id": "definition",
      "label": "Task definition",
      "openByDefault": true
    },
    {
      "id": "input",
      "label": "Input Configuration"
    },
    {
      "id": "authentication",
      "label": "Authentication"
    },
    {
      "id": "output",
      "label": "Output Mapping"
    },
    {
      "id": "errors",
      "label": "Error Handling"
    }
  ],
  "properties": [
    {
      "type": "Hidden",
      "value": "io.camunda:my-custom-connector:1",
      "group": "definition",
      "binding": {
        "type": "zeebe:taskDefinition",
        "property": "type"
      }
    },
    {
      "label": "Message",
      "description": "The message to process",
      "type": "String",
      "group": "input",
      "binding": {
        "type": "zeebe:input",
        "name": "message"
      },
      "constraints": {
        "notEmpty": true
      }
    },
    {
      "label": "Endpoint URL",
      "description": "The endpoint to send the message to",
      "type": "String",
      "group": "input",
      "binding": {
        "type": "zeebe:input",
        "name": "endpoint"
      },
      "constraints": {
        "notEmpty": true,
        "pattern": {
          "value": "^https?://.*",
          "message": "Must be a valid HTTP(S) URL"
        }
      }
    },
    {
      "label": "Authentication Token",
      "description": "Bearer token for authentication",
      "type": "String",
      "group": "authentication",
      "binding": {
        "type": "zeebe:input",
        "name": "authentication.token"
      },
      "constraints": {
        "notEmpty": true
      }
    },
    {
      "label": "Username",
      "description": "Username for authentication",
      "type": "String",
      "group": "authentication",
      "optional": true,
      "binding": {
        "type": "zeebe:input",
        "name": "authentication.user"
      }
    },
    {
      "label": "Result Variable",
      "description": "Name of variable to store the response in",
      "type": "String",
      "group": "output",
      "value": "connectorResult",
      "binding": {
        "type": "zeebe:taskHeader",
        "key": "resultVariable"
      }
    },
    {
      "label": "Error Expression",
      "description": "Expression to handle connector errors",
      "group": "errors",
      "type": "Text",
      "feel": "required",
      "binding": {
        "type": "zeebe:taskHeader",
        "key": "errorExpression"
      },
      "value": "if contains(error.message, \"CONNECTOR_ERROR\") then bpmnError(\"CUSTOM_CONNECTOR_ERROR\", error.message) else null"
    }
  ]
}

3. Error Handling

The errorExpression in the template above handles errors dynamically. You can customize it further:

if contains(error.message, "TIMEOUT") then bpmnError("TIMEOUT_ERROR", "Service timeout occurred")
else if contains(error.message, "AUTH") then bpmnError("AUTH_ERROR", "Authentication failed")
else if contains(error.message, "CONNECTOR_ERROR") then bpmnError("CUSTOM_ERROR", error.message)
else bpmnError("GENERIC_ERROR", "Unexpected error occurred")

4. Usage in Camunda Modeler

  1. Deploy the template: Place your my-custom-connector.json file in the element-templates folder of your Camunda Modeler installation
  2. Create a Service Task: In your BPMN diagram, add a Service Task
  3. Apply the template: Select the Service Task, go to the properties panel, and choose “My Custom Connector” from the template dropdown
  4. Configure properties: Fill in the required fields (message, endpoint, authentication token)
  5. Add error handling: Attach a boundary error event to your Service Task and set the error code to match your errorExpression (e.g., “CUSTOM_CONNECTOR_ERROR”)

5. Build and Deploy

  1. Build: Package your connector as a fat JAR using Maven: mvn clean package
  2. Deploy:
    • For Docker: Mount your JAR into the connectors runtime container
    • For Spring Boot: Run as a standalone application with the spring-boot-starter-camunda-connectors dependency

References

This example provides a complete working setup that you can customize for your specific use case. Let me know if you need clarification on any part!