Skip to main content

Spring AI integration - Java SDK

Spring AI is an agent framework for Java applications — chat clients, tool calling, vector stores, embeddings, and MCP servers, all wired through Spring Boot. The Temporal Spring AI integration makes Spring AI agents durable: model calls run through Temporal Activities recorded in Workflow history, and tools are dispatched per their type so each kind lands in the right place in Workflow execution — Activity stubs and Nexus stubs as durable operations, @SideEffectTool classes wrapped in Workflow.sideEffect, and plain tools running directly in Workflow code. Agents retry on failure and replay deterministically without changing how you write Spring AI code.

The integration is built on the Temporal Java SDK's Plugin system and is distributed as the io.temporal:temporal-spring-ai module alongside the existing Spring Boot integration.

info

The Spring AI Integration is in Public Preview. Refer to the Temporal product release stages guide for more information.

Compatibility

DependencyMinimum version
Java17
Spring Boot3.x
Spring AI1.1.0
Temporal Java SDK1.35.0

Add the dependency

Add temporal-spring-ai alongside temporal-spring-boot-starter and a Spring AI model starter (for example, spring-ai-starter-model-openai).

Apache Maven:

<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-spring-ai</artifactId>
<version>${temporal-sdk.version}</version>
</dependency>

Gradle Groovy DSL:

implementation "io.temporal:temporal-spring-ai:${temporalSdkVersion}"

When temporal-spring-ai is on the classpath, the SpringAiPlugin auto-registers ChatModelActivity with all Temporal Workers created by the Spring Boot integration. Optional Activities are auto-configured when their dependencies are present:

FeatureDependencyRegistered Activity
Vector storespring-ai-ragVectorStoreActivity
Embeddingsspring-ai-ragEmbeddingModelActivity
MCPspring-ai-mcpMcpClientActivity

Call a chat model from a Workflow

Use ActivityChatModel as a Spring AI ChatModel inside a Workflow. Every call goes through a Temporal Activity, so model responses are durable and retried per your Activity options.

Wrap ActivityChatModel in a TemporalChatClient to build prompts and register tools:

springai/basic/src/main/java/io/temporal/samples/springai/chat/ChatWorkflowImpl.java

@WorkflowInit
public ChatWorkflowImpl(String systemPrompt) {
// Build an activity-backed chat model. The factory creates the activity stub
// internally and registers per-call Summaries on the Temporal UI.
ActivityChatModel activityChatModel = ActivityChatModel.forDefault();

// Create an activity stub for weather tools - these execute as durable activities
WeatherActivity weatherTool =
Workflow.newActivityStub(
WeatherActivity.class,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(30))
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build())
.build());

// Create deterministic tools - these execute directly in the workflow
StringTools stringTools = new StringTools();

// Create side-effect tools - these are wrapped in Workflow.sideEffect()
// The result is recorded in history, making replay deterministic
TimestampTools timestampTools = new TimestampTools();

// Create chat memory - uses in-memory storage that gets rebuilt on replay
ChatMemory chatMemory =
MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20)
.build();

// Build a TemporalChatClient with tools and memory
// - Activity stubs (weatherTool) become durable AI tools
// - plain workflow tool classes (stringTools) execute directly in workflow
// - @SideEffectTool classes (timestampTools) are wrapped in sideEffect()
// - PromptChatMemoryAdvisor maintains conversation history
this.chatClient =
TemporalChatClient.builder(activityChatModel)
.defaultSystem(systemPrompt)
.defaultTools(weatherTool, stringTools, timestampTools)
.defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build())
.build();
}

ActivityChatModel.forDefault() resolves to the default Spring AI ChatModel bean. To target a specific model in a multi-model application, pass its bean name to ActivityChatModel.forModel("openai").

note

Streaming responses are not currently supported.

Register tools

Tools passed to defaultTools() are dispatched based on their type. The integration handles Temporal determinism for you when the tool is durable, and gives you control when it isn't.

Activity stubs

An interface annotated with both @ActivityInterface and Spring AI @Tool methods is auto-detected and executed as a Temporal Activity. Use this for external calls that need retries and timeouts.

springai/basic/src/main/java/io/temporal/samples/springai/chat/WeatherActivity.java

