Chain of Agents: Sequential Prompt Chaining with LangChain4j
I am a Software Engineer from Dallas, Texas, USA, developing cyber security softwares.
The previous round left the demo with two open ideas in its "Future Experiments" list: chain of agents (sequential prompt chaining) and streaming function calling (which shipped alongside the crew). Everything before this ran agents either through a single AI service call or through a supervisor that routes to one worker at a time. This round fills the gap: a deterministic, ordered pipeline where each stage feeds its output into the next via a shared AgenticScope.
What Is a Chain of Agents?
LangChain4j's AgenticServices.sequenceBuilder() creates an UntypedAgent from a list of sub-agents that execute in order. Each sub-agent reads its input from the shared scope, writes its result to a named key, and the next agent picks up where the last one left off. There is no routing decision, no supervisor choosing which worker to call — it is a fixed, linear pipeline.
This is useful when the stages are known in advance and each one depends on the previous stage's output. The demo uses a blog-post generation pipeline:
OutlineAgent — takes a topic, produces a structured outline (
outputKey = "outline")DraftAgent — takes the outline, writes a full draft (
outputKey = "draft")EditorAgent — takes the draft, edits for clarity and flow (
outputKey = "edited")FormatAgent — takes the edited text, formats into a publish-ready Markdown post (
outputKey = "formatted")
flowchart LR
T["topic input"] --> OA["OutlineAgent<br/>outputKey=outline"]
OA --> DA["DraftAgent<br/>outputKey=draft"]
DA --> EA["EditorAgent<br/>outputKey=edited"]
EA --> FA["FormatAgent<br/>outputKey=formatted"]
FA --> OUT["formatted blog post"]
The Code
Each sub-agent is a plain Java interface with @SystemMessage, @UserMessage, and @Agent(outputKey = "..."):
@SystemMessage("You are a blog post outline specialist. Create a clear, structured outline "
+ "with a title, introduction, 3-5 main sections, and a conclusion.")
@UserMessage("Create a blog post outline for the following topic.\nTopic: {{topic}}")
public interface OutlineAgent {
@Agent(outputKey = "outline", description = "Creates a structured blog post outline")
String createOutline(@V("topic") String topic);
}
DraftAgent, EditorAgent, and FormatAgent follow the same pattern, each with its own outputKey and system message tailored to its role.
The pipeline is assembled in ChainOfAgentsService:
OutlineAgent outlineAgent = AgenticServices.agentBuilder(OutlineAgent.class)
.chatModel(chatModel).build();
DraftAgent draftAgent = AgenticServices.agentBuilder(DraftAgent.class)
.chatModel(chatModel).build();
EditorAgent editorAgent = AgenticServices.agentBuilder(EditorAgent.class)
.chatModel(chatModel).build();
FormatAgent formatAgent = AgenticServices.agentBuilder(FormatAgent.class)
.chatModel(chatModel).build();
this.pipeline = AgenticServices.sequenceBuilder()
.subAgents(outlineAgent, draftAgent, editorAgent, formatAgent)
.outputKey("formatted")
.build();
Two details:
Each sub-agent is built independently with its own
AgenticServices.agentBuilder()call, then composed viasequenceBuilder(). This is different from the crew, where the supervisor and sub-agents are built together.outputKey("formatted")tells the pipeline which scope key becomes the top-level return value ofpipeline.invoke().
To get the full trace (all intermediate outputs), use invokeWithAgenticScope():
ResultWithAgenticScope<String> result =
pipeline.invokeWithAgenticScope(Map.of("topic", topic));
String formatted = result.result();
String outline = result.agenticScope().readState("outline", (String) null);
String draft = result.agenticScope().readState("draft", (String) null);
The REST endpoint (POST /api/chain) and CLI command (/chain <topic>) both return the full trace, not just the final output.
Gotchas
Three things tripped us up during implementation:
@SystemMessageand@UserMessagego on the method, not the interface. Placing them at the interface level produces a compile error:annotation interface not applicable to this kind of declaration. This is the same rule as for crew sub-agents, but it is easy to forget when the interface has only one method.UntypedAgent.invoke()returnsObject, notMap<String, Object>. The return value is the value of the configuredoutputKey, not the full scope map. To access intermediate outputs, you must useinvokeWithAgenticScope()andagenticScope().readState(key).Scope keys must be unique across all sub-agents. Each agent's
outputKeywrites to a different key in the shared scope. If two agents share a key, the second one silently overwrites the first.
Testing
The ChainOfAgentsServiceTest uses a ScriptedSequenceChatModel — a fake ChatModel that returns canned responses in order, one per agent invocation. This makes the test fully offline with no API calls:
ScriptedSequenceChatModel chatModel = new ScriptedSequenceChatModel(
List.of(OUTLINE, DRAFT, EDITED, FORMATTED));
ChainOfAgentsService service = new ChainOfAgentsService(chatModel);
ChainPipelineResult result = service.runWithTrace("Test Topic");
assertThat(result.outline()).isEqualTo(OUTLINE);
assertThat(result.draft()).isEqualTo(DRAFT);
assertThat(result.edited()).isEqualTo(EDITED);
assertThat(result.formatted()).isEqualTo(FORMATTED);
What We Learned
The sequenceBuilder() fills a real gap between "one agent, one call" and "supervisor with routing." When the pipeline is fixed — outline, draft, edit, format — there is no need for a supervisor to decide which agent runs next. The shared AgenticScope is the glue: each agent writes its result, the next agent reads it, and the final outputKey gives you the finished product.
This also reinforces the value of the ModelRegistry abstraction. Every agent in the chain shares the same ChatModel bean, so switching providers at runtime switches the entire pipeline in one move. The registry keeps paying for itself.
Next up: The demo now has a supervisor-based crew (parallel delegation) and a chain-of-agents pipeline (sequential delegation). The remaining open idea is parallel agent execution — running multiple agents concurrently and merging their results.