Announcing Durable Execution for Spring AI Agents
Diagrid's new Spring AI integration brings durable execution to your agents. Add one dependency, crash the process mid-run, and watch the agent finish the job.
Javier Aliaga
Senior Software Engineer
Diagrid has released diagrid-spring-ai, a library that integrates the Catalyst runtime into Spring to bring durable execution, memory persistence, and agent discovery. The durability semantics (resume, checkpointing, attach-by-id) are built on the Dapr Workflow runtime, and the goal is to bring durable execution to any Spring AI application or agent without rewriting your application.
Durability alone does not make an agent production-ready. You still need authorization on privileged calls with identity, observability, and a way to trust the record of what ran. Together those make up what we call Agentic Durable Execution.
Whatever you use to build agents has to answer two questions. The first is how you build the agent: how tools are declared, how the model loop runs, how agents compose. The second is what happens when the process dies while an agent is halfway through its work.
The Java ecosystem has answered the first question thoroughly. In the last two years a handful of credible frameworks have appeared, and they split two ways. Some give you an Agent class and control when it runs. Others give you the building blocks and leave the composition to you. Spring AI took the second route, shipping the tool-calling loop, memory, and structured output along with official guidance for composing agents out of them, and its docs are explicit about preferring "simplicity and composability over complex frameworks." Either way, building an agent on the JVM is a solved problem.
Throughout this post, "agent" means what Spring AI's agentic patterns guidance means: a model calling tools in a loop until it produces an answer. A Spring AI agent is one you composed that way.
The second question has fewer answers. This post answers it, and two more follow:
- This post. Why a crash hurts agents more than it ever hurt ordinary services, and the fix in practice. An agent killed mid-run finishes the job on restart, without being re-triggered.
- Why durability belongs below the framework. How to judge the answers you will find, because they are not all the same answer, what failure detection actually requires, and what "exactly-once" means.
- Beyond the call: durable memory, agent discovery, and tracing. The rest of the platform around a durable agent.
- Design notes: execution identity and exactly-once tools. The internals and trade-offs underneath the zero-code promise.
An agent turn is a small distributed transaction
A web request that dies mid-flight is usually cheap to lose. The client retries, the handler re-runs, and if the handler is idempotent, nobody notices.
Here is what an agent looks like in Spring AI, with no reliability machinery at all. This is the code whose crash behavior the rest of this post is about:
@Configuration
public class EventPlannerAgentConfig {
private static final String SYSTEM = """
You are an event planner. Call all three tools in sequence:
1. First call step_one_search with the city name
2. Then call step_two_compare with the result from step 1
3. Finally call step_three_confirm with the result from step 2
Do NOT skip any steps.""";
@Bean("spring-ai-event-planner")
ChatClient eventPlanner(ChatClient.Builder builder) {
return builder.defaultSystem(SYSTEM).build();
}
}
@RestController
class EventPlannerController {
private final ChatClient chatClient;
public EventPlannerController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@PostMapping("/run")
RunResponse run(@RequestBody RunRequest request) {
return new RunResponse(
chatClient.prompt().user(request.prompt()).call().content());
}
}Nothing in this code looks dangerous. The danger is what call() does at runtime, and that behavior breaks the cheap-to-lose assumption.
It is long. That single call() is a loop. Spring AI sends the conversation to the model, executes the tools the model asks for, feeds the results back, and repeats until the model produces an answer. Several model calls, several tool calls, seconds to minutes of wall-clock time. The longer a request runs, the more likely a deploy, an OOM kill, or a node eviction lands in the middle of it. Agents turn rare mid-request crashes into routine ones.
It accumulates state that exists nowhere else. Between iterations of that loop, the whole transcript so far lives in the JVM heap. Which tools were called, what they returned, what the model concluded from them. Lose the process and you lose the run's progress along with its response.
Its steps have side effects. The bookFlight tool charges the customer. If the process dies after bookFlight returns but before the model composes its answer, the work is gone, but the charge is not. Retry the request and the agent books a second flight. The retry that was harmless for an idempotent web handler becomes a double booking.
So a crash mid-turn forces a choice between two failures: drop the run and lose the work, or replay the run and repeat the side effects. Any answer to the second question has to do better than both. Resume the run, skip what already completed, finish the rest.
Catalyst brings reliability and recovery
Here is the behavior we are after, from the Spring AI event-planner quickstart. The agent calls three tools in sequence, with a crash planted in tool two. The agent then recovers and completes the third tool call.
# Start the agent and ask the agent to plan an event.
# Tool 2 crashes the process mid-run.
$ curl -X POST http://localhost:8080/run \
-H "Content-Type: application/json" \
-d '{"prompt": "Find a venue in Austin for a company gala"}'
# The app log, up to the moment it dies:
>>> TOOL 1: Searching venues in 'Austin'...
>>> TOOL 1 COMPLETE: Found 3 venues
>>> TOOL 2: Comparing venues...
# the process exits here. The curl above never gets a response.
# Remove the planted crash and restart the app
# Nothing is re-triggered. The workflow resumes on its own:
>>> TOOL 2: Comparing venues...
>>> TOOL 2 COMPLETE: Grand Ballroom is the best option
>>> TOOL 3: Confirming booking...
>>> TOOL 3 COMPLETE: Booking confirmed for Grand BallroomThe workflow resumed without being re-triggered. The original HTTP request died with the process, but the run it started did not. The first LLM call and the first tool call had already completed, so the runtime replayed them from persisted history instead of executing them again. TOOL 1 never logs a second time. The only work done twice is the tool that was interrupted mid-flight.
This agent is a Spring AI app. A ChatClient, three @Tool methods and a REST endpoint. It is an agent in the sense Spring AI's agentic-patterns guidance describes: a loop that calls an LLM, runs the tools the LLM asks for, and repeats until there is a final answer. The app has no durability code, and the crash is a single line in tool two to make the process die at a known point. The quickstart walks the whole sequence on Catalyst: create a project with agent infrastructure enabled, run, crash, restart, watch it resume.
One dependency
<dependency>
<groupId>io.diagrid</groupId>
<artifactId>diagrid-spring-ai-starter</artifactId>
<version>0.2.0</version>
</dependency>Two things make the agent above durable. Neither is a change to the agent's logic.
First, add the starter above. It is on Maven Central, and it configures itself once it is on the classpath. There is no annotation to add and nothing to switch on.
Second, point the app at a Catalyst project. Catalyst keeps the execution record, watches the run, and recovers it, so there is nothing to install next to your app and no infrastructure to operate.
That is the whole change. Here is the event-planner agent again, to show what did not change:
@Configuration
public class EventPlannerAgentConfig {
private static final String SYSTEM = """
You are an event planner. Call all three tools in sequence:
1. First call step_one_search with the city name
2. Then call step_two_compare with the result from step 1
3. Finally call step_three_confirm with the result from step 2
Do NOT skip any steps.""";
@Bean("spring-ai-event-planner")
ChatClient eventPlanner(ChatClient.Builder builder) { // <-- inject the builder
return builder.defaultSystem(SYSTEM).build();
}
}
@RestController
class EventPlannerController {
private final ChatClient chatClient;
public EventPlannerController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@PostMapping("/run")
RunResponse run(@RequestBody RunRequest request) {
return new RunResponse(
chatClient.prompt().user(request.prompt()).call().content());
}
}No line in this class mentions durability, workflows, or Catalyst. The one requirement is on the marked line: build your ChatClient from the injected ChatClient.Builder. That is already the idiomatic Spring AI pattern, and it is what lets the starter attach durability to every call that client makes. Build a client by hand with ChatClient.builder(chatModel) instead and you bypass Spring's auto-configuration, which leaves the agent silently not durable. The Spring AI agents docs cover that case.
The rest is ordinary Spring AI. The three tools are plain @Tool methods on a @Component, and no interface, base class, or workflow definition appears anywhere in the app. The crash from the demo above is one line inside tool two, Runtime.getRuntime().halt(1), which kills the JVM without running shutdown hooks so nothing in the app gets a chance to save its work on the way out.
You need Java 17 or newer, Spring Boot 4 with Spring AI 2.0, and a Catalyst project. Java 21 is worth the upgrade, since a durable call blocks while the workflow runs and virtual threads make that wait nearly free.
What actually happens on .call()
The starter registers a Spring AI CallAdvisor at the end of the advisor chain, after your memory, RAG, and logging advisors have shaped the request and just before the framework would invoke the LLM. Instead of letting the loop run in-process, the advisor hooks into the runtime as a workflow:
- The agent loop becomes a Dapr Workflow instance.
- Each LLM call is a checkpointed step. Its result is persisted before the loop continues.
- Each tool call is its own checkpointed step, persisted the same way.
- The caller blocks until the workflow completes, then receives a normal
ChatResponse. To the rest of your advisor chain, and to your code, it looks like a local call.
Re-read the crash demo with that flow in mind. When the process died, the workflow's history was preserved: the first LLM call completed with its output, step_one_search returned three venues, step_two_compare started. The runtime detected that the worker died, and when a new agent instance was created, it replayed the history to restore state, and continued from the first step that had not finished. The completed steps were never re-executed. Only the interrupted tool call ran again. No work was lost, and no completed step ran twice.
Retries follow the same structure. A transient failure in an LLM call or a tool call, such as a rate limit or a network blip, is retried by the runtime with exponential backoff (on by default, tunable under diagrid.spring-ai.retry.*) instead of failing the whole agent run.
Try Catalyst
Connect your favorite Spring AI app today to Catalyst for free and experience agentic durable execution. Try the built-in quickstart or run the event-planner quickstart. Kill the agent mid-run and watch what durability and reliability do to your AI agents.


