Sitemap

Spring AI Recipe: Building a Graph-Based Agentic Workflow

6 min readMay 4, 2026

--

Completely autonomous agents are like unplanned road trips — flexible and full of adventure, but unpredictable. Graph-based workflows provide the roadmap to follow, while still allowing decisions along the way.

Press enter or click to view image in full size

In a previous recipe, we explored dynamic agentic behavior where an LLM decides which tools to use and what steps to take in order to achieve a goal.

But not every workflow should be so flexible and open-ended. For some circumstances deterministic workflows are better than autonomous planning. Sometimes you want:

  • predictable execution
  • explicit branching logic
  • controlled flow between steps
  • or deterministic handling of specific scenarios

That’s where graph-based workflows come in. Rather than allowing the agent to freely decide every step, a graph workflow defines an explicit flow of nodes and transitions between those nodes. The LLM still plays an important role, but the overall process is guided by a predefined structure.

In this recipe, we’ll use Spring AI Alibaba Graph to build a simple, but meaningful, graph-based customer support workflow.

Heads up: Defining a graph-based workflow involves a bit of work, so this recipe is going to be longer then earlier Spring AI recipes. You might want to refresh your drink of choice before getting started.

NOTE: This recipe has been updated since it’s original publication to reflect changes in Spring AI 2.0.0.

Starting the Project

Start with a basic Spring AI project. The example from the chat loop recipe is a good starting point.

One important note: At the time of writing, Spring AI Alibaba Graph appears to target Spring AI 1.1.x. However, simple testing with Spring AI 2.0.0-M5 also appears to work just fine.

Adding Dependencies

Add Spring AI Alibaba Graph:

implementation 'com.alibaba.cloud.ai:spring-ai-alibaba-graph-core'

This also requires the Alibaba BOMs in dependency management:

dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
mavenBom "com.alibaba.cloud.ai:spring-ai-alibaba-bom:${springAiAlibabaVersion}"
mavenBom "com.alibaba.cloud.ai:spring-ai-alibaba-extensions-bom:${springAiAlibabaVersion}"
}
}

And then define the versions:

ext {
set('springAiVersion', "2.0.0-M5")
set('springAiAlibabaVersion', '1.1.2.2')
}

The Workflow

The workflow we’ll build handles customer support requests. The graph itself is intentionally simple:

  1. Classify the request
  2. Route to either billing support or technical support

In illustrated form, the workflow looks something like this:

Custome support workflow

Even though the workflow is small, it demonstrates several important graph concepts:

  • nodes
  • edges
  • conditional transitions and branching
  • workflow state

Defining Workflow Nodes

In Spring AI Alibaba Graph, workflow nodes implement the NodeAction interface.

Each node:

  • receives the workflow state
  • performs some work
  • returns additional state to merge into the workflow

Classifying the Request

The first node determines whether the customer question is billing-related or technical:

public class ClassifySupportRequestNode implements NodeAction {
private static final Logger logger =
LoggerFactory.getLogger(ClassifySupportRequestNode.class);
private final ChatClient chatClient;
public ClassifySupportRequestNode(ChatClient chatClient) {
this.chatClient = chatClient;
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
logger.info("Classifying user question");
var question = state.value("user_question", String.class)
.orElseThrow();
var category = chatClient.prompt()
.system("""
Classify the customer support request as exactly one of:
billing
technical
Respond with only the category name.
""")
.user(question)
.call()
.content()
.trim()
.toLowerCase();
if (!category.equals("billing")
&& !category.equals("technical")) {
logger.info(
"Question category({}) unknown. Defaulting to technical.",
category);
category = "technical";
}
return Map.of("category", category);
}
}

A few important things are happening here:

  • The node pulls "user_question" from workflow state
  • It uses an LLM to classify the question
  • The result is added back into workflow state as "category"

Notice that the node itself does not decide where execution flows next. It only contributes state. The graph definition will decide how that state affects workflow routing.

Billing Support Node

Next, define the billing support handler:

public class BillingSupportNode implements NodeAction {
private static final Logger logger =
LoggerFactory.getLogger(BillingSupportNode.class);
private final ChatClient chatClient;
public BillingSupportNode(ChatClient chatClient) {
this.chatClient = chatClient;
}
@Override
public Map<String, Object> apply(OverAllState state)
throws Exception {
logger.info("Handling billing question.");
String question = state.value("user_question", String.class)
.orElseThrow();
var response = chatClient.prompt()
.system("""
You are a billing support assistant.
Write a brief, helpful response to the customer.
Do not promise refunds. Say that the billing team will
review the account.
""")
.user(question)
.call()
.content();
return Map.of("response", response);
}
}

This node:

  • retrieves the question
  • generates a billing-specific response
  • stores the response into workflow state

Technical Support Node

The technical support node works almost exactly the same way:

