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. Pattern C: Dynamic Runtime Workflows with UntypedAgent

For scenarios where you need to create AI workflows dynamically at runtime (without annotations), Quarkus Flow provides the FlowAgentsBuilderService. This CDI bean gives you programmatic access to LangChain4j’s workflow builders.

Why Use Runtime Workflows?

Use this pattern when:

  • Your workflow structure is determined by runtime data (e.g., user configuration, database records)

  • You’re building a workflow editor or no-code platform

  • You need to create temporary, one-off agent orchestrations

Using FlowAgentsBuilderService

Inject the builder service and use its factory methods to create sequential, parallel, loop, or conditional workflows:

    /**
     * Example: Sequential workflow for data processing pipeline.
     */
    public UntypedAgent createDataPipeline() {
        var validateAgent = AgenticServices.agentAction(scope -> {
            String data = scope.readState("input", "");
            boolean valid = data != null && !data.isEmpty();
            scope.writeState("valid", valid);
        });

        var processAgent = AgenticServices.agentAction(scope -> {
            String data = scope.readState("input", "");
            scope.writeState("processed", data.toUpperCase());
        });

        var saveAgent = AgenticServices.agentAction(scope -> {
            String processed = scope.readState("processed", "");
            scope.writeState("saved", true);
            scope.writeState("result", "Saved: " + processed);
        });

        return builderService.newSequential()
                .subAgents(validateAgent, processAgent, saveAgent)
                .build();
    }

Sequential Workflow Example

public UntypedAgent createDataPipeline() {
    var validateAgent = AgenticServices.agentAction(scope -> {
        String data = scope.readState("input", "");
        scope.writeState("valid", data != null && !data.isEmpty());
    });

    var processAgent = AgenticServices.agentAction(scope -> {
        String data = scope.readState("input", "");
        scope.writeState("processed", data.toUpperCase());
    });

    return builderService.newSequential()
            .subAgents(validateAgent, processAgent)
            .build();
}

Parallel Workflow Example

public UntypedAgent createParallelFetcher() {
    var fetchWeather = AgenticServices.agentAction(scope ->
        scope.writeState("weather", "Sunny, 25°C"));
    var fetchNews = AgenticServices.agentAction(scope ->
        scope.writeState("news", "Breaking: AI advances"));

    return builderService.newParallel()
            .subAgents(fetchWeather, fetchNews)
            .build();
}

Loop Workflow Example

public UntypedAgent createRetryWorkflow(int maxRetries) {
    var attemptTask = AgenticServices.agentAction(scope -> {
        int attempts = scope.readState("attempts", 0);
        scope.writeState("attempts", attempts + 1);
        scope.writeState("success", attempts >= 2); // Success on 3rd attempt
    });

    return builderService.newLoop()
            .maxIterations(maxRetries)
            .exitCondition((scope, iteration) -> scope.readState("success", false))
            .subAgents(attemptTask)
            .build();
}

Conditional Workflow Example

public UntypedAgent createUserRouter() {
    var adminFlow = AgenticServices.agentAction(scope ->
        scope.writeState("permissions", "ALL"));
    var userFlow = AgenticServices.agentAction(scope ->
        scope.writeState("permissions", "READ"));

    return builderService.newConditional()
            .subAgents(scope -> "admin".equals(scope.readState("userType", "")), adminFlow)
            .subAgents(scope -> "user".equals(scope.readState("userType", "")), userFlow)
            .build();
}

Complete examples available at: docs/modules/ROOT/examples/org/acme/langchain4j/DynamicWorkflowExample.java

Builder Methods

Method Description

newSequential()

Creates a sequential workflow where agents execute one after another

newParallel()

Creates a parallel workflow where agents execute concurrently

newLoop()

Creates a loop workflow with maxIterations() and optional exitCondition()

newConditional()

Creates a conditional/router workflow with predicate-based branching

Loop workflows use LangChain4j’s default behavior: if you don’t specify an exitCondition(), the loop runs until maxIterations is reached. The default maxIterations is Integer.MAX_VALUE.

5. 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
)

6. Build-Time Workflow Generation from Annotations

The quarkus-flow-langchain4j extension acts as an ahead-of-time compiler for LangChain4j’s Agentic Workflow annotations. When you annotate your AI service methods with @SequenceAgent, @ParallelAgent, @LoopAgent, or @ConditionalAgent, Quarkus Flow automatically generates full workflow definitions at build time.

Supported Annotations

Annotation Workflow Type

@SequenceAgent

Sequential execution of sub-agents in defined order

@ParallelAgent

Concurrent execution of sub-agents

@LoopAgent

Iterative execution with exit conditions

@ConditionalAgent

Conditional branching based on predicates

How It Works

When you start your application in dev mode (./mvnw quarkus:dev):

  1. Quarkus Flow scans your @RegisterAiService interfaces for Agentic Workflow annotations

  2. It generates a standalone WorkflowDefinition for each annotated method at build time

  3. It derives JSON Schema from the method parameters for input validation

  4. The generated workflows are registered as first-class citizens in the workflow registry

Open the Quarkus Flow Dev UI (http://localhost:8080/q/dev), and you will see your annotated agents listed as fully-fledged workflows. You can:

  • Execute them via auto-generated HTML forms

  • View their internal topology using Mermaid diagrams

  • Inspect execution traces and state transitions

  • Monitor performance and error rates

All of this happens without writing a single line of orchestration code.

See Agentic AI Topology and Internals for details on how the build-time compiler works.

See also