Skip to content

Step 03 - Building Nested Agent Workflows

New Requirement: Comprehensive Car Management

The Miles of Smiles management team wants (again!) a more sophisticated car management system. When cars are returned, the system should automatically:

  1. Analyze feedback for both cleaning needs AND maintenance requirements
  2. Route cars appropriately — send to maintenance if needed, otherwise to car wash if needed
  3. Track all feedback sources — from rentals, car wash, and maintenance teams
  4. Update car conditions based on all collected feedback

This requires a more complex workflow that can handle parallel analysis and conditional routing.


What You’ll Learn

In this step, you will:

  • Build nested workflows, workflows that contain other workflows
  • Use parallel workflows to run multiple agents concurrently
  • Implement conditional workflows that route execution based on conditions
  • Understand activation conditions that control when agents execute
  • See how complex agent systems compose from simple building blocks

Understanding Nested Workflows

In Step 02, you built a simple sequence workflow with two agents running one after another. In this step, you’ll build a three-level nested workflow:

graph TD
    A[CarProcessingWorkflow<br/>Sequence] --> B[1. FeedbackWorkflow<br/>Parallel]
    A --> C[2. ActionWorkflow<br/>Conditional]
    A --> D[3. CarConditionFeedbackAgent<br/>Single Agent]

    B --> B1[CarWashFeedbackAgent]
    B --> B2[MaintenanceFeedbackAgent]

    C --> C1{Condition:<br/>Maintenance needed?}
    C1 -->|Yes| C2[MaintenanceAgent]
    C1 -->|No| C3{Condition:<br/>Car wash needed?}
    C3 -->|Yes| C4[CarWashAgent]
    C3 -->|No| C5[Skip]

The Flow:

  1. FeedbackWorkflow (Parallel): Analyzes feedback simultaneously from two perspectives:

    1. Does the car need maintenance?
    2. Does the car need washing?
  2. ActionWorkflow (Conditional): Routes the car based on the analysis:

    1. If maintenance needed → send to maintenance team
    2. Else if washing needed → send to car wash
    3. Else → do nothing
  3. CarConditionFeedbackAgent (Single): Updates the car’s condition based on all feedback


What Are We Going to Build?

App Blueprint

We’ll transform the car management system to handle:

  • Three feedback sources: rental returns, car wash returns, maintenance returns
  • Parallel analysis: concurrent evaluation for cleaning and maintenance needs
  • Conditional routing: intelligent decision-making about where to send each car
  • Comprehensive tracking: updated car conditions based on all feedback

Architecture Overview

The Nested Workflow Structure

sequenceDiagram
    participant User
    participant Main as CarProcessingWorkflow<br/>(Sequence)
    participant Feedback as FeedbackWorkflow<br/>(Parallel)
    participant Action as ActionWorkflow<br/>(Conditional)
    participant Condition as CarConditionFeedbackAgent
    participant Scope as AgenticScope

    User->>Main: processCarReturn(feedback)
    Main->>Scope: Initialize state

    Note over Main,Feedback: Step 1: Parallel Analysis
    Main->>Feedback: analyzeFeedback()
    par Parallel Execution
        Feedback->>Scope: CarWashFeedbackAgent<br/>Write: carWashRequest
    and
        Feedback->>Scope: MaintenanceFeedbackAgent<br/>Write: maintenanceRequest
    end

    Note over Main,Action: Step 2: Conditional Routing
    Main->>Action: processAction()
    Action->>Action: Check conditions
    alt Maintenance Required
        Action->>Scope: MaintenanceAgent executes
    else Car Wash Required
        Action->>Scope: CarWashAgent executes
    else Neither Required
        Action->>Action: Skip both agents
    end

    Note over Main,Condition: Step 3: Update Condition
    Main->>Condition: analyzeForCondition()
    Condition->>Scope: Write: carCondition

    Main->>User: Return CarConditions

Prerequisites

Before starting:

  • Completed Step 02 (or have the section-2/step-02 code available)
  • Application from Step 02 is stopped (Ctrl+C)

Warning: this chapter involves many steps

In order to build out the solution, you will need to go through quite a few steps. While it is entirely possible to make the code changes manually (or via copy/paste), we recommend starting fresh from Step 03 with the changes already applied. You will then be able to walk through this chapter and focus on the examples and suggested experiments at the end of this chapter.

Option 1: Continue from Step 02

If you want to continue building on your previous code, place yourself at the root of your project and copy the updated files:

cp ../step-03/src/main/resources/META-INF/resources/css/styles.css ./src/main/resources/META-INF/resources/css/styles.css
cp ../step-03/src/main/resources/META-INF/resources/js/app.js ./src/main/resources/META-INF/resources/js/app.js
cp ../step-03/src/main/resources/META-INF/resources/index.html ./src/main/resources/META-INF/resources/index.html
cp ../step-03/src/main/resources/import.sql ./src/main/resources/import.sql
cp ../step-03/src/main/java/com/carmanagement/model/CarStatus.java ./src/main/java/com/carmanagement/model/CarStatus.java
copy ..\step-03\src\main\resources\META-INF\resources\css\styles.css .\src\main\resources\META-INF\resources\css\styles.css
copy ..\step-03\src\main\resources\META-INF\resources\js\app.js .\src\main\resources\META-INF\resources\js\app.js
copy ..\step-03\src\main\resources\META-INF\resources\index.html .\src\main\resources\META-INF\resources\index.html
copy ..\step-03\src\main\resources\import.sql .\src\main\resources\import.sql
copy ..\step-03\src\main\java\com\carmanagement\service\CarService.java .\src\main\java\com\carmanagement\service\CarService.java
copy ..\step-03\src\main\java\com\carmanagement\model\CarStatus.java .\src\main\java\com\carmanagement\model\CarStatus.java

Navigate to the complete section-2/step-03 directory:

cd section-2/step-03

Part 1: Create Feedback Analysis Agents

We need two specialized agents to analyze feedback from different perspectives.

Step 1: Create the MaintenanceFeedbackAgent

This agent determines if a car needs maintenance based on feedback.

In src/main/java/com/carmanagement/agentic/agents, create MaintenanceFeedbackAgent.java:

MaintenanceFeedbackAgent.java
package com.carmanagement.agentic.agents;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.agentic.Agent;

/**
 * Agent that analyzes feedback to determine if maintenance is needed.
 */
public interface MaintenanceFeedbackAgent {

    @SystemMessage("""
        You are a car maintenance analyzer for a car rental company. Your job is to determine if a car needs maintenance based on feedback.
        Analyze the feedback and car information to decide if maintenance is needed.
        If the feedback mentions mechanical issues, strange noises, performance problems, or anything that suggests
        the car needs maintenance, recommend appropriate maintenance.
        Be specific about what type of maintenance is needed (oil change, tire rotation, brake service, engine service, transmission service).
        If no service of any kind, repairs or maintenance are needed, respond with 
        "MAINTENANCE_NOT_REQUIRED".
        Include the reason for your choice but keep your response short.
        """)
    @UserMessage("""
        Car Information:
        Make: {carMake}
        Model: {carModel}
        Year: {carYear}
        Previous Condition: {carCondition}

        Feedback:
        Rental Feedback: {rentalFeedback}
        Car Wash Feedback: {carWashFeedback}
        Maintenance Feedback: {maintenanceFeedback}
        """)
    @Agent(description = "Car maintenance analyzer. Using feedback, determines if a car needs maintenance.",
            outputName = "maintenanceRequest")
    String analyzeForMaintenance(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String carCondition,
            String rentalFeedback,
            String carWashFeedback,
            String maintenanceFeedback);
}

Key Points:

  • System message: Focuses on mechanical issues, performance problems, and maintenance needs
  • Specific output format: Returns “MAINTENANCE_NOT_REQUIRED” when no maintenance is needed (for easy conditional checking)
  • outputName: "maintenanceRequest" — stores the result in AgenticScope’s state
  • Three feedback sources: Analyzes rental, car wash, AND maintenance feedback

Step 2: Create the CarWashFeedbackAgent

This agent determines if a car needs washing based on feedback.

In src/main/java/com/carmanagement/agentic/agents, create CarWashFeedbackAgent.java:

CarWashFeedbackAgent.java
package com.carmanagement.agentic.agents;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.agentic.Agent;

/**
 * Agent that analyzes feedback to determine if a car wash is needed.
 */
public interface CarWashFeedbackAgent {

    @SystemMessage("""
        You are a car wash analyzer for a car rental company. Your job is to determine if a car needs washing based on feedback.
        Analyze the feedback and car information to decide if a car wash is needed.
        If the feedback mentions dirt, mud, stains, or anything that suggests the car is dirty, recommend a car wash.
        Be specific about what type of car wash is needed (exterior, interior, detailing, waxing).
        If no interior or exterior car cleaning services are needed based on the feedback, respond with "CARWASH_NOT_REQUIRED".
        Include the reason for your choice but keep your response short.
        """)
    @UserMessage("""
        Car Information:
        Make: {carMake}
        Model: {carModel}
        Year: {carYear}
        Previous Condition: {carCondition}

        Feedback:
        Rental Feedback: {rentalFeedback}
        Car Wash Feedback: {carWashFeedback}
        Maintenance Feedback: {maintenanceFeedback}
        """)
    @Agent(description = "Car wash analyzer. Using feedback, determines if a car wash is needed.",
            outputName = "carWashRequest")
    String analyzeForCarWash(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String carCondition,
            String rentalFeedback,
            String carWashFeedback,
            String maintenanceFeedback);
}