@ActivityInterface
public interface WeatherActivity {

/**
* Gets the current weather for a city.
*
* <p>The {@code @Tool} annotation makes this method available to the AI model, while the
* {@code @ActivityInterface} ensures it executes as a Temporal activity.
*
* @param city the name of the city
* @return a description of the current weather
*/
@Tool(
description =
"Get the current weather for a city. Returns temperature, conditions, and humidity.")
@ActivityMethod
String getWeather(
@ToolParam(description = "The name of the city (e.g., 'Seattle', 'New York')") String city);

/**
* Gets the weather forecast for a city.
*
* @param city the name of the city
* @param days the number of days to forecast (1-7)
* @return the weather forecast
*/
@Tool(description = "Get the weather forecast for a city for the specified number of days.")
@ActivityMethod
String getForecast(
@ToolParam(description = "The name of the city") String city,
@ToolParam(description = "Number of days to forecast (1-7)") int days);
}

Nexus service stubs

Nexus service stubs with @Tool methods are auto-detected and invoked as Nexus operations, enabling cross-Namespace tool calls.

@SideEffectTool

Classes annotated with @SideEffectTool have each @Tool method wrapped in Workflow.sideEffect(). The result is recorded in history on first execution and replayed from history afterward. Use this for cheap, non-deterministic operations such as timestamps or UUIDs.

springai/basic/src/main/java/io/temporal/samples/springai/chat/TimestampTools.java

@SideEffectTool
public class TimestampTools {

private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(ZoneId.systemDefault());

/**
* Gets the current date and time.
*
* <p>This is non-deterministic (returns different values each time), but wrapped in sideEffect()
* it becomes safe for workflow replay.
*
* @return the current date and time as a formatted string
*/
@Tool(description = "Get the current date and time")
public String getCurrentDateTime() {
return FORMATTER.format(Instant.now());
}

/**
* Gets the current Unix timestamp in milliseconds.
*
* @return the current time in milliseconds since epoch
*/
@Tool(description = "Get the current Unix timestamp in milliseconds")
public long getCurrentTimestamp() {
return System.currentTimeMillis();
}

/**
* Generates a random UUID.
*
* @return a new random UUID string
*/
@Tool(description = "Generate a random UUID")
public String generateUuid() {
return UUID.randomUUID().toString();
}

/**
* Gets the current date and time in a specific timezone.
*
* @param timezone the timezone ID (e.g., "America/New_York", "UTC", "Europe/London")
* @return the current date and time in the specified timezone
*/
@Tool(description = "Get the current date and time in a specific timezone")
public String getDateTimeInTimezone(
@ToolParam(description = "Timezone ID (e.g., 'America/New_York', 'UTC', 'Europe/London')")
String timezone) {
try {
ZoneId zoneId = ZoneId.of(timezone);
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(zoneId);
return formatter.format(Instant.now());
} catch (Exception e) {
return "Invalid timezone: " + timezone + ". Use formats like 'America/New_York' or 'UTC'.";
}
}
}

Plain tools

Any class with @Tool methods that isn't an Activity stub, Nexus stub, or @SideEffectTool runs directly on the Workflow thread. Use this for inherently deterministic tools (such as updating in-memory agent state), or for orchestration of durable primitives as you need, e.g. calling multiple Activities, child Workflows, wait conditions, or other Temporal durable primitives.

springai/basic/src/main/java/io/temporal/samples/springai/chat/StringTools.java

public class StringTools {

@Tool(description = "Reverse a string, returning the characters in opposite order")
public String reverse(@ToolParam(description = "The string to reverse") String input) {
if (input == null) {
return null;
}
return new StringBuilder(input).reverse().toString();
}

@Tool(description = "Count the number of words in a text")
public int countWords(@ToolParam(description = "The text to count words in") String text) {
if (text == null || text.isBlank()) {
return 0;
}
return text.trim().split("\\s+").length;
}

@Tool(description = "Convert text to all uppercase letters")
public String toUpperCase(@ToolParam(description = "The text to convert") String text) {
if (text == null) {
return null;
}
return text.toUpperCase(java.util.Locale.ROOT);
}

@Tool(description = "Convert text to all lowercase letters")
public String toLowerCase(@ToolParam(description = "The text to convert") String text) {
if (text == null) {
return null;
}
return text.toLowerCase(java.util.Locale.ROOT);
}

@Tool(description = "Check if a string is a palindrome (reads the same forwards and backwards)")
public boolean isPalindrome(@ToolParam(description = "The text to check") String text) {
if (text == null) {
return false;
}
String normalized = text.toLowerCase(java.util.Locale.ROOT).replaceAll("\\s+", "");
String reversed = new StringBuilder(normalized).reverse().toString();
return normalized.equals(reversed);
}
}

