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:

  1. Java DSL tasks — call LangChain4j beans from Flow tasks via agent(…​) or function(…​). See Orchestrate LangChain4j agents with the Quarkus Flow Java DSL.

  2. Annotations → generated workflows (this guide) — declare patterns via annotations and let Quarkus Flow generate workflows.

  3. 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 @SequenceAgent and @ParallelAgent methods.

  • For each one, it generates a WorkflowDefinition and 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:

  1. Navigate to the Quarkus Flow card.

  2. You should see workflows whose names are derived from the agent service interfaces.

  3. 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 / @ParallelAgent to 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 AgenticScope and 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