Key Points:

  • System message: Focuses on cleanliness issues — dirt, stains, smells
  • Specific output format: Returns “CARWASH_NOT_REQUIRED” when no washing is needed
  • outputName: "carWashRequest" — stores the result in AgenticScope’s state
  • Same inputs: Also analyzes all three feedback sources

Part 2: Create the Parallel Feedback Workflow

Now we’ll create a workflow that runs both feedback agents concurrently.

Step 3: Create the FeedbackWorkflow

In src/main/java/com/carmanagement/agentic/workflow, create FeedbackWorkflow.java:

FeedbackWorkflow.java
package com.carmanagement.agentic.workflow;

import com.carmanagement.agentic.agents.CarWashFeedbackAgent;
import com.carmanagement.agentic.agents.MaintenanceFeedbackAgent;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.agentic.declarative.ParallelAgent;
import dev.langchain4j.agentic.declarative.SubAgent;

/**
 * Workflow for processing car feedback in parallel.
 */
public interface FeedbackWorkflow {

    /**
     * Runs multiple feedback agents in parallel to analyze different aspects of car feedback.
     */
    @Agent(outputName = "feedbackResult")
    @ParallelAgent(outputName = "feedbackResult", subAgents = {
            @SubAgent(type = CarWashFeedbackAgent.class, outputName = "carWashRequest"),
            @SubAgent(type = MaintenanceFeedbackAgent.class, outputName = "maintenanceRequest")
    })
    String analyzeFeedback(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String carCondition,
            String rentalFeedback,
            String carWashFeedback,
            String maintenanceFeedback);
}

Let’s break it down:

@ParallelAgent Annotation

@ParallelAgent(
    outputName = "feedbackResult",
    subAgents = {
        @SubAgent(type = CarWashFeedbackAgent.class, outputName = "carWashRequest"),
        @SubAgent(type = MaintenanceFeedbackAgent.class, outputName = "maintenanceRequest")
    }
)

This defines a parallel workflow:

  • Both agents execute concurrently
  • Improves performance, no waiting for one to finish before the other starts
  • Each agent has its own outputName to store results independently

Why Parallel Here?

The two feedback agents analyze different aspects (cleaning vs. maintenance) and don’t depend on each other. Running them in parallel cuts the total execution time roughly in half!


Part 3: Create Action Agents

We need agents that can actually request maintenance and car washes.

Step 4: Create the MaintenanceAgent

This agent uses a tool to request maintenance services.

In src/main/java/com/carmanagement/agentic/agents, create MaintenanceAgent.java:

MaintenanceAgent.java
package com.carmanagement.agentic.agents;

import com.carmanagement.agentic.tools.MaintenanceTool;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.agentic.Agent;
import io.quarkiverse.langchain4j.ToolBox;

/**
 * Agent that determines what maintenance services to request.
 */
public interface MaintenanceAgent {

    @SystemMessage("""
        You handle intake for the car maintenance department of a car rental company.
        It is your job to submit a request to the provided requestMaintenance function to take action on the maintenance request.
        Be specific about what services are needed based on the maintenance request.
        """)
    @UserMessage("""
        Car Information:
        Make: {carMake}
        Model: {carModel}
        Year: {carYear}
        Car Number: {carNumber}

        Maintenance Request:
        {maintenanceRequest}
        """)
    @Agent(description = "Car maintenance specialist. Using car information and request, determines what maintenance services are needed.",
            outputName = "maintenanceAgentResult")
    @ToolBox(MaintenanceTool.class)
    String processMaintenance(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String maintenanceRequest);
}

Key Points:

  • Input: maintenanceRequest — reads the output from MaintenanceFeedbackAgent
  • Tool: MaintenanceTool — can request oil changes, brake service, etc. (We will create this tool later)
  • System message: Interprets the maintenance request and calls the appropriate tool

Step 5: Update the CarWashAgent

The CarWashAgent needs to read from the CarWashFeedbackAgent’s output.

Update src/main/java/com/carmanagement/agentic/agents/CarWashAgent.java:

CarWashAgent.java
package com.carmanagement.agentic.agents;

import com.carmanagement.agentic.tools.CarWashTool;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.agentic.Agent;
import io.quarkiverse.langchain4j.ToolBox;

/**
 * Agent that determines what car wash services to request.
 */
public interface CarWashAgent {

