diff --git a/fluent/agentic/pom.xml b/fluent/agentic/pom.xml
index 2ebce363..02d1847d 100644
--- a/fluent/agentic/pom.xml
+++ b/fluent/agentic/pom.xml
@@ -60,6 +60,12 @@
test
1.2.0-SNAPSHOT
+
+ io.serverlessworkflow
+ serverlessworkflow-experimental-lambda
+ ${project.version}
+ test
+
\ No newline at end of file
diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java
index 8248c28c..8ba9d8c7 100644
--- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java
+++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java
@@ -16,6 +16,7 @@
package io.serverlessworkflow.fluent.agentic;
import dev.langchain4j.agentic.Agent;
+import dev.langchain4j.agentic.internal.AgentInstance;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import java.util.List;
@@ -34,4 +35,136 @@ interface MovieExpert {
@Agent
List findMovie(@V("mood") String mood);
}
-}
+
+ interface SettingAgent extends AgentInstance {
+
+ @UserMessage(
+ """
+ Create a vivid {{style}} setting. It should include the time period, the state of technology,
+ key locations, and a brief description of the world’s political or social situation.
+ Make it imaginative, atmospheric, and suitable for a {{style}} novel.
+ """)
+ @Agent(
+ "Generates an imaginative setting including timeline, technology level, and world structure")
+ String invoke(@V("style") String style);
+ }
+
+ interface HeroAgent extends AgentInstance {
+
+ @UserMessage(
+ """
+ Invent a compelling protagonist for a {{style}} story. Describe their background, personality,
+ motivations, and any unique skills or traits.
+ """)
+ @Agent("Creates a unique and relatable protagonist with rich backstory and motivations.")
+ String invoke(@V("style") String style);
+ }
+
+ interface ConflictAgent extends AgentInstance {
+
+ @UserMessage(
+ """
+ Generate a central conflict or threat for a {{style}} plot. It can be external or
+ internal (e.g. moral dilemma, personal transformation).
+ Make it high-stakes and thematically rich.
+ """)
+ @Agent("Proposes a central conflict or dramatic tension to drive a compelling narrative.")
+ String invoke(@V("style") String style);
+ }
+
+ interface FactAgent extends AgentInstance {
+
+ @UserMessage(
+ """
+ Generate a unique sci-fi fact about an alien civilization's {{goal}} environment or evolutionary history. Make it imaginative and specific.
+ """)
+ @Agent("Generates a core fact that defines the foundation of an civilization.")
+ String invoke(@V("fact") String fact);
+ }
+
+ interface CultureAgent extends AgentInstance {
+
+ @UserMessage(
+ """
+ Given the following sci-fi fact about an civilization, describe 3–5 unique cultural traits, traditions, or societal structures that naturally emerge from this environment.
+ Fact:
+ {{fact}}
+ """)
+ @Agent("Derives cultural traits from the environmental/evolutionary fact.")
+ List invoke(@V("fact") String fact);
+ }
+
+ interface TechnologyAgent extends AgentInstance {
+
+ @UserMessage(
+ """
+ Given the following sci-fi fact about an alien civilization, describe 3–5 technologies or engineering solutions they might have developed. Focus on tools, transportation, communication, and survival systems.
+ Fact:
+ {{fact}}
+ """)
+ @Agent("Derives plausible technological inventions from the fact.")
+ List invoke(@V("fact") String fact);
+ }
+
+ interface StorySeedAgent extends AgentInstance {
+
+ @UserMessage(
+ """
+ You are a science fiction writer. Given the following title, come up with a short story premise. Describe the world, the central concept, and the thematic direction (e.g., dystopia, exploration, AI ethics).
+ Title: {{title}}
+ """)
+ @Agent("Generates a high-level sci-fi premise based on a title.")
+ String invoke(@V("title") String title);
+ }
+
+ interface PlotAgent extends AgentInstance {
+
+ @UserMessage(
+ """
+ Using the following premise, outline a three-act structure for a science fiction short story. Include a brief description of the main character, the inciting incident, the rising conflict, and the resolution.
+ Premise:
+ {{premise}}
+ """)
+ @Agent("Transforms a premise into a structured sci-fi plot.")
+ String invoke(@V("premise") String premise);
+ }
+
+ interface SceneAgent extends AgentInstance {
+
+ @UserMessage(
+ """
+ Write the opening scene of a science fiction short story based on the following plot outline. Introduce the main character and immerse the reader in the setting. Use vivid, cinematic language.
+ Plot:
+ {{plot}}
+ """)
+ @Agent("Generates the opening scene of the story from a plot outline.")
+ String invoke(@V("plot") String plot);
+ }
+
+ interface MeetingInvitationDraft extends AgentInstance {
+
+ @UserMessage(
+ """
+ You are a professional meeting invitation writer. Draft a concise and clear meeting invitation email based on the following details:
+ Subject: {{subject}}
+ Date: {{date}}
+ Time: {{time}}
+ Location: {{location}}
+ Agenda: {{agenda}}
+ """)
+ @Agent("Drafts a professional meeting invitation email.")
+ String invoke(@V("subject") String subject, @V("date") String date, @V("time") String time, @V("location") String location, @V("agenda") String agenda);
+ }
+
+ interface MeetingInvitationStyle extends AgentInstance {
+
+ @UserMessage(
+ """
+ You are a professional meeting invitation writer. Rewrite the following meeting invitation email to better fit the {{style}} style:
+ Original Invitation: {{invitation}}
+ """)
+ @Agent("Edits a meeting invitation email to better fit a given style.")
+ String invoke(@V("invitation") String invitation, @V("style") String style);
+ }
+
+ }
diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java
new file mode 100644
index 00000000..0071d0bd
--- /dev/null
+++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.serverlessworkflow.fluent.agentic;
+
+import static io.serverlessworkflow.fluent.agentic.Agents.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import dev.langchain4j.agentic.AgentServices;
+import dev.langchain4j.agentic.workflow.HumanInTheLoop;
+import io.serverlessworkflow.api.types.Workflow;
+import io.serverlessworkflow.impl.WorkflowApplication;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.Test;
+
+class WorkflowTests {
+
+ @Test
+ public void testAgent() throws ExecutionException, InterruptedException {
+ final StorySeedAgent storySeedAgent = mock(StorySeedAgent.class);
+
+ when(storySeedAgent.invoke(eq("A Great Story"))).thenReturn("storySeedAgent");
+ when(storySeedAgent.outputName()).thenReturn("premise");
+
+ Workflow workflow =
+ AgentWorkflowBuilder.workflow("storyFlow")
+ .tasks(d -> d.agent("story", storySeedAgent))
+ .build();
+
+ Map topic = new HashMap<>();
+ topic.put("title", "A Great Story");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ String result =
+ app.workflowDefinition(workflow).instance(topic).start().get().asText().orElseThrow();
+
+ assertEquals("storySeedAgent", result);
+ }
+ }
+
+ @Test
+ public void testAgents() throws ExecutionException, InterruptedException {
+ final StorySeedAgent storySeedAgent = mock(StorySeedAgent.class);
+ final PlotAgent plotAgent = mock(PlotAgent.class);
+ final SceneAgent sceneAgent = mock(SceneAgent.class);
+
+ when(storySeedAgent.invoke(eq("A Great Story"))).thenReturn("storySeedAgent");
+ when(storySeedAgent.outputName()).thenReturn("premise");
+
+ when(plotAgent.invoke(eq("storySeedAgent"))).thenReturn("plotAgent");
+ when(plotAgent.outputName()).thenReturn("plot");
+
+ when(sceneAgent.invoke(eq("plotAgent"))).thenReturn("sceneAgent");
+ when(sceneAgent.outputName()).thenReturn("story");
+
+ Workflow workflow =
+ AgentWorkflowBuilder.workflow("storyFlow")
+ .tasks(
+ d ->
+ d.agent("story", storySeedAgent)
+ .agent("plot", plotAgent)
+ .agent("scene", sceneAgent))
+ .build();
+
+ Map topic = new HashMap<>();
+ topic.put("title", "A Great Story");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ String result =
+ app.workflowDefinition(workflow).instance(topic).start().get().asText().orElseThrow();
+
+ assertEquals("sceneAgent", result);
+ }
+ }
+
+ @Test
+ public void testSequence() throws ExecutionException, InterruptedException {
+ final StorySeedAgent storySeedAgent = mock(StorySeedAgent.class);
+ final PlotAgent plotAgent = mock(PlotAgent.class);
+ final SceneAgent sceneAgent = mock(SceneAgent.class);
+
+ when(storySeedAgent.invoke(eq("A Great Story"))).thenReturn("storySeedAgent");
+ when(storySeedAgent.outputName()).thenReturn("premise");
+
+ when(plotAgent.invoke(eq("storySeedAgent"))).thenReturn("plotAgent");
+ when(plotAgent.outputName()).thenReturn("plot");
+
+ when(sceneAgent.invoke(eq("plotAgent"))).thenReturn("sceneAgent");
+ when(sceneAgent.outputName()).thenReturn("story");
+
+ Workflow workflow =
+ AgentWorkflowBuilder.workflow("storyFlow")
+ .tasks(d -> d.sequence("story", storySeedAgent, plotAgent, sceneAgent))
+ .build();
+
+ Map topic = new HashMap<>();
+ topic.put("title", "A Great Story");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ String result =
+ app.workflowDefinition(workflow).instance(topic).start().get().asText().orElseThrow();
+
+ assertEquals("sceneAgent", result);
+ }
+ }
+
+ @Test
+ public void testParallel() throws ExecutionException, InterruptedException {
+
+ final SettingAgent setting = mock(SettingAgent.class);
+ final HeroAgent hero = mock(HeroAgent.class);
+ final ConflictAgent conflict = mock(ConflictAgent.class);
+
+ when(setting.invoke(eq("sci-fi"))).thenReturn("Fake conflict response");
+ when(setting.outputName()).thenReturn("setting");
+
+ when(hero.invoke(eq("sci-fi"))).thenReturn("Fake hero response");
+ when(hero.outputName()).thenReturn("hero");
+
+ when(conflict.invoke(eq("sci-fi"))).thenReturn("Fake setting response");
+ when(conflict.outputName()).thenReturn("conflict");
+
+ Workflow workflow =
+ AgentWorkflowBuilder.workflow("parallelFlow")
+ .tasks(d -> d.parallel("story", setting, hero, conflict))
+ .build();
+
+ Map topic = new HashMap<>();
+ topic.put("style", "sci-fi");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ Map result =
+ app.workflowDefinition(workflow).instance(topic).start().get().asMap().orElseThrow();
+
+ assertEquals(3, result.size());
+ assertTrue(result.containsKey("branch-0-story"));
+ assertTrue(result.containsKey("branch-1-story"));
+ assertTrue(result.containsKey("branch-2-story"));
+
+ Set values =
+ result.values().stream().map(Object::toString).collect(Collectors.toSet());
+
+ assertTrue(values.contains("Fake conflict response"));
+ assertTrue(values.contains("Fake hero response"));
+ assertTrue(values.contains("Fake setting response"));
+ }
+ }
+
+ @Test
+ // TODO: callFn must be replace with a .output() method once it's available
+ public void testSeqAndThenParallel() throws ExecutionException, InterruptedException {
+ final FactAgent factAgent = mock(FactAgent.class);
+ final CultureAgent cultureAgent = mock(CultureAgent.class);
+ final TechnologyAgent technologyAgent = mock(TechnologyAgent.class);
+
+ List cultureTraits =
+ List.of("Alien Culture Trait 1", "Alien Culture Trait 2", "Alien Culture Trait 3");
+
+ List technologyTraits =
+ List.of("Alien Technology Trait 1", "Alien Technology Trait 2", "Alien Technology Trait 3");
+
+ when(factAgent.invoke(eq("alien"))).thenReturn("Some Fact about aliens");
+ when(factAgent.outputName()).thenReturn("fact");
+
+ when(cultureAgent.invoke(eq("Some Fact about aliens"))).thenReturn(cultureTraits);
+ when(cultureAgent.outputName()).thenReturn("culture");
+
+ when(technologyAgent.invoke(eq("Some Fact about aliens"))).thenReturn(technologyTraits);
+ when(technologyAgent.outputName()).thenReturn("technology");
+ Workflow workflow =
+ AgentWorkflowBuilder.workflow("alienCultureFlow")
+ .tasks(
+ d ->
+ d.sequence("fact", factAgent)
+ .callFn(
+ f ->
+ f.function(
+ (Function>)
+ fact -> {
+ Map result = new HashMap<>();
+ result.put("fact", fact);
+ return result;
+ }))
+ .parallel("cultureAndTechnology", cultureAgent, technologyAgent))
+ .build();
+
+ Map topic = new HashMap<>();
+ topic.put("fact", "alien");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ Map result =
+ app.workflowDefinition(workflow).instance(topic).start().get().asMap().orElseThrow();
+
+ assertEquals(2, result.size());
+ assertTrue(result.containsKey("branch-0-cultureAndTechnology"));
+ assertTrue(result.containsKey("branch-1-cultureAndTechnology"));
+
+ assertEquals(cultureTraits, result.get("branch-0-cultureAndTechnology"));
+ assertEquals(technologyTraits, result.get("branch-1-cultureAndTechnology"));
+ }
+ }
+
+ @Test
+ public void humanInTheLoop() throws ExecutionException, InterruptedException {
+ final MeetingInvitationDraft meetingInvitationDraft = mock(MeetingInvitationDraft.class);
+ when(meetingInvitationDraft.invoke(eq("Meeting with John Doe"),
+ eq("2023-10-01"), eq("08:00AM"),
+ eq("London"),
+ eq("Discuss project updates")))
+ .thenReturn("Drafted meeting invitation for John Doe");
+ when(meetingInvitationDraft.outputName()).thenReturn("draft");
+
+
+ final MeetingInvitationStyle meetingInvitationStyle = mock(MeetingInvitationStyle.class);
+ when(meetingInvitationStyle.invoke(eq("Drafted meeting invitation for John Doe"), eq("formal")))
+ .thenReturn("Styled meeting invitation for John Doe");
+ when(meetingInvitationStyle.outputName()).thenReturn("styled");
+
+ AtomicReference request = new AtomicReference<>();
+
+ HumanInTheLoop humanInTheLoop = AgentServices.humanInTheLoopBuilder()
+ .description("What level of formality would you like? (please reply with “formal”, “casual”, or “friendly”)")
+ .inputName("style")
+ .outputName("style")
+ .requestWriter(q -> request.set("What level of formality would you like? (please reply with “formal”, “casual”, or “friendly”)"))
+ .responseReader(() -> "formal")
+ .build();
+
+ Workflow workflow =
+ AgentWorkflowBuilder.workflow("meetingInvitationFlow")
+ .tasks(
+ d ->
+ d.sequence("draft", meetingInvitationDraft, humanInTheLoop, meetingInvitationStyle)
+ ).build();
+ Map initialValues = new HashMap<>();
+ initialValues.put("title", "Meeting with John Doe");
+ initialValues.put("date", "2023-10-01");
+ initialValues.put("time", "08:00AM");
+ initialValues.put("location", "London");
+ initialValues.put("agenda", "Discuss project updates");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ String result =
+ app.workflowDefinition(workflow).instance(initialValues).start().get().asText().orElseThrow();
+
+ assertEquals("Styled meeting invitation for John Doe", result);
+ }
+
+
+ }
+}