CtrlK
BlogDocsLog inGet started
Tessl Logo

gamussa/langchain4j-agentic

Build and demo Java AI agent systems with langchain4j-agentic: workflow patterns, supervisor, custom Planner strategies (incl. the flagship typed-verdict / CriticResult-style critic pattern), plus MCP tools, A2A remote agents, build setup, and conference-demo storylines. Pinned to 1.15.0 / 1.15.0-beta25.

84

4.76x
Quality

89%

Does it follow best practices?

Impact

100%

4.76x

Average score across 2 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

workflow-patterns.mddocs/

Workflow Patterns

Deterministic orchestration of agents. All builders come from AgenticServices and produce an agent that can itself be nested in another workflow.


Sequential workflow

Agents run one after another; each output feeds the next. Use for ordered, dependent tasks ("prompt chaining").

CreativeWriter creativeWriter = AgenticServices
    .agentBuilder(CreativeWriter.class).chatModel(BASE_MODEL).outputKey("story").build();
AudienceEditor audienceEditor = AgenticServices
    .agentBuilder(AudienceEditor.class).chatModel(BASE_MODEL).outputKey("story").build();
StyleEditor styleEditor = AgenticServices
    .agentBuilder(StyleEditor.class).chatModel(BASE_MODEL).outputKey("story").build();

UntypedAgent novelCreator = AgenticServices
    .sequenceBuilder()
    .subAgents(creativeWriter, audienceEditor, styleEditor)
    .outputKey("story")
    .build();

Map input = Map.of(
    "topic", "dragons and wizards",
    "style", "fantasy",
    "audience", "young adults"
);
String story = (String) novelCreator.invoke(input);

All three editors share outputKey("story"), so each rewrites the same variable — the classic refinement chain. The AudienceEditor/StyleEditor read both story (from upstream) and their own audience/style arguments from the scope.

Typed workflow interface

Give the workflow its own interface for strongly typed invocation:

public interface NovelCreator {
    @Agent
    String createNovel(@V("topic") String topic, @V("audience") String audience, @V("style") String style);
}

NovelCreator novelCreator = AgenticServices
    .sequenceBuilder(NovelCreator.class)
    .subAgents(creativeWriter, audienceEditor, styleEditor)
    .outputKey("story")
    .build();
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");

Loop workflow

Invoke agents repeatedly until a condition holds or maxIterations is reached. Ideal for iterative refinement (generate → score → edit → re-score).

StyleScorer styleScorer = AgenticServices
    .agentBuilder(StyleScorer.class).chatModel(BASE_MODEL).outputKey("score").build();
StyleEditor styleEditor = AgenticServices
    .agentBuilder(StyleEditor.class).chatModel(BASE_MODEL).outputKey("story").build();

UntypedAgent styleReviewLoop = AgenticServices
    .loopBuilder()
    .subAgents(styleScorer, styleEditor)
    .maxIterations(5)
    .exitCondition(agenticScope -> agenticScope.readState("score", 0.0) >= 0.8)
    .build();
  • exitCondition takes a Predicate<AgenticScope>checked after every sub-agent invocation by default, exiting as soon as it's satisfied (minimizes calls).
  • Use testExitAtLoopEnd(true) to force all sub-agents to run before testing the condition.
  • An overload accepts a BiPredicate<AgenticScope, Integer> whose second argument is the iteration counter:
UntypedAgent styleReviewLoop = AgenticServices
    .loopBuilder()
    .subAgents(styleScorer, styleEditor)
    .maxIterations(5)
    .testExitAtLoopEnd(true)
    .exitCondition((agenticScope, loopCounter) -> {
        double score = agenticScope.readState("score", 0.0);
        return loopCounter <= 3 ? score >= 0.8 : score >= 0.6; // lower the bar after 3 tries
    })
    .build();

A loop is itself an agent, so it composes — e.g. a CreativeWriter followed by the styleReviewLoop in a sequence:

StyledWriter styledWriter = AgenticServices
    .sequenceBuilder(StyledWriter.class)
    .subAgents(creativeWriter, styleReviewLoop)
    .outputKey("story")
    .build();
String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy");

Parallel workflow

Run independent agents concurrently and merge their outputs.

FoodExpert foodExpert = AgenticServices
    .agentBuilder(FoodExpert.class).chatModel(BASE_MODEL).outputKey("meals").build();
MovieExpert movieExpert = AgenticServices
    .agentBuilder(MovieExpert.class).chatModel(BASE_MODEL).outputKey("movies").build();

EveningPlannerAgent eveningPlannerAgent = AgenticServices
    .parallelBuilder(EveningPlannerAgent.class)
    .subAgents(foodExpert, movieExpert)
    .executor(Executors.newFixedThreadPool(2))
    .outputKey("plans")
    .output(agenticScope -> {
        List<String> movies = agenticScope.readState("movies", List.of());
        List<String> meals  = agenticScope.readState("meals", List.of());
        List<EveningPlan> moviesAndMeals = new ArrayList<>();
        for (int i = 0; i < movies.size(); i++) {
            if (i >= meals.size()) break;
            moviesAndMeals.add(new EveningPlan(movies.get(i), meals.get(i)));
        }
        return moviesAndMeals;
    })
    .build();
List<EveningPlan> plans = eveningPlannerAgent.plan("romantic");
  • output(Function<AgenticScope, ?>) assembles sub-agent outputs into the final result. It is available in any pattern (not just parallel) when you need to combine rather than return a single scope value.
  • executor(...) is optional; without it an internal cached thread pool is used.