Activity options and retry behavior

ActivityChatModel.forDefault() and forModel(name) build the chat Activity stub with sensible defaults: a 2-minute start-to-close timeout, 3 attempts, and org.springframework.ai.retry.NonTransientAiException and java.lang.IllegalArgumentException classified as non-retryable so a bad API key or invalid prompt fails fast.

Pass an ActivityOptions directly when you need finer control — a specific Task Queue, heartbeats, priority, or a custom RetryOptions:

ActivityChatModel chatModel = ActivityChatModel.forDefault(
ActivityOptions.newBuilder(ActivityChatModel.defaultActivityOptions())
.setTaskQueue("chat-heavy")
.build());

For configuration-driven per-model overrides, declare a ChatModelActivityOptions bean. The plugin consults it whenever forDefault() or forModel(name) runs in a Workflow. Use the special key ChatModelTypes.DEFAULT_MODEL_NAME (the literal "default") as a global catch-all that applies to any model not explicitly listed — including models contributed by third-party starters:

springai/multimodel/src/main/java/io/temporal/samples/springai/multimodel/ChatModelConfig.java

@Bean
public ChatModelActivityOptions chatModelActivityOptions() {
return new ChatModelActivityOptions(
Map.of(
"anthropicChatModel",
ActivityOptions.newBuilder(ActivityChatModel.defaultActivityOptions())
.setStartToCloseTimeout(Duration.ofMinutes(5))
.setScheduleToCloseTimeout(Duration.ofMinutes(15))
.build()));
}

Keys that neither match a registered ChatModel bean nor equal "default" cause plugin construction to fail, so a typo surfaces at startup rather than at first call.

ActivityMcpClient.create() and create(ActivityOptions) work the same way for MCP tool calls, with a 30-second default timeout.

Provider-specific chat options

Provider-specific ChatOptions subclasses — for example, AnthropicChatOptions to enable extended thinking, or OpenAiChatOptions to set reasoning_effort — pass through the Activity boundary unchanged. Attach them via ChatClient.defaultOptions(...) and the plugin re-applies them on the Activity side before calling the underlying model:

springai/multimodel/src/main/java/io/temporal/samples/springai/multimodel/MultiModelWorkflowImpl.java

AnthropicChatOptions thinkingOptions =
AnthropicChatOptions.builder()
.thinking(AnthropicApi.ThinkingType.ENABLED, 1024)
.temperature(1.0)
.maxTokens(4096)
.build();
chatClients.put(
"think",
TemporalChatClient.builder(anthropicModel)
.defaultSystem(
"You are a helpful assistant powered by Anthropic with extended thinking. "
+ "Use the thinking budget to reason carefully, then give a crisp answer "
+ "that reflects the reasoning you did.")
.defaultOptions(thinkingOptions)
.build());

The pass-through relies on the ChatOptions subclass overriding copy() to return its own type — every provider class shipped with Spring AI does.

Media in messages

Prefer URI-based media when attaching images, audio, or other binary content to chat messages. Raw byte[] media gets serialized into every chat Activity's input and result payload, which end up inside Temporal Workflow history events. Server-side history events have a fixed 2 MiB size limit; to leave headroom for messages, tool definitions, and options, the plugin enforces a 1 MiB default cap on inline bytes and fails fast with a non-retryable ApplicationFailure pointing at the URI alternative.

// Preferred — only the URL crosses the Activity boundary.
Media image = new Media(MimeTypeUtils.IMAGE_PNG, URI.create("https://cdn.example.com/pic.png"));

Override the cap by setting the system property io.temporal.springai.maxMediaBytes before your worker starts (positive integer; 0 disables the check). For anything larger than a small thumbnail, route the bytes to a binary store from an Activity and pass only the URL across the conversation.

Use vector stores, embeddings, and MCP

When the corresponding Spring AI modules are on the classpath, the integration registers Activities for vector stores, embeddings, and MCP tool calls. Inject the matching Spring AI types into your Activities or Workflows and use them as you would in any Spring AI application — each operation is executed through a Temporal Activity.

You can also register these plugins explicitly, without relying on auto-configuration:

new VectorStorePlugin(vectorStore);
new EmbeddingModelPlugin(embeddingModel);
new McpPlugin();

ActivityMcpClient wraps a Spring AI MCP client so that remote MCP tool calls become durable Activity executions.

Learn more