09-mini-projects

Mini Project 6: Automated Weekly Report Generator

A research, draft, fact-check, and publish pipeline built on LangChain4j AgenticServices workflows — a scheduled batch job, not another chat interface.

August 15, 2026
spring-ailangchain4jagenticservicesworkflowbatchmini-project

Not Every Agent Needs a Chat Window

Most of this roadmap's projects assume a human typing a question and waiting for an answer. A weekly on-call summary or a sales digest doesn't work that way — nobody's sitting at a chat window on Monday morning waiting to ask for it. It needs to already exist in the team's inbox by 8am, generated on a schedule, from data nobody had to manually paste in.

This project builds exactly that: a research → draft → fact-check → publish pipeline that runs as a scheduled batch job, built primarily with LangChain4j's AgenticServices sequential and conditional workflow builders, and contrasted against the hand-rolled equivalent this roadmap's agentic-patterns guide already walked through.


Setup

groovy
// build.gradle
dependencies {
    implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'
    implementation 'dev.langchain4j:langchain4j-spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

This project assumes the "Spring AI and LangChain4j coexisting in one application" pattern from this roadmap's sixth phase — both frameworks' chat abstractions are configured side by side, each doing what it's better suited for.


1. Each Pipeline Stage as an AiService

LangChain4j's AiService interfaces declare each stage's contract — a plain Java interface, no implementation body:

java
interface ResearchAgent {
    @SystemMessage("Pull the key metrics and incidents from the provided data sources for the given week.")
    String research(String weekOf, String rawData);
}
 
interface DraftAgent {
    @SystemMessage("Draft a concise weekly report from the research notes. Use headers: Highlights, Incidents, Metrics.")
    String draft(String researchNotes);
}
 
interface FactCheckAgent {
    @SystemMessage("Compare the draft against the original research notes. List any claim in the draft not supported by the notes.")
    FactCheckResult factCheck(String draft, String researchNotes);
}
 
record FactCheckResult(boolean passed, List<String> unsupportedClaims) {}

Each interface is deliberately narrow — one job, one clear input/output contract — the same reasoning this roadmap's agentic-patterns guide gives for keeping individual agent responsibilities small enough to test and reason about in isolation.


2. Wiring the Sequential Workflow

AgenticServices.sequenceBuilder() chains stages without hand-written glue code for passing state between them:

java
@Configuration
public class ReportPipelineConfig {
 
    @Bean
    UntypedAgent weeklyReportPipeline(ChatModel model) {
        ResearchAgent research = AiServices.create(ResearchAgent.class, model);
        DraftAgent draft = AiServices.create(DraftAgent.class, model);
        FactCheckAgent factCheck = AiServices.create(FactCheckAgent.class, model);
 
        return AgenticServices.sequenceBuilder()
            .subAgents(research, draft, factCheck)
            .outputName("factCheckResult")
            .build();
    }
}

Compare this to the hand-rolled sequential chain from the agentic-patterns guide — research.apply(input) feeding directly into draft.apply(...), feeding into factCheck.apply(...), each step manually threading its output into the next call. sequenceBuilder() is that same shape, declared rather than wired by hand; reach for the hand-rolled version when a step's output needs non-trivial transformation before the next step can consume it, and the builder when it's a straight pass-through.


3. A Conditional Branch: Retry on Failed Fact-Check

A draft that fails fact-checking shouldn't publish silently — AgenticServices.conditionalBuilder() expresses the branch declaratively:

java
@Bean
UntypedAgent weeklyReportPipelineWithRetry(ChatModel model) {
    ResearchAgent research = AiServices.create(ResearchAgent.class, model);
    DraftAgent draft = AiServices.create(DraftAgent.class, model);
    FactCheckAgent factCheck = AiServices.create(FactCheckAgent.class, model);
 
    UntypedAgent draftAndCheck = AgenticServices.sequenceBuilder()
        .subAgents(draft, factCheck)
        .build();
 
    UntypedAgent draftCheckRetryOnce = AgenticServices.conditionalBuilder()
        .subAgents(agenticScope -> {
            FactCheckResult result = (FactCheckResult) agenticScope.readState("factCheckResult");
            return !result.passed();
        }, draftAndCheck) // re-run draft+check exactly once if the first pass failed
        .build();
 
    return AgenticServices.sequenceBuilder()
        .subAgents(research, draftAndCheck, draftCheckRetryOnce)
        .build();
}

This is the same reflection pattern (generate, critique, revise) this roadmap's agentic-patterns guide covers under "self-reflection loops" — the fact-check stage is the critique, and the conditional re-run is the revise step, bounded to a single retry rather than looping indefinitely. A report that fails fact-checking twice routes to a human editor instead of a third automatic attempt; unbounded reflection loops are a real cost and latency risk for a scheduled batch job nobody's watching in real time.


4. Scheduling and Publishing

Wire the whole pipeline to a @Scheduled job and hand the final artifact to wherever the team actually reads reports:

java
@Component
public class WeeklyReportJob {
 
    private final UntypedAgent weeklyReportPipeline;
    private final ReportDataSource dataSource;
    private final SlackPublisher publisher;
 
    @Scheduled(cron = "0 0 8 * * MON")
    public void generateAndPublish() {
        String weekOf = LocalDate.now().minusWeeks(1).toString();
        String rawData = dataSource.pullMetricsAndIncidents(weekOf);
 
        Map<String, Object> result = weeklyReportPipeline.invoke(Map.of(
            "weekOf", weekOf,
            "rawData", rawData
        ));
 
        FactCheckResult factCheck = (FactCheckResult) result.get("factCheckResult");
        if (factCheck.passed()) {
            publisher.publish((String) result.get("draft"));
        } else {
            publisher.flagForHumanReview((String) result.get("draft"), factCheck.unsupportedClaims());
        }
    }
}

Monday 8am, the report is already in Slack — or, if it failed its own fact-check twice, already sitting in an editor's queue with the specific unsupported claims flagged, instead of a blank inbox and someone remembering to ask the bot for it.


Putting It Together

The full pipeline is research → (draft → fact-check, with one conditional retry) → publish-or-flag, expressed as composed AgenticServices builders rather than a hand-written chain of method calls — declarative where the shape is a standard sequence-with-a-branch, with the hand-rolled equivalent from this roadmap's agentic-patterns guide as the fallback for any stage whose data-passing logic gets more complex than a straight handoff.


What's Next

Mini Project 9 (Incident Postmortem Drafting Agent) uses a similar draft-then-evaluate shape, but with a custom LLM-as-judge rubric instead of a fact-check-against-source-notes step — worth comparing the two evaluation approaches side by side.

Frequently asked questions

Why LangChain4j's AgenticServices here instead of Spring AI's advisor chain used throughout the rest of this roadmap?

Advisors are built for intercepting a single ChatClient call (memory, RAG, logging); AgenticServices is built for composing multiple distinct AiService calls into a multi-step workflow with branching and state passed between them. This project's shape — several genuinely separate stages, not one call with cross-cutting concerns attached — is exactly what AgenticServices is for, which is why this roadmap introduces both frameworks rather than picking one.

What happens if the scheduled job fails partway through — does it retry the whole pipeline?

Not automatically in this guide's version. Wrap the @Scheduled method's body in your normal batch-job failure handling (a try/catch that alerts on-call, or Spring Batch if the pipeline grows complex enough to need checkpointing and step-level retry) — AgenticServices handles the workflow's internal branching, not job-level infrastructure concerns like restart-from-failure.

Could the fact-check stage use structured output instead of returning a custom FactCheckResult from an AiService method?

That's exactly what's happening — LangChain4j's AiService interfaces support typed return values the same way Spring AI's .entity() does; FactCheckResult here is LangChain4j's version of the structured-output pattern from this roadmap's second phase, just expressed through an interface method's return type instead of a fluent .entity(Class) call.

Is one retry enough, or should the conditional branch allow more attempts?

One is a reasonable default for a scheduled job where a human safety net (flagging for review) already exists on the second failure — more retries mostly just delay reaching that safety net while adding latency and cost. Tune upward only if you have data showing a second automatic retry meaningfully improves the pass rate over routing to a human.