Parallel mapper workflow

A variation where the same sub-agent runs in parallel once per item in a collection (a concurrent "map").

public interface BatchHoroscopeAgent extends AgentInstance {
    @Agent
    List<String> generateHoroscopes(@V("persons") List<Person> persons);
}

PersonAstrologyAgent personAstrologyAgent = AgenticServices
    .agentBuilder(PersonAstrologyAgent.class).chatModel(BASE_MODEL).outputKey("horoscope").build();

BatchHoroscopeAgent agent = AgenticServices
    .parallelMapperBuilder(BatchHoroscopeAgent.class)
    .subAgents(personAstrologyAgent)
    .itemsProvider("persons")                       // which arg holds the collection
    .executor(Executors.newFixedThreadPool(3))
    .build();

List<Person> persons = List.of(
    new Person("Mario", "aries"),
    new Person("Luigi", "pisces"),
    new Person("Peach", "leo"));
List<String> horoscopes = agent.generateHoroscopes(persons);
  • itemsProvider names the collection argument; it can be omitted when there is exactly one iterable (Collection/array) argument.
  • Each invocation receives one item; results are aggregated into a list automatically.
  • Sub-agents must not use ChatMemory — the framework throws an exception, since the agent is invoked repeatedly and independently.

Conditional workflow

Invoke an agent only when a condition (over the AgenticScope) is satisfied. Common for routing after classification.

public enum RequestCategory { LEGAL, MEDICAL, TECHNICAL, UNKNOWN }
CategoryRouter routerAgent = AgenticServices
    .agentBuilder(CategoryRouter.class).chatModel(BASE_MODEL).outputKey("category").build();
MedicalExpert medicalExpert = AgenticServices
    .agentBuilder(MedicalExpert.class).chatModel(BASE_MODEL).outputKey("response").build();
LegalExpert legalExpert = AgenticServices
    .agentBuilder(LegalExpert.class).chatModel(BASE_MODEL).outputKey("response").build();
TechnicalExpert technicalExpert = AgenticServices
    .agentBuilder(TechnicalExpert.class).chatModel(BASE_MODEL).outputKey("response").build();

UntypedAgent expertsAgent = AgenticServices.conditionalBuilder()
    .subAgents(scope -> scope.readState("category", RequestCategory.UNKNOWN) == RequestCategory.MEDICAL, medicalExpert)
    .subAgents(scope -> scope.readState("category", RequestCategory.UNKNOWN) == RequestCategory.LEGAL, legalExpert)
    .subAgents(scope -> scope.readState("category", RequestCategory.UNKNOWN) == RequestCategory.TECHNICAL, technicalExpert)
    .build();

ExpertRouterAgent expertRouterAgent = AgenticServices
    .sequenceBuilder(ExpertRouterAgent.class)
    .subAgents(routerAgent, expertsAgent)   // classify first, then route
    .outputKey("response")
    .build();

String response = expertRouterAgent.ask("I broke my leg what should I do");

conditionalBuilder().subAgents(Predicate<AgenticScope>, agent...) registers each agent (or group) behind an activation predicate. Typically chained after a router agent that writes the discriminator (here category). First matching predicate wins; include a scope -> true default if you need a fallback.


Human-in-the-loop

Pause a workflow for human input. humanInput is a Function<String, String> — swap in any IO (console, HTTP endpoint, Slack DM).

UntypedAgent approval = AgenticServices.humanInTheLoopBuilder()
        .prompt(scope -> "Approve action: " + scope.readState("plan") + " ? (yes/no)")
        .humanInput(System.console()::readLine)
        .outputKey("approval")
        .build();

UntypedAgent gated = AgenticServices.sequenceBuilder()
        .subAgents(planner, approval, executor)   // gate before a destructive step
        .build();

Great for demos: show what the agent wants to do, then approve before it acts.


Planner + executor + critic (the workhorse topology)

Combines a sequence with a loop: plan once, then iterate execute→critique until a score threshold. This is the baseline the flagship pattern upgrades — see custom-strategy-critic-result.md, which replaces the bare Double score with a typed CriticResult<T>.

var planner  = AgenticServices.agentBuilder(Planner.class).chatModel(claudeOpus).build();
var executor = AgenticServices.agentBuilder(Executor.class)
        .chatModel(claudeSonnet)
        .tools(new FileSystemTools(), new ShellTool(), new WebFetchTool())
        .maxSequentialToolsInvocations(10)
        .build();
var critic   = AgenticServices.agentBuilder(Critic.class).chatModel(claudeSonnet).build();

UntypedAgent executeLoop = AgenticServices.loopBuilder()
        .subAgents(executor, critic)
        .maxIterations(8)
        .exitCondition(scope -> scope.readState("score", 0.0) >= 0.85)
        .build();

UntypedAgent system = AgenticServices.sequenceBuilder()
        .subAgents(planner, executeLoop)
        .outputKey("progress")
        .build();

Here Planner/Executor/Critic are plain @Agent interfaces (outputKey = plan / progress / score). For real control flow over the loop, prefer the typed-CriticResult Custom Strategy.

Note: a parallelBuilder for LLM-heavy fan-out benefits from .executor(Executors.newVirtualThreadPerTaskExecutor()) (Java 21) since each sub-agent blocks on HTTP.

docs

agent-configuration.md

agents-and-scope.md

custom-strategy-critic-result.md

declarative-api.md

demo-storylines.md

gotchas.md

index.md

mcp-and-a2a.md

pure-agentic.md

tools-memory-and-build.md

workflow-patterns.md

README.md

tile.json