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):

  1. Quarkus Flow automatically discovers your @SequenceAgent and @ParallelAgent methods.

  2. It generates a standalone WorkflowDefinition for each one in the background.

  3. 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