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
89%
Does it follow best practices?
Impact
100%
4.76xAverage score across 2 eval scenarios
Passed
No known issues
Deterministic orchestration of agents. All builders come from AgenticServices and produce an agent that can itself be nested in another 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.
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");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).testExitAtLoopEnd(true) to force all sub-agents to run before testing the condition.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");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.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.ChatMemory — the framework throws an exception, since the agent is invoked repeatedly and independently.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.
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.
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
parallelBuilderfor LLM-heavy fan-out benefits from.executor(Executors.newVirtualThreadPerTaskExecutor())(Java 21) since each sub-agent blocks on HTTP.