Lab 5 – Agentic workflows via LangChain4j annotations
In this final lab, you will experience the "Auto-Generation Magic" of Quarkus Flow. While the previous lab showed you how to manually orchestrate agents using the Java DSL, here you will let the engine automatically discover and compile LangChain4j annotations into full-blown workflows.
You will:
-
Add the
quarkus-flow-langchain4jandquarkus-langchain4j-agenticextensions. -
Implement sequential and parallel AI patterns using declarative annotations.
-
Visualize the auto-generated execution graphs in the Quarkus Flow Dev UI.
1. Prerequisites
You should have the project from Lab 4 open. Ensure you have:
-
Quarkus Flow set up and running.
-
A LangChain4j model provider (Ollama or OpenAI) configured in
application.properties.
2. Add the Agentic Workflow dependencies
To enable the build-time discovery of LangChain4j patterns, add the following two extensions to your pom.xml:
<dependency>
<groupId>io.quarkiverse.flow</groupId>
<artifactId>quarkus-flow-langchain4j</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-agentic</artifactId>
</dependency>
Adding these allows Quarkus Flow to act as a compiler that translates declarative AI routing into standard CNCF execution graphs at build time.
3. Implement the Annotated Workflows
Instead of creating a new Flow subclass, we will define an interface that uses the LangChain4j Agentic API. Quarkus Flow will scan these annotations and generate the corresponding workflow metadata automatically.
Create the file src/main/java/org/acme/langchain4j/Agents.java:
package org.acme.langchain4j;
import static java.util.Objects.requireNonNullElse;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.agentic.declarative.Output;
import dev.langchain4j.agentic.declarative.ParallelAgent;
import dev.langchain4j.agentic.declarative.SequenceAgent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import io.quarkiverse.langchain4j.RegisterAiService;
/**
* Example LangChain4j agentic workflows backed by Quarkus Flow.
* <p>
* When the app boots, quarkus-langchain4j creates beans for these
*
* @RegisterAiService interfaces. The quarkus-flow-langchain4j extension transparently builds WorkflowDefinitions for
* the
*
* @SequenceAgent and @ParallelAgent methods and registers them in the Quarkus Flow runtime.
* <p>
* You will see them under the Quarkus Flow Dev UI: - document.name ~=
* "story-creator-with-configurable-style-editor" - document.name ~= "evening-planner-agent"
*/
public final class Agents {
private Agents() {
}
// --- Domain types --------------------------------------------------------
public enum Mood {
ROMANTIC,
CHILL,
PARTY,
FAMILY
}
// --- 1) Sequential workflow: story creator --------------------------------
/**
* Top-level workflow interface that chains three sub-agents:
* <p>
* 1. CreativeWriter -> drafts the story 2. AudienceEditor -> adapts to a given audience 3. StyleEditor -> adapts
* the writing style
* <p>
* The Quarkus Flow integration builds a workflow whose input schema matches the method parameters (topic, style,
* audience). In Dev UI, you’ll see a workflow with a document name derived from this class.
*/
@RegisterAiService
public interface StoryCreatorWithConfigurableStyleEditor {
@SequenceAgent(outputKey = "story", subAgents = { CreativeWriter.class, AudienceEditor.class,
StyleEditor.class })
String write(@V("topic") String topic, @V("style") String style, @V("audience") String audience);
}
@RegisterAiService
public interface CreativeWriter {
@Agent(name = "Creative writer", description = "Draft a short story about a topic.", outputKey = "story")
@SystemMessage("""
You are a creative fiction writer.
Write short, vivid stories, 4–6 sentences long.
""")
@UserMessage("""
Topic: {topic}
Write a short story about this topic.
""")
String draft(@V("topic") String topic);
}
@RegisterAiService
public interface AudienceEditor {
@Agent(name = "Audience editor", description = "Adapt story to a target audience.", outputKey = "story")
@SystemMessage("""
You rewrite stories to better fit the target audience.
Keep structure similar but adjust language, tone, and difficulty.
""")
@UserMessage("""
Audience: {audience}
Rewrite the story below so it is ideal for this audience.
Story:
{story}
""")
String adapt(@V("story") String story, @V("audience") String audience);
}
@RegisterAiService
public interface StyleEditor {
@Agent(name = "Style editor", description = "Adapt story to a specific writing style.", outputKey = "story")
@SystemMessage("""
You rewrite stories in the requested writing style
(for example: fantasy, noir, comedy, sci-fi).
Keep the meaning, adjust style.
""")
@UserMessage("""
Style: {style}
Rewrite the story below with this style.
Story:
{story}
""")
String restyle(@V("story") String story, @V("style") String style);
}
// --- 2) Parallel workflow: evening planner --------------------------------
/**
* Example of a parallel workflow that plans an evening.
* <p>
* The @ParallelAgent method fans out to three sub-agents in parallel and then returns a single aggregated
* EveningPlan.
* <p>
* The Quarkus Flow integration builds a fork-join style workflow where each branch represents one of the sub-agents
* below.
*/
@RegisterAiService
public interface EveningPlannerAgent {
/**
* LC4J post-processor that builds the final EveningPlan from the values written by the sub-agents + original
* inputs still in the scope.
*/
@Output
static EveningPlan toEveningPlan(@V("city") String city, @V("mood") Mood mood, @V("dinner") String dinner,
@V("drinks") String drinks, @V("activity") String activity) {
return new EveningPlan(requireNonNullElse(city, "unknown city"), mood != null ? mood : Mood.CHILL,
requireNonNullElse(dinner, "surprise dinner"), requireNonNullElse(drinks, "surprise drinks"),
requireNonNullElse(activity, "surprise activity"));
}
@ParallelAgent(outputKey = "plan", subAgents = { DinnerAgent.class, DrinksAgent.class, ActivityAgent.class })
EveningPlan plan(@V("city") String city, @V("mood") Mood mood);
}
// Not sure why we need to force this to avoid quarkus-langchain4j complaining about outputKeys.
// TODO: open an issue
public interface DumbAgent {
@Agent(outputKey = "city")
String city();
@Agent(outputKey = "mood")
Mood mood();
@Agent(outputKey = "topic")
String topic();
@Agent(outputKey = "style")
String style();
@Agent(outputKey = "audience")
String audience();
}
@RegisterAiService
public interface DinnerAgent {
@Agent(name = "Dinner planner", outputKey = "dinner")
@SystemMessage("""
You suggest a single, concrete dinner option in the given city
for a given mood. Be specific and short.
""")
@UserMessage("""
City: {city}
Mood: {mood}
Suggest where to have dinner (one place, one sentence).
""")
String suggestDinner(@V("city") String city, @V("mood") Mood mood);
}
@RegisterAiService
public interface DrinksAgent {
@Agent(name = "Drinks planner", outputKey = "drinks")
@SystemMessage("""
You suggest one place for drinks after dinner, matching the mood.
""")
@UserMessage("""
City: {city}
Mood: {mood}
Suggest where to have a drink (one place, one sentence).
""")
String suggestDrinks(@V("city") String city, @V("mood") Mood mood);
}
@RegisterAiService
public interface ActivityAgent {
@Agent(name = "Activity planner", outputKey = "activity")
@SystemMessage("""
You suggest one short activity to wrap up the evening.
""")
@UserMessage("""
City: {city}
Mood: {mood}
Suggest one activity, after dinner and drinks, to finish the evening.
""")
String suggestActivity(@V("city") String city, @V("mood") Mood mood);
}
public record EveningPlan(String city, Mood mood, String dinner, String drinks, String activity) {
}
}
What is happening here?
-
@SequenceAgent: Tells Quarkus Flow to generate a workflow that executes methods in a specific order (e.g., Writer → Editor). -
@ParallelAgent: Tells the engine to execute multiple agent calls concurrently (e.g., Restaurant + Movie). -
Automatic Discovery: Because these are CDI beans, Quarkus Flow finds them at build time and registers them as first-class workflows.
4. Explore in the Quarkus Flow Dev UI
Start (or restart) your application in dev mode:
./mvnw quarkus:dev
-
Open the Quarkus Dev UI (
http://localhost:8080/q/dev). -
Navigate to Quarkus Flow → Workflows.
-
You should see two brand-new workflows that you didn’t manually define:
story-creator-with-configurable-style-editorandevening-planner-agent. -
Click on
evening-planner-agentto execute the workflow and then view the auto-generated Mermaid diagram.
Notice how the topology compares to the manual DSL version. The engine has automatically mapped the sequential annotation to a series of CNCF "Function Tasks" and handled the data mapping between the internal AI services.
5. Summary & Takeaways
Congratulations! You have completed the entire Quarkus Flow Workshop. You have learned how to:
-
Build Reactive Orchestrations that are non-blocking by default.
-
Integrate External Services via HTTP and OpenAPI with automatic error mapping.
-
Build Event-Driven Workflows using YAML and Kafka.
-
Orchestrate AI Agents using both the powerful Java DSL and declarative annotations.
6. Finish Line
You’ve successfully mastered the main concepts of Quarkus Flow!
If you have suggestions, found issues, or want to contribute to the engine, please reach out via GitHub. We are excited to see what you build next!