    @SystemMessage("""
        You handle intake for the car wash department of a car rental company.
        """)
    @UserMessage("""
        Taking into account all provided feedback, determine if the car needs a car wash.
        If the feedback indicates the car is dirty, has stains, or any other cleanliness issues,
        call the provided tool and recommend appropriate car wash services (exterior wash, interior cleaning, waxing, detailing).
        Be specific about what services are needed.
        If no specific car wash request is provided, request a standard exterior wash.

        Car Information:
        Make: {carMake}
        Model: {carModel}
        Year: {carYear}
        Car Number: {carNumber}

        Car Wash Request:
        {carWashRequest}
        """)
    @Agent(description = "Car wash specialist. Determines what car wash services are needed.",
            outputName = "carWashAgentResult")
    @ToolBox(CarWashTool.class)
    String processCarWash(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String carWashRequest);
}

Key change:

Now takes carWashRequest as input (instead of analyzing raw feedback itself). This follows the separation of concerns principle:

  • Feedback agents: Analyze and decide
  • Action agents: Execute based on decisions

Part 4: Create the Conditional Action Workflow

Now we’ll create a workflow that conditionally executes agents based on the feedback analysis.

Step 6: Create the ActionWorkflow

In src/main/java/com/carmanagement/agentic/workflow, create ActionWorkflow.java:

ActionWorkflow.java
package com.carmanagement.agentic.workflow;

import com.carmanagement.agentic.agents.CarWashAgent;
import com.carmanagement.agentic.agents.MaintenanceAgent;
import dev.langchain4j.agentic.declarative.ActivationCondition;
import dev.langchain4j.agentic.declarative.ConditionalAgent;
import dev.langchain4j.agentic.declarative.SubAgent;

/**
 * Workflow for processing car actions conditionally.
 */
public interface ActionWorkflow {

    /**
     * Runs the appropriate action agent based on the feedback analysis.
     */
    @ConditionalAgent(outputName = "actionResult", subAgents = {
            @SubAgent(type = MaintenanceAgent.class, outputName = "actionResult"),
            @SubAgent(type = CarWashAgent.class, outputName = "actionResult")
    })
    String processAction(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String carCondition,
            String carWashRequest,
            String maintenanceRequest);

    @ActivationCondition(MaintenanceAgent.class)
    static boolean activateMaintenance(String maintenanceRequest) {
        return isRequired(maintenanceRequest);
    }

    @ActivationCondition(CarWashAgent.class)
    static boolean activateCarWash(String carWashRequest) {
        return isRequired(carWashRequest);
    }

    private static boolean isRequired(String value) {
        return value != null && !value.isEmpty() && !value.toUpperCase().contains("NOT_REQUIRED");
    }
}

Let’s break it down:

@ConditionalAgent Annotation

@ConditionalAgent(
    outputName = "actionResult",
    subAgents = {
        @SubAgent(type = MaintenanceAgent.class, outputName = "actionResult"),
        @SubAgent(type = CarWashAgent.class, outputName = "actionResult")
    }
)

A conditional workflow is a sequence where each agent only runs if its condition is met.

@ActivationCondition Methods

@ActivationCondition(MaintenanceAgent.class)
static boolean activateMaintenance(String maintenanceRequest) {
    return isRequired(maintenanceRequest);
}

@ActivationCondition(CarWashAgent.class)
static boolean activateCarWash(String carWashRequest) {
    return isRequired(carWashRequest);
}

These methods control when each agent executes:

  • activateMaintenance: Returns true if maintenance is needed
  • activateCarWash: Returns true if car wash is needed

This logic is defined in the isRequired method:

private static boolean isRequired(String value) {
    return value != null && !value.isEmpty() && !value.toUpperCase().contains("NOT_REQUIRED");
}

The parameters are automatically extracted from AgenticScope’s state by name.

Execution Logic

if (activateMaintenance(maintenanceRequest) == true)
    → Execute MaintenanceAgent
    → Skip CarWashAgent (regardless of its condition)
else if (activateCarWash(carWashRequest) == true)
    → Execute CarWashAgent
else
    → Skip both agents

This implements priority routing: maintenance takes precedence over car wash.


Part 5: Update the Car Condition Agent

Step 7: Update CarConditionFeedbackAgent

The condition agent should now use the analyzed requests instead of raw feedback.

Update src/main/java/com/carmanagement/agentic/agents/CarConditionFeedbackAgent.java:

CarConditionFeedbackAgent.java
package com.carmanagement.agentic.agents;

import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;

/**
 * Agent that analyzes feedback to update the car condition.
 */
public interface CarConditionFeedbackAgent {

