Orchestrate LangChain4j Agents and Patterns
LangChain4j provides powerful abstractions for interacting with LLMs and defining AI agents. However, AI processes rarely exist in isolation—they need to interact with external services, pause for human approval, and survive system restarts.
Quarkus Flow acts as the durable, observable orchestration layer for your AI. By wrapping your LangChain4j components into standard Flow Tasks, you gain:
-
Durability: Long-running agentic loops survive application restarts.
-
Observability: Full execution traces, logs, and Dev UI visualizers.
-
Integration: Seamlessly mix AI tasks with HTTP calls, messaging, and scheduled events.
1. Add the dependencies
Add the required extensions to your pom.xml. You will need the base LangChain4j dependencies, your chosen AI provider (e.g., Ollama or OpenAI), and optionally the dedicated Quarkus Flow integration.
<dependencies>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-agentic</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-ollama</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.flow</groupId>
<artifactId>quarkus-flow-langchain4j</artifactId>
</dependency>
</dependencies>
Configure your chosen provider in application.properties:
quarkus.langchain4j.ollama.base-url=http://localhost:11434
quarkus.langchain4j.ollama.chat-model.model=llama3.2
2. Pattern A: Orchestrating Simple Agents
The most common approach is to define a standard LangChain4j AI Service and orchestrate it directly as a task within your Java DSL workflow.
Define the Agent
Create a plain interface annotated with @RegisterAiService. Use @SystemMessage to define the system prompt and the expected output structure.
@RegisterAiService
@ApplicationScoped
@SystemMessage("""
You draft a short, friendly newsletter paragraph.
Return ONLY the final draft text (no extra markup).
""")
public interface DrafterAgent {
// Exactly two parameters: memoryId + one argument (brief)
@UserMessage("Brief:\n{{brief}}")
String draft(@MemoryId String memoryId,
@V("brief") String brief);
}
Compose the Agent in a Flow
Inject the agent (or call it via method reference) using the agent(name, fn, ResultType.class) task provider.
import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*;
public class NewsletterFlow extends Flow {
@Override
public Workflow descriptor() {
return workflow("newsletter-drafter")
.tasks(
// Call the agent, passing the global context as input
agent("draftAgent", drafterAgent::draft, String.class)
.inputFrom(".topic")
.exportAs("{ draft: . }")
)
.build();
}
}
Shape the Data
When piping multiple agents together, use Data Flow transformations to keep prompts clean and context tightly scoped:
-
inputFrom(…): Select exactly what part of the workflow context the agent sees. -
exportAs(…): Expose the agent’s output to the global context for the next task.
// The critic only sees the "draft" field, not the whole context
agent("criticAgent", criticAgent::critique, String.class)
.inputFrom("$.draft")
.exportAs(r -> Map.of("review", r, "status", r.needsRevision() ? "REVISION" : "OK"));
3. Pattern B: Orchestrating Agentic Subflows
LangChain4j provides declarative annotations like @SequenceAgent and @ParallelAgent to define complex multi-agent patterns. From Quarkus Flow’s perspective, these annotated methods are just Subflows.
You can define the complex internal routing of your AI using LangChain4j’s annotations, and then call that entire pattern as a single task from your overarching business workflow.
Define the Agentic Pattern
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) {
}
}
Call the Pattern from Quarkus Flow
Inject the annotated bean into your Flow class and orchestrate it alongside standard business tasks (like HTTP calls or emitting events).
import org.acme.langchain4j.Agents;
import jakarta.inject.Inject;
import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*;
public class OrderWorkflow extends Flow {
@Inject
Agents.EveningPlannerAgent eveningPlanner; // LangChain4j @ParallelAgent
@Override
public Workflow descriptor() {
return workflow("evening-planner")
.tasks(
// Call the LangChain4j pattern as a subflow task
function("planEvening", eveningPlanner::plan, Agents.EveningPlan.class)
.inputFrom("$.location")
.exportAs("{ plan: . }"),
// Continue with non-AI business logic
post("notify", "https://example.org/notify"),
emitJson("org.acme.plan.created", Agents.EveningPlan.class)
)
.build();
}
}
4. Adding Human-in-the-Loop (HITL) Guardrails
You can easily pause an AI orchestration to wait for human approval by combining an emit task with a listen task.
// 1. Ask a human for a review
emitJson("org.acme.email.review.required", CriticAgentReview.class),
// 2. Pause the workflow and wait for the human's response event
listen("waitHumanReview", toOne("org.acme.newsletter.review.done"))
.outputAs((java.util.Collection<Object> c) -> c.iterator().next()),
// 3. Branch based on the human's decision
switchWhenOrElse(
(HumanReview h) -> ReviewStatus.NEEDS_REVISION.equals(h.status()),
"draftAgent", // If rejected, loop back to the AI Drafter task
"sendNewsletter", // If approved, proceed to finalization
HumanReview.class
)
5. The Dev UI "Auto-Generation" Magic
If you only want to use LangChain4j’s @SequenceAgent or @ParallelAgent annotations without writing a larger Java DSL workflow, the quarkus-flow-langchain4j extension provides a massive developer experience boost.
When you start your application in dev mode (./mvnw quarkus:dev):
-
Quarkus Flow automatically discovers your
@SequenceAgentand@ParallelAgentmethods. -
It generates a standalone
WorkflowDefinitionfor each one in the background. -
It derives a JSON Schema from the agent’s method parameters.
Open the Quarkus Flow Dev UI, and you will see your LangChain4j agents listed as fully-fledged workflows. You can execute them via auto-generated HTML forms, view their internal topology using Mermaid diagrams, and inspect their execution traces—all without writing a single line of orchestration code.
See also
-
Data flow and context management — deeper dive into
inputFromandexportAs. -
Java DSL cheatsheet — a quick reference for all available workflow tasks.
-
Use messaging and events — how to emit and listen for CloudEvents.