public class TechnicalSupportNode implements NodeAction {
private static final Logger logger =
LoggerFactory.getLogger(TechnicalSupportNode.class);
private final ChatClient chatClient;
public TechnicalSupportNode(ChatClient chatClient) {
this.chatClient = chatClient;
}
@Override
public Map<String, Object> apply(OverAllState state)
throws Exception {
logger.info("Handling technical question.");
String message = state.value("user_question", String.class)
.orElseThrow();
String response = chatClient.prompt()
.system("""
You are a technical support assistant.
Write a brief, helpful response to the customer.
Suggest one or two practical troubleshooting steps or
ask if they have tried turning it off and then back on again.
""")
.user(message)
.call()
.content();
return Map.of("response", response);
}
}

The only meaningful difference is the system prompt that guides the LLM’s behavior.

Building the Graph

Now that all nodes are defined, it’s time to connect them into a workflow. That happens by defining a CompiledGraph bean:

@Configuration
public class SupportGraphConfiguration {
static final String CLASSIFY = "classify";
static final String BILLING = "billing";
static final String TECHNICAL = "technical";

@Bean
CompiledGraph supportGraph(ChatClient chatClient)
throws GraphStateException {
return new StateGraph("support-triage", this::stateStrategies)
// Nodes
.addNode(CLASSIFY,
node_async(new ClassifySupportRequestNode(chatClient)))
.addNode(BILLING,
node_async(new BillingSupportNode(chatClient)))
.addNode(TECHNICAL,
node_async(new TechnicalSupportNode(chatClient)))
// Edges
.addEdge(START, CLASSIFY)
.addConditionalEdges(
CLASSIFY,
edge_async(state -> {
String category =
state.value("category", String.class)
.orElse(TECHNICAL);
return switch (category) {
case BILLING -> BILLING;
case TECHNICAL -> TECHNICAL;
default -> TECHNICAL;
};
}),
Map.of(
BILLING, BILLING,
TECHNICAL, TECHNICAL
))
.addEdge(BILLING, END)
.addEdge(TECHNICAL, END)
.compile();
}
private Map<String, KeyStrategy> stateStrategies() {
return Map.of(
"user_question", new ReplaceStrategy(),
"category", new ReplaceStrategy(),
"response", new ReplaceStrategy()
);
}
}

This is where the graph workflow truly comes together. A few important concepts appear here.

Nodes

Each node represents a unit of work. They are added to the graph by calling addNode().

Edges

Edges define transitions between nodes. They are added to the graph by calling addEdge(from, to).

START and END

There are two special nodes:

  • START
  • END

Execution begins at START and terminates at END.

Conditional Routing

The most interesting part is:

.addConditionalEdges(...)

This allows workflow execution to branch dynamically based on workflow state. In this case:

  • "billing" routes to the billing node
  • "technical" routes to the technical node

This is the core idea behind graph-based workflows:

  • structured flow
  • dynamic branching

Understanding Workflow State

Workflow state is essentially a shared map passed between nodes. Each node can: read state, add state, or replace state. The stateStrategies() method determines how updates from each node are merged into the workflow’s state.

Here we use ReplaceStrategy, meaning that new values overwrite previous values. Other options include:

  • AppendStrategy — Appends new values to any existing values, essentially forming a list of values collected as the workflow progresses.
  • MergeStrategy — A more advanced strategy in which complex values are intelligently merged with existing values. A Map, for example, would have it’s entries merged with any entries in an existing Map .

Running the Workflow

Finally, wire the graph into the chat loop:

@Bean
ApplicationRunner go(CompiledGraph compiledGraph) {
return args -> {
System.out.println("How can I help?\n");
try (Scanner scanner = new Scanner(System.in)) {
while (true) {
System.out.print("> ");
Map<String, Object> initialState = new HashMap<>();
initialState.put(
"user_question",
scanner.nextLine());
var workflowResult =
compiledGraph.invoke(initialState)
.orElseThrow();
System.out.println(
"\n - "
+ workflowResult.data().get("response"));
}
}
};
}

Unlike previous recipes that invoked ChatClient directly, this version invokes the graph itself.

The graph:

  1. receives initial workflow state
  2. executes nodes
  3. transitions between nodes
  4. returns final workflow state

Trying It Out

Run the application and try questions such as:

How can I help?

> I was overcharged $50

Or:

How can I help?

> My computer screen is frozen

You should:

  • receive an appropriate response
  • and see logging proving the workflow moved through classification…
  • then billing or technical support

Key Takeaway

Graph workflows occupy an interesting middle ground between:

  • rigid procedural logic
  • and completely autonomous agents

They provide:

  • structure
  • predictability
  • branching behavior
  • and controlled agentic execution

This makes them ideal for:

  • support workflows
  • approval flows
  • orchestrated multi-step systems
  • and any situation where you want explicit control over execution paths

There’s much more that you can do with Spring AI Alibaba Graph, such as looping and handling human-in-the-loop interactions. I’ll cover some of those things in future recipes.

You can find the code for this recipe and my other Spring AI recipes at https://github.com/habuma/spring-ai-recipes.

If you’re exploring Spring AI, I’ve written a book (Spring AI in Action) that covers the core foundations — and I’ll continue sharing newer patterns like this here as the ecosystem evolves. You can find my book on the publisher’s website, at Amazon, or anywhere you buy technical books.

--

--

Craig Walls
Craig Walls

Written by Craig Walls

Author Spring AI in Action, Spring in Action, and Build Talking Apps for Alexa