Generate Quarkus Flow workflows from LangChain4j Agentic annotations
This guide shows how to declare agentic workflow patterns using the LangChain4j Agentic Workflow API
(@SequenceAgent, @ParallelAgent, …) and let Quarkus Flow generate and register them as first-class workflows.
Choose your approach
Quarkus Flow supports three complementary ways to use LangChain4j:
-
Java DSL tasks — call LangChain4j beans from Flow tasks via
agent(…)orfunction(…). See Orchestrate LangChain4j agents with the Quarkus Flow Java DSL. -
Annotations → generated workflows (this guide) — declare patterns via annotations and let Quarkus Flow generate workflows.
-
Hybrid — call an annotated agentic workflow from a larger Java DSL workflow via
function(…). See 4. Hybrid: call an annotated agentic workflow from a larger Flow.
1. Add the dependencies
In your Quarkus project, add:
<dependencies>
<!-- LangChain4j integration with Quarkus Flow -->
<dependency>
<groupId>io.quarkiverse.flow</groupId>
<artifactId>quarkus-flow-langchain4j</artifactId>
</dependency>
<!-- Agentic workflows in Quarkus LangChain4j -->
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-agentic</artifactId>
</dependency>
<!-- Choose one LangChain4j provider, for example Ollama -->
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-ollama</artifactId>
</dependency>
</dependencies>
Configure your provider as usual, for example with Ollama:
quarkus.langchain4j.ollama.base-url=http://localhost:11434
quarkus.langchain4j.ollama.chat-model.model=llama3.2
2. Declare agentic workflows with annotations
Create a class (for example org.acme.langchain4j.Agents) and declare your agents and workflows:
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.agentic.declarative.SubAgent;
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 = { @SubAgent(type = CreativeWriter.class, outputKey = "story"),
@SubAgent(type = AudienceEditor.class, outputKey = "story"),
@SubAgent(type = StyleEditor.class, outputKey = "story") })
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.")
@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.")
@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.")
@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 = { @SubAgent(type = DinnerAgent.class, outputKey = "dinner"),
@SubAgent(type = DrinksAgent.class, outputKey = "drinks"),
@SubAgent(type = ActivityAgent.class, outputKey = "activity") })
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")
@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")
@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")
@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) {
}
}
When the application starts:
-
Quarkus LangChain4j creates the agent services.
-
Quarkus Flow discovers the
@SequenceAgentand@ParallelAgentmethods. -
For each one, it generates a
WorkflowDefinitionand registers it.
3. Run and explore in the Dev UI
Start your application in dev mode:
./mvnw quarkus:dev
Open the Dev UI in your browser (usually at http://localhost:8080/q/dev), then:
-
Navigate to the Quarkus Flow card.
-
You should see workflows whose names are derived from the agent service interfaces.
-
Click the play icon of one of these workflows.
For each workflow:
-
Quarkus Flow derives an input JSON Schema from the agent method parameters and auto-generates an HTML form.
-
You can submit the form to run the workflow.
-
The output is displayed in the bottom panel (for example a generated story or an
EveningPlan). -
You can switch to the Mermaid view to see the underlying workflow topology (sequence or fork-join).
4. Hybrid: call an annotated agentic workflow from a larger Flow
A common pattern is to keep agentic topology in annotations, but still orchestrate a bigger workflow in Quarkus Flow. For example, you can:
-
Use
@SequenceAgent/@ParallelAgentto define the internal agentic pattern. -
Inject the generated agentic bean.
-
Call it from a Java DSL workflow using a
function(…)task, then continue with non-AI tasks (HTTP, messaging, timers, HITL, …).
Example:
import org.acme.langchain4j.Agents;
import io.serverlessworkflow.api.types.Workflow;
import jakarta.inject.Inject;
import static io.serverlessworkflow.fluent.func.FuncWorkflowBuilder.workflow;
import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.emitJson;
import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.function;
import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.post;
public class OrderWorkflow extends Flow {
@Inject
Agents.EveningPlannerAgent eveningPlanner; // annotated agentic method (e.g., @ParallelAgent)
@Override
public Workflow descriptor() {
return workflow()
.tasks(function("planEvening", eveningPlanner::plan, Agents.EveningPlan.class)
.outputAs("$.plan"),
post("notify", "https://example.org/notify"),
emitJson("org.acme.plan.created", Agents.EveningPlan.class))
.build();
}
}
This keeps LangChain4j’s semantics for the agentic pattern (including AgenticScope and @Output mappers),
while letting Quarkus Flow orchestrate the broader business process.
5. Compare: calling agents directly vs running generated workflows
You can still inject and use the agents in your own code:
@Inject
Agents.StoryCreatorWithConfigurableStyleEditor storyCreator;
@Inject
Agents.EveningPlannerAgent eveningPlanner;
public void demo() {
String story = storyCreator.write("Quarkus", "sci-fi", "developers");
Agents.EveningPlan plan = eveningPlanner.plan("Toronto", Agents.Mood.ROMANTIC);
}
The important point:
-
When you call the agent method directly, Quarkus LangChain4j manages the
AgenticScopeand agent orchestration. -
When you run the generated workflow from the Dev UI, Quarkus Flow executes the same agentic logic via the agent bean, but adds:
-
Auto-generated forms from the derived input schema.
-
Mermaid diagrams and workflow topology visualization.
-
Workflow traces/telemetry and a workflow execution surface (instances, debugging, etc.).
-
6. Why use Quarkus Flow on top of Quarkus LangChain4j?
Using Quarkus Flow on top of Quarkus LangChain4j gives you:
-
A runtime control plane for your agentic workflows:
-
Visual topology.
-
Rich traces and logs.
-
Instance-level inspection.
-
-
Better experimentation/debugging via Dev UI with realistic inputs (including non-happy paths).
-
Easier integration with the rest of your system:
-
Chain agentic workflows with HTTP, messaging, listen/emit gates, etc.
-
Use the same workflow engine for non-AI processes.
-
In short: you keep the ergonomic LC4J agent APIs, and Quarkus Flow turns them into production-grade workflows with Dev UI, telemetry, and robustness.
See also
-
Orchestrate LangChain4j agents with the Quarkus Flow Java DSL — orchestrate LangChain4j agents as Java DSL tasks.
-
Agentic workflows with LangChain4j and Quarkus Flow — how the annotations integration works internally.