    @SystemMessage("""       
        You are a car condition analyzer for a car rental company. Your job is to determine the current condition of a car based on feedback.
        Analyze all feedback and the previous car condition to provide an updated condition description.
        Always provide a very short (no more than 200 characters) condition description, even if there's minimal feedback.
        Do not add any headers or prefixes to your response.
        """)
    @UserMessage("""
            Car Information:
            Make: {carMake}
            Model: {carModel}
            Year: {carYear}
            Previous Condition: {carCondition}

            Feedback from other agents:
            Car Wash Recommendation: {carWashRequest}
            Maintenance Recommendation: {maintenanceRequest}
            """)
    @Agent(description = "Car condition analyzer. Determines the current condition of a car based on feedback.",
            outputName = "carCondition")
    String analyzeForCondition(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String carCondition,
            String carWashRequest,
            String maintenanceRequest);
}

Key changes:

  • Now takes carWashRequest and maintenanceRequest as inputs
  • Uses the analyzed requests (which include reasoning) to determine condition
  • More accurate condition updates based on professional analysis

Part 6: Create Supporting Infrastructure

Step 8: Create the MaintenanceTool

In src/main/java/com/carmanagement/agentic/tools, create MaintenanceTool.java:

MaintenanceTool.java
package com.carmanagement.agentic.tools;

import com.carmanagement.model.CarInfo;
import com.carmanagement.model.CarStatus;
import dev.langchain4j.agent.tool.Tool;
import jakarta.enterprise.context.Dependent;
import jakarta.transaction.Transactional;

/**
 * Tool for requesting car maintenance operations.
 */
@Dependent
public class MaintenanceTool {

    /**
     * Requests maintenance for a car based on the provided parameters.
     *
     * @param carNumber The car number
     * @param carMake The car make
     * @param carModel The car model
     * @param carYear The car year
     * @param oilChange Whether to request an oil change
     * @param tireRotation Whether to request tire rotation
     * @param brakeService Whether to request brake service
     * @param engineService Whether to request engine service
     * @param transmissionService Whether to request transmission service
     * @param requestText The maintenance request text
     * @return A summary of the maintenance request
     */
    @Tool("Requests maintenance with the specified options")
    @Transactional
    public String requestMaintenance(
            Long carNumber,
            String carMake,
            String carModel,
            Integer carYear,
            boolean oilChange,
            boolean tireRotation,
            boolean brakeService,
            boolean engineService,
            boolean transmissionService,
            String requestText) {

        // In a more elaborate implementation, we might make an API call to a maintenance service here

        // Update car status to IN_MAINTENANCE
        CarInfo carInfo = CarInfo.findById(carNumber);
        if (carInfo != null) {
            carInfo.status = CarStatus.IN_MAINTENANCE;
            carInfo.persist();
        }

        StringBuilder summary = new StringBuilder();
        summary.append("Maintenance requested for ").append(carMake).append(" ")
               .append(carModel).append(" (").append(carYear).append("), Car #")
               .append(carNumber).append(":\n");

        if (oilChange) {
            summary.append("- Oil change\n");
        }

        if (tireRotation) {
            summary.append("- Tire rotation\n");
        }

        if (brakeService) {
            summary.append("- Brake service\n");
        }

        if (engineService) {
            summary.append("- Engine service\n");
        }

        if (transmissionService) {
            summary.append("- Transmission service\n");
        }

        if (requestText != null && !requestText.isEmpty()) {
            summary.append("Additional notes: ").append(requestText);
        }

        String result = summary.toString();
        System.out.println("\uD83D\uDE97 MaintenanceTool result: " + result);
        return result;
    }
}

Similar to CarWashTool, this tool:

  • Uses @Dependent scope (required for tool detection)
  • Provides maintenance options: oil change, tire rotation, brake service, etc.
  • Updates car status to IN_MAINTENANCE
  • Returns a summary of requested services

Step 9: Create the RequiredAction Model

We need a model to represent what action is required for a car.

In src/main/java/com/carmanagement/model, create RequiredAction.java:

RequiredAction.java
package com.carmanagement.model;

/**
 * Enum representing the type of possible required actions for car processing
 */
public enum RequiredAction {
    MAINTENANCE,
    CAR_WASH,
    NONE
}

Step 10: Update the CarConditions Model

Update src/main/java/com/carmanagement/model/CarConditions.java:

CarConditions.java
package com.carmanagement.model;

/**
 * Record representing the conditions of a car.
 *
 * @param generalCondition   A description of the car's general condition
 * @param requiredAction    Indicates the action required
 */
public record CarConditions(String generalCondition, RequiredAction requiredAction) {
}
Notice how it has changed from boolean carWashRequired to RequiredAction requiredAction to support three states.

Step 11: Add Maintenance Returns API

Update src/main/java/com/carmanagement/resource/CarManagementResource.java:

CarManagementResource.java
package com.carmanagement.resource;

import com.carmanagement.service.CarManagementService;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import org.jboss.resteasy.reactive.RestQuery;

/**
 * REST resource for car management operations.
 */
@Path("/car-management")
public class CarManagementResource {

