Step 03 - Composing Multiple Agentic Workflows
Composing Multiple Agentic Workflows
In the previous step, you created a sequential workflow that orchestrated two agents working together:
- The
CleaningAgentdetermined if a car needed cleaning - The
CarConditionFeedbackAgentupdated the car’s condition based on feedback
A single sequential pattern works well you just need agents to run one after another. However, real-world scenarios often require more sophisticated orchestration patterns.
In this step, you’ll learn how to compose workflows by nesting them within other workflows to build complex agentic systems, by creating multi-level orchestration patterns that combine mixed pattern executions.
New Requirement from Miles of Smiles Management: Comprehensive Car Management
The Miles of Smiles management team wants a more sophisticated car management system. When cars are returned, they want the system to automatically:
- Analyze feedback for both cleaning needs AND maintenance requirements simultaneously
- Route cars appropriately to maintenance if needed, otherwise to cleaning if needed
- Track all feedback sources from rentals, cleaning, and maintenance teams
- Update car conditions based on all collected feedback
These new features require more advanced orchestration between agents to route requests appropriately.
What You’ll Learn
In this step, you will:
- Understand how to nest workflows within workflows to create sophisticated multi-level orchestration
- Explore additional types of workflow patterns such as parallel and conditional
- See how
AgenticScopeflows through nested workflow levels
Composing Agentic Workflows
Let’s start with a bit of theory. In Step 02, you learned about the four fundamental workflow patterns: sequential, parallel, conditional, and loop. Now we’ll explore how to compose these patterns by nesting workflows within other workflows.
What is Agentic Workflow Nesting?
Workflow nesting means using a workflow as a sub-agent within another workflow. This allows you to:
- Combine patterns: Use parallel execution within a sequential flow
- Organize complexity: Break down complex orchestration into manageable pieces
- Reuse workflows: Use the same sub-workflow in multiple places
- Create hierarchies: Build multi-level agent systems
Example: Nested Agentic Workflow Architecture
graph TD
Main[Main Workflow<br/>Sequential] --> Sub1[Sub-Workflow 1<br/>Parallel]
Main --> Sub2[Sub-Workflow 2<br/>Conditional]
Main --> Agent[Single Agent]
Sub1 --> A1[Agent A]
Sub1 --> A2[Agent B]
Sub2 --> B1[Agent C]
Sub2 --> B2[Agent D]
style Main fill:#90EE90
style Sub1 fill:#87CEEB
style Sub2 fill:#FFD700
style Agent fill:#90EE90
This creates a three-level nested workflow where:
- The main sequential workflow coordinates the overall flow
- Sub-workflows handle specific orchestration patterns (parallel, conditional)
- Individual agents perform specific tasks
How AgenticScope Works with Nesting
When workflows are nested, the AgenticScope flows through all levels:
graph LR
A[Main Workflow<br/>AgenticScope] --> B[Sub-Workflow 1<br/>Same Scope]
B --> C[Agent A<br/>Same Scope]
B --> D[Agent B<br/>Same Scope]
A --> E[Sub-Workflow 2<br/>Same Scope]
Key points:
- All nested workflows and agents share the same
AgenticScopeinstance - Data written by any agent is available to all subsequent agents
- The scope maintains state throughout the entire execution hierarchy
What is Being Added?
Now that we have that bit of theory under out belt, we can continue to transform our car management system. We’re going to implement:
- 3 feedback sources: collect human feedback not only from the car rental returns team, but also from the cleaning team, and from the maintenance team
- Parallel analysis: do concurrent evaluation for both cleaning and maintenance needs
- Conditional routing: implement intelligent decision-making about where to send each car
- Update the system: add additional Tools the agents can call to update the car conditions in the database based on all feedback
The Complete Solution Architecture
The final system uses a 3-level nested workflow, similar to what we saw above in the theoretical part:
graph TB
Start([Car Return]) --> A[CarProcessingWorkflow<br/>Sequential - Level 1]
A --> B[Step 1: FeedbackWorkflow<br/>Parallel - Level 2]
B --> B1[CleaningFeedbackAgent]
B --> B2[MaintenanceFeedbackAgent]
B1 --> BEnd[Both complete]
B2 --> BEnd
BEnd --> C[Step 2: CarAssignmentWorkflow<br/>Conditional - Level 2]
C --> C1{Maintenance<br/>needed?}
C1 -->|Yes| C2[MaintenanceAgent]
C1 -->|No| C3{Cleaning<br/>needed?}
C3 -->|Yes| C4[CleaningAgent]
C3 -->|No| C5[Skip both]
C2 --> CEnd[Action complete]
C4 --> CEnd
C5 --> CEnd
CEnd --> D[Step 3: CarConditionFeedbackAgent<br/>Single Agent - Level 1]
D --> End([Updated Car Conditions])
style A fill:#90EE90
style B fill:#87CEEB
style C fill:#FFD700
style D fill:#90EE90
style Start fill:#E8E8E8
style End fill:#E8E8E8
style BEnd fill:#F0F0F0
style CEnd fill:#F0F0F0
The Flow:
-
FeedbackWorkflow (Parallel): Analyzes feedback simultaneously from two perspectives:
- Does the car need maintenance?
- Does the car need cleaning?
-
CarAssignmentWorkflow (Conditional): Routes the car based on the analysis:
- If maintenance needed, then send to maintenance team
- Else if cleaning needed, then send to cleaning
- Else: do nothing
-
CarConditionFeedbackAgent (Single): Updates the car’s condition based on all feedback
Key Implementation Details
How Each Level Works
Level 1: Main Sequential Workflow
The CarProcessingWorkflow orchestrates the entire process sequentially:
@SequenceAgent(outputKey = "carProcessingAgentResult",
subAgents = { FeedbackWorkflow.class, CarAssignmentWorkflow.class, CarConditionFeedbackAgent.class })
Notice that subAgents contains two workflows and one agent. Workflows are first-class citizens that can be nested just like agents.
Level 2a: Parallel Feedback Workflow
The FeedbackWorkflow runs multiple agents simultaneously:
@ParallelAgent(outputKey = "feedbackResult",
subAgents = { CleaningFeedbackAgent.class, MaintenanceFeedbackAgent.class })
The agents analyze different aspects (cleaning vs. maintenance) and don’t depend on each other, so running them concurrently improves performance.
Level 2b: Conditional Action Workflow
The CarAssignmentWorkflow uses activation conditions to intelligently route cars to the appropriate team based on the analysis it has done:
@ConditionalAgent(outputKey = "analysisResult",
subAgents = { MaintenanceAgent.class, CleaningAgent.class })
String processAction(
String carMake,
String carModel,
Integer carYear,
Long carNumber,
String carCondition,
String cleaningRequest,
String maintenanceRequest);
@ActivationCondition(MaintenanceAgent.class)
static boolean assignToMaintenance(String maintenanceRequest) {
return isRequired(maintenanceRequest);
}
@ActivationCondition(CleaningAgent.class)
static boolean assignToCleaning(String cleaningRequest) {
return isRequired(cleaningRequest);
}
private static boolean isRequired(String value) {
return value != null && !value.isEmpty() && !value.toUpperCase().contains("NOT_REQUIRED");
}
- If
assignToMaintenance()returnstrue→ MaintenanceAgent runs, CleaningAgent skipped - Else if
assignToCleaning()returnstrue→ CleaningAgent runs - Else → Both skipped
This implements priority routing: maintenance takes precedence over cleaning.
Try the Complete Solution
Now that you understand the architecture, let’s see it in action!
Start the Application
- Navigate to the complete solution directory:
Test Different Scenarios
Try these scenarios to see how the nested workflows route cars:
Scenario 1: Maintenance Priority
Enter the following text in the feedback field for the Honda Civic:
What happens:
flowchart TD
Start([Input: Engine making strange noise<br/>and car is dirty])
Start --> FW[FeedbackWorkflow<br/>Parallel Analysis]
FW --> MFA[MaintenanceFeedbackAgent]
FW --> CFA[CleaningFeedbackAgent]
MFA --> MR[Detects: maintenance needed ✓]
CFA --> CR[Detects: cleaning needed ✓]
MR --> CAW{CarAssignmentWorkflow<br/>Conditional Routing}
CR --> CAW
CAW -->|Maintenance has higher priority| MA[Route to MaintenanceAgent]
MA --> Result([Result: Car goes to maintenance])
style FW fill:#FAE5D3
style CAW fill:#D5F5E3
style MA fill:#F9E79F
style Result fill:#D2B4DE
Scenario 2: Cleaning Only
Enter the following text in the Mercedes Benz feedback field:
What happens:
flowchart TD
Start([Input: Car is very dirty<br/>but runs fine])
Start --> FW[FeedbackWorkflow<br/>Parallel Analysis]
FW --> MFA[MaintenanceFeedbackAgent]
FW --> CFA[CleaningFeedbackAgent]
MFA --> MR[Detects: no maintenance needed ✗]
CFA --> CR[Detects: cleaning needed ✓]
MR --> CAW{CarAssignmentWorkflow<br/>Conditional Routing}
CR --> CAW
CAW -->|No maintenance needed, 2nd priority is cleaning| CA[Route to CleaningAgent]
CA --> Result([Result: Car goes to cleaning])
style FW fill:#FAE5D3
style CAW fill:#D5F5E3
style CA fill:#F9E79F
style Result fill:#D2B4DE
Scenario 3: Maintenance Return
Now use the “Maintenance Return” tab and enter in the For F-150 field:
What happens:
- FeedbackWorkflow: Analyzes maintenance feedback
- CarAssignmentWorkflow: Routes to CleaningAgent
- Result: Car goes to cleaning after maintenance
Check the Logs
Watch the console output to see the workflow execution:
FeedbackWorkflow executing...
├─ CleaningFeedbackAgent analyzing...
└─ MaintenanceFeedbackAgent analyzing...
CarAssignmentWorkflow evaluating conditions...
└─ MaintenanceAgent activated
CarConditionFeedbackAgent updating...
Notice how the parallel agents execute simultaneously, then the conditional workflow makes routing decisions!
Why Nested Workflows Matter
Separation of Concerns
Each workflow has a single responsibility:
- FeedbackWorkflow: Analyze all feedback sources
- CarAssignmentWorkflow: Route cars to appropriate teams
- CarProcessingWorkflow: Orchestrate the complete process
This makes the system easier to understand, test, and modify.
Reusability
Workflows are reusable components. For example, you could use FeedbackWorkflow in other contexts:
// Use in a different workflow
@SequenceAgent(subAgents = {
FeedbackWorkflow.class, // Reuse the same workflow!
SomeOtherAgent.class
})
public interface DifferentWorkflow extends Workflow { }
Type Safety
The entire nested structure is type-checked at compile time:
// This won't compile if the workflow structure is invalid
CarProcessingWorkflow workflow = AiServices.create(
CarProcessingWorkflow.class,
chatLanguageModel
);
AgenticScope Across Levels
The AgenticScope flows through all levels of nesting:
sequenceDiagram
participant Main as CarProcessingWorkflow
participant Feedback as FeedbackWorkflow
participant Action as ActionWorkflow
participant Scope as AgenticScope
Main->>Scope: Store: rentalFeedback, cleaningFeedback, maintenanceFeedback
Note over Feedback: Level 2 - Parallel
Main->>Feedback: Execute
Feedback->>Scope: Write: maintenanceRequest, cleaningRequest
Note over Action: Level 2 - Conditional
Main->>Action: Execute
Action->>Scope: Read: maintenanceRequest, cleaningRequest
Action->>Scope: Execute selected agent
Main->>Scope: Final agent updates condition
All agents and workflows share the same scope, enabling seamless data flow across levels.
Optional: Implement It Yourself
If you want hands-on practice implementing these patterns, you can build the solution step-by-step. Be warned though, there are quite a few changes to make, so if you’re short on time and would like to learn about the next concepts, feel free to skip to the next step. As a middle-of-the-road solution, you can also read on and walk through the implementation steps in the source code of section-2/step-03.
What You’ll Build
Starting from your current code in section-2/step-02, you’ll add:
- Feedback Analysis Agents (MaintenanceFeedbackAgent, CleaningFeedbackAgent)
- Parallel Feedback Workflow to run them concurrently
- Action Agents (MaintenanceAgent, updated CleaningAgent)
- Conditional Action Workflow for intelligent routing
- Nested Main Workflow that orchestrates everything
- Supporting Infrastructure (tools, models, service updates)
Time: 60-90 minutes (20-30 minutes if you’re working directly from step-03)
Prerequisites
Before starting:
- Completed Step 02 (or have the
section-2/step-02code available) - Application from Step 02 is stopped (Ctrl+C)
If you want to continue building on your Step 02 code, you’ll need to copy the updated UI files from step-03:
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
Create Feedback Analysis Agents
MaintenanceFeedbackAgent
Create src/main/java/com/carmanagement/agentic/agents/MaintenanceFeedbackAgent.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 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, significant body damage 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, body work).
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}
Cleaning Feedback: {cleaningFeedback}
Maintenance Feedback: {maintenanceFeedback}
""")
@Agent(description = "Car maintenance analyzer. Using feedback, determines if a car needs maintenance.",
outputKey = "maintenanceRequest")
String analyzeForMaintenance(
String carMake,
String carModel,
Integer carYear,
Long carNumber,
String carCondition,
String rentalFeedback,
String cleaningFeedback,
String maintenanceFeedback);
}
Key Points:
- Focuses on mechanical issues, body damage, and maintenance needs
- Returns “MAINTENANCE_NOT_REQUIRED” for easy conditional checking
- Uses
outputKeyto store result in AgenticScope
CleaningFeedbackAgent
Create src/main/java/com/carmanagement/agentic/agents/CleaningFeedbackAgent.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 determine if a cleaning is needed.
*/
public interface CleaningFeedbackAgent {
@SystemMessage("""
You are a cleaning analyzer for a car rental company. Your job is to determine if a car needs cleaning based on feedback.
Analyze the feedback and car information to decide if a cleaning is needed.
If the feedback mentions dirt, mud, stains, or anything that suggests the car is dirty, recommend a cleaning.
Be specific about what type of cleaning is needed (exterior, interior, detailing, waxing).
If no interior or exterior car cleaning services are needed based on the feedback, respond with "CLEANING_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}
Cleaning Feedback: {cleaningFeedback}
Maintenance Feedback: {maintenanceFeedback}
""")
@Agent(description = "Cleaning analyzer. Using feedback, determines if a cleaning is needed.",
outputKey = "cleaningRequest")
String analyzeForCleaning(
String carMake,
String carModel,
Integer carYear,
Long carNumber,
String carCondition,
String rentalFeedback,
String cleaningFeedback,
String maintenanceFeedback);
}
Key Points:
- Focuses on cleanliness issues
- Returns “CLEANING_NOT_REQUIRED” when no cleaning needed
- Stores result in AgenticScope for later use
Create the Parallel Feedback Workflow
Create src/main/java/com/carmanagement/agentic/workflow/FeedbackWorkflow.java:
package com.carmanagement.agentic.workflow;
import com.carmanagement.agentic.agents.CleaningFeedbackAgent;
import com.carmanagement.agentic.agents.MaintenanceFeedbackAgent;
import dev.langchain4j.agentic.declarative.ParallelAgent;
/**
* Workflow for processing car feedback in parallel.
*/
public interface FeedbackWorkflow {
/**
* Runs multiple feedback agents in parallel to analyze different aspects of car feedback.
*/
@ParallelAgent(outputKey = "feedbackResult",
subAgents = { CleaningFeedbackAgent.class, MaintenanceFeedbackAgent.class })
String analyzeFeedback(
String carMake,
String carModel,
Integer carYear,
Long carNumber,
String carCondition,
String rentalFeedback,
String cleaningFeedback,
String maintenanceFeedback);
}
Key Points:
@ParallelAgentmakes both agents execute simultaneously- Both agents can read from and write to the shared
AgenticScope - Results are available to subsequent workflow steps
Create Car Specialist Agents
These agents will examine the analysis results and determine what should happen to the car based on their specialty (either maintenance or cleaning)
MaintenanceAgent
Create src/main/java/com/carmanagement/agentic/agents/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.",
outputKey = "analysisResult")
@ToolBox(MaintenanceTool.class)
String processMaintenance(
String carMake,
String carModel,
Integer carYear,
Long carNumber,
String maintenanceRequest);
}
Update CleaningAgent
Update src/main/java/com/carmanagement/agentic/agents/CleaningAgent.java:
package com.carmanagement.agentic.agents;
import io.quarkiverse.langchain4j.ToolBox;
import com.carmanagement.agentic.tools.CleaningTool;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
/**
* Agent that determines what cleaning services to request.
*/
public interface CleaningAgent {
@SystemMessage("""
You handle intake for the cleaning department of a car rental company.
""")
@UserMessage("""
Taking into account all provided feedback, determine if the car needs a cleaning.
If the feedback indicates the car is dirty, has stains, or any other cleanliness issues,
call the provided tool and recommend appropriate cleaning services (exterior wash, interior cleaning, waxing, detailing).
Be specific about what services are needed.
If no specific cleaning request is provided, request a standard exterior wash.
Car Information:
Make: {carMake}
Model: {carModel}
Year: {carYear}
Car Number: {carNumber}
Cleaning Request:
{cleaningRequest}
""")
@Agent(description = "Cleaning specialist. Determines what cleaning services are needed.",
outputKey = "analysisResult")
@ToolBox(CleaningTool.class)
String processCleaning(
String carMake,
String carModel,
Integer carYear,
Long carNumber,
String cleaningRequest);
}
Create the Conditional Car Assignment Workflow
Create src/main/java/com/carmanagement/agentic/workflow/CarAssignmentWorkflow.java:
package com.carmanagement.agentic.workflow;
import com.carmanagement.agentic.agents.CleaningAgent;
import com.carmanagement.agentic.agents.MaintenanceAgent;
import dev.langchain4j.agentic.declarative.ActivationCondition;
import dev.langchain4j.agentic.declarative.ConditionalAgent;
/**
* Workflow for assigning cars to appropriate teams based on feedback analysis.
*/
public interface CarAssignmentWorkflow {
/**
* Assigns the car to the appropriate team based on the feedback analysis.
*/
@ConditionalAgent(outputKey = "analysisResult",
subAgents = { MaintenanceAgent.class, CleaningAgent.class })
String processAction(
String carMake,
String carModel,
Integer carYear,
Long carNumber,
String carCondition,
String cleaningRequest,
String maintenanceRequest);
@ActivationCondition(MaintenanceAgent.class)
static boolean assignToMaintenance(String maintenanceRequest) {
return isRequired(maintenanceRequest);
}
@ActivationCondition(CleaningAgent.class)
static boolean assignToCleaning(String cleaningRequest) {
return isRequired(cleaningRequest);
}
private static boolean isRequired(String value) {
return value != null && !value.isEmpty() && !value.toUpperCase().contains("NOT_REQUIRED");
}
}
Key Points:
@ConditionalAgentevaluates conditions to determine which agent runs@ActivationConditionmethods returntrueto activate an agent- Maintenance has priority (checked first)
- Cleaning only runs if maintenance is not needed
Update CarConditionFeedbackAgent
Update src/main/java/com/carmanagement/agentic/agents/CarConditionFeedbackAgent.java to add the new maintenance recommendations:
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:
Cleaning Recommendation: {cleaningRequest}
Maintenance Recommendation: {maintenanceRequest}
""")
@Agent(description = "Car condition analyzer. Determines the current condition of a car based on feedback.",
outputKey = "carCondition")
String analyzeForCondition(
String carMake,
String carModel,
Integer carYear,
Long carNumber,
String carCondition,
String cleaningRequest,
String maintenanceRequest);
}
Create Supporting Infrastructure
Create a Maintenance Tool for function calling
Create src/main/java/com/carmanagement/agentic/tools/MaintenanceTool.java to add a function for creating a request to the maintenance team:
package com.carmanagement.agentic.tools;
import dev.langchain4j.agent.tool.Tool;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.Dependent;
/**
* 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")
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
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);
}
Log.info(" └─ MaintenanceAgent activated");
String result = summary.toString();
Log.debug("🚗 MaintenanceTool result: " + result);
return result;
}
}
Create CarAssignment Model
Create src/main/java/com/carmanagement/model/CarAssignment.java:
package com.carmanagement.model;
/**
* Enum representing the type of possible car assignments for car processing
*/
public enum CarAssignment {
MAINTENANCE,
CLEANING,
NONE
}
Update CarConditions Model
Update src/main/java/com/carmanagement/model/CarConditions.java:
package com.carmanagement.model;
/**
* Record representing the conditions of a car.
*
* @param generalCondition A description of the car's general condition
* @param carAssignment Indicates the action required
*/
public record CarConditions(String generalCondition, CarAssignment carAssignment) {
}
Update the main Car Processing Workflow
Now it’s finally time to compose the sub workflows in the main Car Processing workflow! We need to replace the direct call to the CleaningAgent with the FeedbackWorkflow that will analyze the feedback from the intake, and then the CarAssignmentWorkflow to actually assign the car to the appropriate team based on the analysis.
We’ll also update the outputKey and the output method to make sure the assignment happens with the right priority.
Update src/main/java/com/carmanagement/agentic/workflow/CarProcessingWorkflow.java:
package com.carmanagement.agentic.workflow;
import com.carmanagement.agentic.agents.CarConditionFeedbackAgent;
import com.carmanagement.model.CarConditions;
import com.carmanagement.model.CarAssignment;
import dev.langchain4j.agentic.declarative.Output;
import dev.langchain4j.agentic.declarative.SequenceAgent;
/**
* 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(outputKey = "carProcessingAgentResult",
subAgents = { FeedbackWorkflow.class, CarAssignmentWorkflow.class, CarConditionFeedbackAgent.class })
CarConditions processCarReturn(
String carMake,
String carModel,
Integer carYear,
Long carNumber,
String carCondition,
String rentalFeedback,
String cleaningFeedback,
String maintenanceFeedback);
@Output
static CarConditions output(String carCondition, String maintenanceRequest, String cleaningRequest) {
CarAssignment carAssignment;
// Check maintenance first (higher priority)
if (isRequired(maintenanceRequest)) {
carAssignment = CarAssignment.MAINTENANCE;
} else if (isRequired(cleaningRequest)) {
carAssignment = CarAssignment.CLEANING;
} else {
carAssignment = CarAssignment.NONE;
}
return new CarConditions(carCondition, carAssignment);
}
private static boolean isRequired(String value) {
return value != null && !value.isEmpty() && !value.toUpperCase().contains("NOT_REQUIRED");
}
}
Key Points:
@SequenceAgentruns the workflows and agents in order- Sub-agents can be other workflows (FeedbackWorkflow, CarAssignmentWorkflow)
- All agents share the same
AgenticScope - The
@Outputmethod retrieves the maintenanceRequest and cleaningRequest from the AgenticScope to determine what needs to happen next, and then returns this result.
Update the Service Layer
And finally, we’ll update the Car Management service to handle the result from the Agentic AI workflow and update the system accordingly.
Update src/main/java/com/carmanagement/service/CarManagementService.java:
package com.carmanagement.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import com.carmanagement.agentic.workflow.CarProcessingWorkflow;
import com.carmanagement.model.CarConditions;
import com.carmanagement.model.CarInfo;
import com.carmanagement.model.CarStatus;
import io.quarkus.logging.Log;
/**
* 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 cleaningFeedback Optional cleaning feedback
* @param maintenanceFeedback Optional maintenance feedback
* @return Result of the processing
*/
@Transactional
public String processCarReturn(Long carNumber, String rentalFeedback, String cleaningFeedback, String maintenanceFeedback) {
CarInfo carInfo = CarInfo.findById(carNumber);
if (carInfo == null) {
return "Car not found with number: " + carNumber;
}
Log.info("FeedbackWorkflow executing...");
Log.info(" ├─ CleaningFeedbackAgent analyzing...");
Log.info(" └─ MaintenanceFeedbackAgent analyzing...");
Log.info("CarAssignmentWorkflow evaluating conditions...");
// 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 : "",
cleaningFeedback != null ? cleaningFeedback : "",
maintenanceFeedback != null ? maintenanceFeedback : "");
Log.info("CarConditionFeedbackAgent updating...");
// Update the car's condition with the result from CarConditionFeedbackAgent
carInfo.condition = carConditions.generalCondition();
// Update the car status based on the required action
switch (carConditions.carAssignment()) {
case MAINTENANCE:
carInfo.status = CarStatus.IN_MAINTENANCE;
break;
case CLEANING:
carInfo.status = CarStatus.AT_CLEANING;
break;
case NONE:
carInfo.status = CarStatus.AVAILABLE;
break;
}
// Persist the changes to the database
carInfo.persist();
return carConditions.generalCondition();
}
}
Test Your Implementation
Once you’ve implemented all the parts:
-
Start your application:
-
Test with the scenarios described earlier
-
Compare your implementation with the complete solution in
section-2/step-03
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
CarAssignmentWorkflowwith 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 CarAssignmentWorkflow currently uses simple isRequired() checks. Try adding:
- Cost-based conditions (only send to maintenance if estimated cost < $500)
- Time-based conditions (skip cleaning if it was cleaned 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
@ActivationConditionmethods are correctly named - Check that parameter names match the
outputKeyvalues exactly - Add logging to the condition methods to see what values they’re receiving
Error: Cannot find symbol ‘CarAssignment’
Make sure you created both:
- The
CarAssignmentenum - Updated
CarConditionsto use it
Agents getting wrong input values
Remember that parameter names must match the outputKey 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!