diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 0b0315e0..10f45581 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -159,3 +159,15 @@ jobs:
- name: Build with Maven
run: mvn verify -PcheckFormat -B
working-directory: event-processing
+ build-extended-connector-runtime:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ - name: Build with Maven
+ run: mvn verify -PcheckFormat -B
+ working-directory: extended-connector-runtime
diff --git a/extended-connector-runtime/README.md b/extended-connector-runtime/README.md
new file mode 100644
index 00000000..1d058097
--- /dev/null
+++ b/extended-connector-runtime/README.md
@@ -0,0 +1,37 @@
+# Extended Connector Runtime
+
+As the connector runtime lies outside the engine, it can be adjusted.
+
+This example shows how a custom feel function can be introduced.
+
+This feel function can then be used on a connector runtime side feel expressions (for example result expression and error expression on outbound connectors).
+
+## How this works
+
+As the connector runtime uses a standardized way to bootstrap the feel engine and this way includes using the SPI function provider, this can be used to extend the functions being used.
+
+The [SPI file](./src/main/resources/META-INF/services/org.camunda.feel.context.CustomFunctionProvider) points to the function provider that determines on how the function is loaded.
+
+The provider then resolves the function by its name.
+
+The function itself is instantiated and gets a static reference to a spring bean injected. This workaround is chosen as there is no way to access the SPI context from within spring.
+
+## What kind of example is implemented here
+
+This example adds a function:
+
+```
+nextExecutionBackoff(backoff: days-time-duration): days-time-duration
+```
+
+This function then uses the `schedulingService` bean to determine the next timestamp where the execution could happen according to a defined schedule.
+
+## How can I run it
+
+As the example comes as spring boot application containing the rest connector, you can configure the connection to camunda and start the application using
+
+```bash
+mvn spring-boot:run
+```
+
+>Tip: Disable the default connectors coming with your Camunda 8 installation to test out this runtime.
diff --git a/extended-connector-runtime/pom.xml b/extended-connector-runtime/pom.xml
new file mode 100644
index 00000000..7c284b0a
--- /dev/null
+++ b/extended-connector-runtime/pom.xml
@@ -0,0 +1,174 @@
+
+
+ 4.0.0
+
+ io.camunda
+ extended-connector-runtime
+ 1.0-SNAPSHOT
+
+
+ 21
+ 21
+ UTF-8
+ 8.6.5
+ 8.5.9
+ 3.4.0
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${version.spring-boot}
+ pom
+ import
+
+
+ io.camunda
+ zeebe-bom
+ ${version.camunda}
+
+
+
+
+
+
+ io.camunda.connector
+ spring-boot-starter-camunda-connectors
+ ${version.connectors}
+
+
+ io.camunda
+ zeebe-client-java
+ ${version.camunda}
+
+
+ io.camunda.connector
+ connector-http-json
+ ${version.connectors}
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ io.camunda
+ camunda-process-test-spring
+ 8.6.6
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+ -parameters
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ 2.43.0
+
+
+
+
+ *.md
+ .gitignore
+
+
+
+
+ true
+ 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.2
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${version.spring-boot}
+
+
+
+ repackage
+
+
+
+
+
+
+
+
+
+ autoFormat
+
+ true
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+
+
+ spotless-format
+
+ apply
+
+ process-sources
+
+
+
+
+
+
+
+
+
+ checkFormat
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+
+
+ spotless-check
+
+ check
+
+ validate
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extended-connector-runtime/src/main/java/com/camunda/consulting/example/App.java b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/App.java
new file mode 100644
index 00000000..42f21183
--- /dev/null
+++ b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/App.java
@@ -0,0 +1,18 @@
+package com.camunda.consulting.example;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ApplicationContext;
+
+@SpringBootApplication
+public class App {
+ public static ApplicationContext applicationContext;
+
+ public static void main(String[] args) {
+ applicationContext = SpringApplication.run(App.class, args);
+ }
+
+ public static ApplicationContext getApplicationContext() {
+ return applicationContext;
+ }
+}
diff --git a/extended-connector-runtime/src/main/java/com/camunda/consulting/example/CustomFeelFunctionProvider.java b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/CustomFeelFunctionProvider.java
new file mode 100644
index 00000000..ae1d9521
--- /dev/null
+++ b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/CustomFeelFunctionProvider.java
@@ -0,0 +1,26 @@
+package com.camunda.consulting.example;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import org.camunda.feel.context.JavaFunction;
+import org.camunda.feel.context.JavaFunctionProvider;
+
+public class CustomFeelFunctionProvider extends JavaFunctionProvider {
+
+ @Override
+ public Optional resolveFunction(String functionName) {
+ if (NextExecutionTimeslotFeelFunction.NAME.equals(functionName)) {
+ return Optional.of(
+ new JavaFunction(
+ NextExecutionTimeslotFeelFunction.PARAMS,
+ NextExecutionTimeslotFeelFunction.getInstance()));
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Collection getFunctionNames() {
+ return List.of(NextExecutionTimeslotFeelFunction.NAME);
+ }
+}
diff --git a/extended-connector-runtime/src/main/java/com/camunda/consulting/example/NextExecutionTimeslotFeelFunction.java b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/NextExecutionTimeslotFeelFunction.java
new file mode 100644
index 00000000..6308449d
--- /dev/null
+++ b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/NextExecutionTimeslotFeelFunction.java
@@ -0,0 +1,53 @@
+package com.camunda.consulting.example;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.function.Function;
+import org.camunda.feel.syntaxtree.Val;
+import org.camunda.feel.syntaxtree.ValDayTimeDuration;
+import org.springframework.stereotype.Component;
+
+@Component
+public class NextExecutionTimeslotFeelFunction implements Function, Val> {
+ public static final List PARAMS = List.of("backoff");
+ public static final String NAME = "nextExecutionBackoff";
+
+ /**
+ * This field is required to access a bean from outside the spring context as spi is used for the
+ * function provider
+ */
+ private static NextExecutionTimeslotFeelFunction instance;
+
+ private final SchedulingService schedulingService;
+
+ public NextExecutionTimeslotFeelFunction(SchedulingService schedulingService) {
+ this.schedulingService = schedulingService;
+ instance = this;
+ }
+
+ public static NextExecutionTimeslotFeelFunction getInstance() {
+ return instance;
+ }
+
+ @Override
+ public Val apply(List vals) {
+ Val scheduledExecution = vals.get(0);
+ if (scheduledExecution instanceof ValDayTimeDuration duration) {
+ Duration value = duration.value();
+ return new ValDayTimeDuration(calculateNextExecution(value));
+ } else {
+ throw new IllegalStateException(
+ "Param 'scheduledExecution' expected to be of type 'date and time'");
+ }
+ }
+
+ private Duration calculateNextExecution(Duration duration) {
+ // we use the same "now" for both transformations to prevent inconsistencies
+ ZonedDateTime now = ZonedDateTime.now();
+ ZonedDateTime scheduledExecution = now.plus(duration);
+ return Duration.between(now, schedulingService.schedule(scheduledExecution))
+ .truncatedTo(ChronoUnit.SECONDS);
+ }
+}
diff --git a/extended-connector-runtime/src/main/java/com/camunda/consulting/example/SchedulingService.java b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/SchedulingService.java
new file mode 100644
index 00000000..9fb23d3f
--- /dev/null
+++ b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/SchedulingService.java
@@ -0,0 +1,12 @@
+package com.camunda.consulting.example;
+
+import java.time.ZonedDateTime;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SchedulingService {
+ public ZonedDateTime schedule(ZonedDateTime scheduledExecution) {
+ // you can call your scheduling system here
+ return scheduledExecution.plusMinutes(10);
+ }
+}
diff --git a/extended-connector-runtime/src/main/resources/META-INF/services/org.camunda.feel.context.CustomFunctionProvider b/extended-connector-runtime/src/main/resources/META-INF/services/org.camunda.feel.context.CustomFunctionProvider
new file mode 100644
index 00000000..bf174769
--- /dev/null
+++ b/extended-connector-runtime/src/main/resources/META-INF/services/org.camunda.feel.context.CustomFunctionProvider
@@ -0,0 +1 @@
+com.camunda.consulting.example.CustomFeelFunctionProvider
\ No newline at end of file
diff --git a/extended-connector-runtime/src/main/resources/application.yaml b/extended-connector-runtime/src/main/resources/application.yaml
new file mode 100644
index 00000000..abba98c2
--- /dev/null
+++ b/extended-connector-runtime/src/main/resources/application.yaml
@@ -0,0 +1,8 @@
+camunda:
+ connector:
+ polling:
+ enabled: false
+ webhook:
+ enabled: false
+ client:
+ mode: simple
\ No newline at end of file
diff --git a/extended-connector-runtime/src/test/java/com/camunda/consulting/example/AppTest.java b/extended-connector-runtime/src/test/java/com/camunda/consulting/example/AppTest.java
new file mode 100644
index 00000000..ec8e0715
--- /dev/null
+++ b/extended-connector-runtime/src/test/java/com/camunda/consulting/example/AppTest.java
@@ -0,0 +1,40 @@
+package com.camunda.consulting.example;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import io.camunda.process.test.api.CamundaSpringProcessTest;
+import io.camunda.zeebe.client.ZeebeClient;
+import io.camunda.zeebe.client.api.response.ProcessInstanceResult;
+import java.time.Duration;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+@CamundaSpringProcessTest
+public class AppTest {
+ @Autowired ZeebeClient zeebeClient;
+
+ @BeforeEach
+ public void setup() {
+ zeebeClient.newDeployResourceCommand().addResourceFromClasspath("test.bpmn").send().join();
+ }
+
+ @Test
+ void shouldExecute() {
+ Duration nextExecutionBackoff = Duration.ofMinutes(20);
+ ProcessInstanceResult result =
+ zeebeClient
+ .newCreateInstanceCommand()
+ .bpmnProcessId("test")
+ .latestVersion()
+ .withResult()
+ .send()
+ .join();
+ assertThat(result).isNotNull();
+ assertThat(Duration.parse((CharSequence) result.getVariable("nextExecutionBackoff")))
+ .isEqualTo(nextExecutionBackoff);
+ }
+}
diff --git a/extended-connector-runtime/src/test/resources/test.bpmn b/extended-connector-runtime/src/test/resources/test.bpmn
new file mode 100644
index 00000000..b1c3d1e0
--- /dev/null
+++ b/extended-connector-runtime/src/test/resources/test.bpmn
@@ -0,0 +1,53 @@
+
+
+
+
+ Flow_1mof3ja
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_1mof3ja
+ Flow_1rh30i0
+
+
+ Flow_1rh30i0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+