    @Inject
    CarManagementService carManagementService;

    /**
     * Process a car return from rental.
     * 
     * @param carNumber The car number
     * @param rentalFeedback Optional rental feedback
     * @return Result of the processing
     */
    @POST
    @Path("/rental-return/{carNumber}")
    public Response processRentalReturn(Long carNumber, @RestQuery String rentalFeedback) {

        try {
            String result = carManagementService.processCarReturn(carNumber, rentalFeedback, "", "");
            return Response.ok(result).build();
        } catch (Exception e) {
            Log.error(e.getMessage(), e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity("Error processing rental return: " + e.getMessage())
                    .build();
        }
    }

    /**
     * Process a car return from car wash.
     * 
     * @param carNumber The car number
     * @param carWashFeedback Optional car wash feedback
     * @return Result of the processing
     */
    @POST
    @Path("/car-wash-return/{carNumber}")
    public Response processCarWashReturn(Long carNumber, @RestQuery String carWashFeedback) {

        try {
            String result = carManagementService.processCarReturn(carNumber, "", carWashFeedback, "");
            return Response.ok(result).build();
        } catch (Exception e) {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity("Error processing car wash return: " + e.getMessage())
                    .build();
        }
    }

    /**
     * Process a car return from maintenance.
     *
     * @param carNumber The car number
     * @param maintenanceFeedback Optional maintenance feedback
     * @return Result of the processing
     */
    @POST
    @Path("/maintenance-return/{carNumber}")
    public Response processMaintenanceReturn(Long carNumber, @RestQuery String maintenanceFeedback) {

        try {
            String result = carManagementService.processCarReturn(carNumber, "", "", maintenanceFeedback);
            return Response.ok(result).build();
        } catch (Exception e) {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity("Error processing maintenance return: " + e.getMessage())
                    .build();
        }
    }
}

This adds a new endpoint for the maintenance team to return cars with feedback, and adjusts the existing endpoints.


Part 7: Update the Main Workflow

Step 12: Update CarProcessingWorkflow

This is where everything comes together! Update the workflow to use nested workflows.

Update src/main/java/com/carmanagement/agentic/workflow/CarProcessingWorkflow.java:

CarProcessingWorkflow.java
package com.carmanagement.agentic.workflow;

import com.carmanagement.agentic.agents.CarConditionFeedbackAgent;
import com.carmanagement.model.CarConditions;
import com.carmanagement.model.RequiredAction;
import dev.langchain4j.agentic.declarative.Output;
import dev.langchain4j.agentic.declarative.SequenceAgent;
import dev.langchain4j.agentic.declarative.SubAgent;

/**
 * Workflow for processing car returns using a sequence of agents.
 */
public interface CarProcessingWorkflow {

    /**
     * Processes a car return by running feedback analysis and then appropriate actions.
     */
    @SequenceAgent(outputName = "carProcessingAgentResult", subAgents = {
            @SubAgent(type = FeedbackWorkflow.class, outputName = "carProcessingAgentResult"),
            @SubAgent(type = ActionWorkflow.class, outputName = "carProcessingAgentResult"),
            @SubAgent(type = CarConditionFeedbackAgent.class, outputName = "carProcessingAgentResult")
    })
    CarConditions processCarReturn(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String carCondition,
            String rentalFeedback,
            String carWashFeedback,
            String maintenanceFeedback);

    @Output
    static CarConditions output(String carCondition, String maintenanceRequest, String carWashRequest) {
        RequiredAction requiredAction;
        // Check maintenance first (higher priority)
        if (isRequired(maintenanceRequest)) {
            requiredAction = RequiredAction.MAINTENANCE;
        } else if (isRequired(carWashRequest)) {
            requiredAction = RequiredAction.CAR_WASH;
        } else {
            requiredAction = RequiredAction.NONE;
        }
        return new CarConditions(carCondition, requiredAction);
    }

    private static boolean isRequired(String value) {
        return value != null && !value.isEmpty() && !value.toUpperCase().contains("NOT_REQUIRED");
    }
}

Let’s break it down:

The Sequence

@SequenceAgent(outputName = "carProcessingAgentResult", subAgents = {
    @SubAgent(type = FeedbackWorkflow.class, outputName = "carProcessingAgentResult"),
    @SubAgent(type = ActionWorkflow.class, outputName = "carProcessingAgentResult"),
    @SubAgent(type = CarConditionFeedbackAgent.class, outputName = "carProcessingAgentResult")
})

Notice the subagents include workflows (FeedbackWorkflow, ActionWorkflow), not just agents!

This is workflow composition: workflows can contain other workflows.

The Execution Order

