Example: Structured Output¶
This example shows two approaches to controlling agent output format:
- Typed output with a Java record -- the agent produces JSON that is automatically parsed into a strongly-typed object.
- Formatted text (Markdown) -- the agent produces well-formatted prose using
expectedOutputandresponseFormatinstructions, with no parsing required.
Example 1: Typed JSON Output¶
Use outputType when you need the agent's output as a structured Java object -- for downstream processing, serialization, validation, or API responses.
What It Does¶
- Researcher produces a structured report (title, list of findings, conclusion)
- The framework parses the agent's JSON into a
ResearchReportrecord - The caller accesses individual fields via
getParsedOutput(ResearchReport.class)
Full Code¶
import dev.langchain4j.model.openai.OpenAiChatModel;
import net.agentensemble.*;
import net.agentensemble.ensemble.EnsembleOutput;
import net.agentensemble.task.TaskOutput;
import java.util.List;
public class StructuredOutputExample {
record ResearchReport(String title, List<String> findings, String conclusion) {}
public static void main(String[] args) {
var model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var researcher = Agent.builder()
.role("Senior Research Analyst")
.goal("Find accurate, well-structured information on any given topic")
.llm(model)
.build();
var researchTask = Task.builder()
.description("Research the most important developments in AI agents in 2025")
.expectedOutput("A structured report with a title, a list of key findings, and a conclusion")
.agent(researcher)
.outputType(ResearchReport.class) // instruct the agent to produce JSON
.maxOutputRetries(3) // retry up to 3 times if JSON is invalid (default)
.build();
EnsembleOutput output = Ensemble.builder()
.agent(researcher)
.task(researchTask)
.build()
.run();
TaskOutput taskOutput = output.getTaskOutputs().get(0);
// Raw text is always available
System.out.println("Raw: " + taskOutput.getRaw());
// Typed access to the parsed object
ResearchReport report = taskOutput.getParsedOutput(ResearchReport.class);
System.out.println("Title: " + report.title());
System.out.println("Findings:");
report.findings().forEach(f -> System.out.println(" - " + f));
System.out.println("Conclusion: " + report.conclusion());
}
}
How It Works¶
When the task has outputType set:
- The agent's user prompt gains an
## Output Formatsection containing the JSON schema derived fromResearchReport. - After the agent produces its response, the framework extracts JSON from the raw text (handling prose, markdown fences, etc.).
- The JSON is deserialized into
ResearchReportusing Jackson. - If parsing fails, a correction prompt is sent to the agent showing the error and asking it to try again (up to
maxOutputRetriestimes). - If all retries are exhausted,
OutputParsingExceptionis thrown with the full error history.
Supported Types¶
| Type | Schema Example |
|---|---|
String |
"string" |
int, long, Integer, Long |
"integer" |
double, float, Double, Float |
"number" |
boolean, Boolean |
"boolean" |
List<String> |
["string"] |
List<MyRecord> |
[{...nested schema...}] |
Map<String, String> |
{"string": "string"} |
| Enum | "enum: VALUE1, VALUE2" |
| Nested record/POJO | Inlined nested object |
Example 2: Formatted Markdown Output¶
Use expectedOutput and Agent.responseFormat when you need well-structured prose (Markdown, bullet points, prose sections) without needing to parse the result into Java objects.
What It Does¶
- Researcher produces a research summary in plain text
- Writer produces a polished Markdown blog post incorporating the research
Full Code¶
import dev.langchain4j.model.openai.OpenAiChatModel;
import net.agentensemble.*;
import net.agentensemble.ensemble.EnsembleOutput;
import net.agentensemble.task.TaskOutput;
import net.agentensemble.workflow.Workflow;
import java.util.List;
import java.util.Map;
public class MarkdownOutputExample {
public static void main(String[] args) {
var model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
// Researcher produces plain-text findings
var researcher = Agent.builder()
.role("Senior Research Analyst")
.goal("Find accurate, well-sourced information on any given topic")
.llm(model)
.build();
// Writer produces Markdown -- responseFormat enforces the format
var writer = Agent.builder()
.role("Content Writer")
.goal("Write engaging, well-structured blog posts from research notes")
.responseFormat(
"Always format your response in Markdown. " +
"Include a title (# heading), an introduction paragraph, " +
"three sections with subheadings (## heading), and a conclusion.")
.llm(model)
.build();
var researchTask = Task.builder()
.description("Research the latest developments in {topic}")
.expectedOutput("A factual summary of key developments, major players, and future outlook")
.agent(researcher)
.build();
var writeTask = Task.builder()
.description("Write a 700-word blog post about {topic} based on the research provided")
.expectedOutput(
"A 700-word blog post in Markdown format with: " +
"an engaging title, introduction, three sections with subheadings, and a conclusion")
.agent(writer)
.context(List.of(researchTask)) // writer receives researcher's output
.build();
EnsembleOutput output = Ensemble.builder()
.agent(researcher)
.agent(writer)
.task(researchTask)
.task(writeTask)
.workflow(Workflow.SEQUENTIAL)
.build()
.run(Map.of("topic", "AI agents in enterprise software"));
// The final output is the writer's Markdown blog post
System.out.println(output.getRaw());
// Access individual task outputs
for (TaskOutput t : output.getTaskOutputs()) {
System.out.printf("[%s] completed in %s%n",
t.getAgentRole(), t.getDuration());
}
}
}
Key Points¶
responseFormatonAgentappends formatting instructions to the system prompt, guiding the LLM's output style across all tasks assigned to that agent.expectedOutputonTaskprovides task-specific quality guidance, describing length, structure, and content requirements.- The
rawfield ofTaskOutputalways contains the complete agent response as a string.
Running the Example¶
git clone https://github.com/AgentEnsemble/agentensemble.git
cd agentensemble
export OPENAI_API_KEY=your-api-key
# Default topic (AI agents in 2025)
./gradlew :agentensemble-examples:runStructuredOutput
# Custom topic
./gradlew :agentensemble-examples:runStructuredOutput --args="quantum computing"
The example runs both Part 1 (typed JSON output) and Part 2 (formatted Markdown output) for the same topic in a single execution so you can compare the two approaches side by side.
Combining Both Approaches¶
You can mix structured and plain-text tasks in the same ensemble:
// Task 1: structured output -- the researcher produces a parsed Java object
var researchTask = Task.builder()
.description("Research AI trends")
.expectedOutput("A structured report")
.agent(researcher)
.outputType(ResearchReport.class)
.build();
// Task 2: Markdown output -- the writer uses the research to produce prose
var writeTask = Task.builder()
.description("Write a blog post based on the research")
.expectedOutput("A 700-word blog post in Markdown")
.agent(writer)
.context(List.of(researchTask)) // receives the raw JSON as context
// no outputType -- plain text result
.build();
EnsembleOutput output = Ensemble.builder()
.agent(researcher).agent(writer)
.task(researchTask).task(writeTask)
.build()
.run();
// Access the structured output from task 1
ResearchReport report = output.getTaskOutputs().get(0).getParsedOutput(ResearchReport.class);
// Access the Markdown from task 2
String blogPost = output.getTaskOutputs().get(1).getRaw();