  1. FeedbackWorkflow (parallel): Analyzes feedback for both maintenance and car wash needs
  2. ActionWorkflow (conditional): Routes the car to maintenance or car wash based on analysis
  3. CarConditionFeedbackAgent (single): Updates the car’s overall condition

The @Output Method

@Output
static CarConditions output(String carCondition, String maintenanceRequest, String carWashRequest) {
    RequiredAction requiredAction;
    if (isRequired(maintenanceRequest)) {
        requiredAction = RequiredAction.MAINTENANCE;
    } else if (isRequired(carWashRequest)) {
        requiredAction = RequiredAction.CAR_WASH;
    } else {
        requiredAction = RequiredAction.NONE;
    }
    return new CarConditions(carCondition, requiredAction);
}

This static method extracts three values from AgenticScope’s state and combines them into the final result.


Part 8: Update the Service Layer

Step 13: Update CarManagementService

And to get things over the finish line, we need to update the car management service to handle the new workflow structure.

Update src/main/java/com/carmanagement/service/CarManagementService.java:

CarManagementService.java
package com.carmanagement.service;

import com.carmanagement.agentic.workflow.CarProcessingWorkflow;
import com.carmanagement.model.CarConditions;
import com.carmanagement.model.CarInfo;
import com.carmanagement.model.CarStatus;
import com.carmanagement.model.RequiredAction;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

/**
 * Service for managing car returns from various operations.
 */
@ApplicationScoped
public class CarManagementService {

    @Inject
    CarProcessingWorkflow carProcessingWorkflow;

    /**
     * Process a car return from any operation.
     *
     * @param carNumber The car number
     * @param rentalFeedback Optional rental feedback
     * @param carWashFeedback Optional car wash feedback
     * @param maintenanceFeedback Optional maintenance feedback
     * @return Result of the processing
     */
    @Transactional
    public String processCarReturn(Long carNumber, String rentalFeedback, String carWashFeedback, String maintenanceFeedback) {
        CarInfo carInfo = CarInfo.findById(carNumber);
        if (carInfo == null) {
            return "Car not found with number: " + carNumber;
        }

        // Process the car return using the workflow and get the AgenticScope
        CarConditions carConditions = carProcessingWorkflow.processCarReturn(
                carInfo.make,
                carInfo.model,
                carInfo.year,
                carNumber,
                carInfo.condition,
                rentalFeedback != null ? rentalFeedback : "",
                carWashFeedback != null ? carWashFeedback : "",
                maintenanceFeedback != null ? maintenanceFeedback : "");

        // Update the car's condition with the result from CarConditionFeedbackAgent
        carInfo.condition = carConditions.generalCondition();

        // If no action is required, then the car should be available for the next rental
        if (carConditions.requiredAction() == RequiredAction.NONE) {
            carInfo.status = CarStatus.AVAILABLE;
        }

        // Persist the changes to the database
        carInfo.persist();

        return carConditions.generalCondition();
    }
}

Key changes:

  • Now passes maintenanceFeedback parameter to the workflow
  • Uses RequiredAction enum to determine car status
  • Sets status to IN_MAINTENANCE or AT_CAR_WASH based on the required action

Try It Out

Whew!! That was a lot, right? But we’ve built a fully functional car management system with LangChain4j’s Agentic AI functionality! High-fives to everyone around you are encouraged! Let’s test it out to understand how everything works together.

Start the application (if it wasn’t already running):

./mvnw quarkus:dev

Open http://localhost:8080.

Notice the New UI

The Returns section now has a Maintenance Return tab:

Maintenance Returns Tab

Test the Complete Workflow

Enter feedback on the Maintenance Return tab for the Ford F-150:

buffed out the scratch. car could use a wash now.

Click Return.

What happens?

  1. Parallel Analysis (FeedbackWorkflow):

    1. MaintenanceFeedbackAgent: “MAINTENANCE_NOT_REQUIRED” (scratch fixed)
    2. CarWashFeedbackAgent: “Car wash needed for general cleaning”
  2. Conditional Routing (ActionWorkflow):

    1. Maintenance condition: false (not required)
    2. Car wash condition: true (required)
    3. → Executes CarWashAgent, sends car to car wash
  3. Condition Update (CarConditionFeedbackAgent):

    1. Updates car condition: “Scratch removed, clean overall”
  4. UI Update:

    1. Car status → AT_CAR_WASH
    2. Condition column updates

Check the Logs

Look for evidence of parallel execution:

🚗 CarWashTool result: Car wash requested for Ford F-150 (2021), Car #12:
- Exterior wash
Additional notes: Recommend an exterior car wash. The feedback suggests the car could use a wash after the scratch was buffed out.

How It All Works Together

Let’s trace a complete example with maintenance needed:

Example: “Strange engine noise”

Rental feedback: "Engine making strange knocking sound"
sequenceDiagram
    participant User
    participant Main as CarProcessingWorkflow
    participant Feedback as FeedbackWorkflow
    participant Action as ActionWorkflow
    participant Scope as AgenticScope State

    User->>Main: Return car with feedback
    Main->>Scope: Store: rentalFeedback="Engine knocking"

    rect rgb(255, 243, 205)
    Note over Main,Feedback: Parallel Workflow
    Main->>Feedback: Execute
    par
        Feedback->>Scope: MaintenanceFeedbackAgent<br/>Write: "Engine service needed - knocking sound"
    and
        Feedback->>Scope: CarWashFeedbackAgent<br/>Write: "CARWASH_NOT_REQUIRED"
    end
    end

    rect rgb(248, 215, 218)
    Note over Main,Action: Conditional Workflow
    Main->>Action: Execute
    Action->>Scope: Read: maintenanceRequest, carWashRequest
    Action->>Action: activateMaintenance() = true
    Action->>Scope: MaintenanceAgent executes<br/>Write: "Maintenance requested: engine service"
    Note over Action: CarWashAgent skipped (condition false)
    end

    Main->>Scope: CarConditionFeedbackAgent<br/>Write: "Requires engine inspection"

    Main->>Main: @Output combines results
    Main->>User: CarConditions(condition="Requires engine inspection", action=MAINTENANCE)

Understanding Workflow Composition

The power of this system comes from composability — complex workflows built from simple pieces. We keep control on the flow, while letting agents focus on their specific tasks.

Building Blocks

Component Type Purpose
CarWashFeedbackAgent Agent Analyzes cleaning needs
MaintenanceFeedbackAgent Agent Analyzes maintenance needs
FeedbackWorkflow Parallel Workflow Runs both analyses concurrently
CarWashAgent Agent Requests car washing
MaintenanceAgent Agent Requests maintenance
ActionWorkflow Conditional Workflow Routes to appropriate action
CarConditionFeedbackAgent Agent Updates car condition
CarProcessingWorkflow Sequence Workflow Orchestrates everything

Composition Hierarchy

CarProcessingWorkflow (Sequence)
├── FeedbackWorkflow (Parallel)
│   ├── CarWashFeedbackAgent
│   └── MaintenanceFeedbackAgent
├── ActionWorkflow (Conditional)
│   ├── MaintenanceAgent (with MaintenanceTool)
│   └── CarWashAgent (with CarWashTool)
└── CarConditionFeedbackAgent

This is a three-level nested workflow!


Key Takeaways

  • Workflows compose: Build complex systems by nesting simple workflows
  • Parallel workflows improve response time: Independent tasks run concurrently
  • Conditional workflows enable routing: Execute different paths based on runtime conditions
  • Activation conditions are powerful: Control agent execution with simple boolean logic
  • Separation of concerns: Analysis agents separate from action agents for clarity
  • Type safety throughout: Compile-time checks on the entire workflow structure

Experiment Further

1. Add Priority Levels

What if you wanted to add a third level of priority (e.g., emergency repairs)?

  • Add an EmergencyRepairFeedbackAgent
  • Update ActionWorkflow with a third condition
  • Ensure emergency repairs take highest priority

2. Make Feedback Analysis Sequential

Try changing FeedbackWorkflow from @ParallelAgent to @SequenceAgent. How does this affect performance? When might you want sequential analysis?

3. Add More Sophisticated Conditions

The ActionWorkflow currently uses simple isRequired() checks. Try adding:

  • Cost-based conditions (only send to maintenance if estimated cost < $500)
  • Time-based conditions (skip car wash if it was washed in last 24 hours)
  • Severity-based conditions (emergency repairs vs. routine maintenance)

4. Visualize the Workflow

Add logging to each agent and workflow to print when they start and finish. Observe the parallel execution in the logs!


Troubleshooting

Parallel agents not executing in parallel

Check that your system has multiple CPU cores and that the thread pool is configured properly. In development mode, Quarkus should handle this automatically.

Conditional workflow always/never executing certain agents
  • Verify your @ActivationCondition methods are correctly named
  • Check that parameter names match the outputName values exactly
  • Add logging to the condition methods to see what values they’re receiving
Error: Cannot find symbol ‘RequiredAction’

Make sure you created both:

  • The RequiredAction enum
  • Updated CarConditions to use it
Agents getting wrong input values

Remember that parameter names must match the outputName from previous agents or workflow inputs. Check for typos!


What’s Next?

You’ve built a sophisticated multi-level nested workflow combining sequence, parallel, and conditional execution!

In Step 04, you’ll learn about Agent-to-Agent (A2A) communication — connecting your workflows to remote agents running in separate systems!

Continue to Step 04 - Using Remote Agents (A2A)