diff --git a/docs/develop/integrations.mdx b/docs/develop/integrations.mdx
index 92f5afa45e..ad7982bc8a 100644
--- a/docs/develop/integrations.mdx
+++ b/docs/develop/integrations.mdx
@@ -17,5 +17,6 @@ description: AI framework integrations available for Temporal SDKs.
Temporal integrates with popular AI and agent frameworks.
The following integrations are available by SDK:
+- [Java SDK integrations](/develop/java/integrations)
- [Python SDK integrations](/develop/python/integrations)
- [TypeScript SDK integrations](/develop/typescript/integrations)
diff --git a/docs/develop/java/integrations/index.mdx b/docs/develop/java/integrations/index.mdx
index d2713d4406..5103398ef8 100644
--- a/docs/develop/java/integrations/index.mdx
+++ b/docs/develop/java/integrations/index.mdx
@@ -6,15 +6,28 @@ description: This section covers integrations with the Java SDK
toc_max_heading_level: 4
keywords:
- Java SDK
+ - integrations
+ - ai frameworks
tags:
- Java SDK
- Temporal SDKs
+ - Integrations
---
import * as Components from '@site/src/components';

-## Integrations
+## Framework integrations
- [Spring Boot](/develop/java/integrations/spring-boot-integration)
+
+## AI integrations
+
+The following AI framework integrations are available for the Temporal Java SDK:
+
+| Framework | SDK docs | Integration guide |
+| --- | --- | --- |
+| Spring AI | [docs.spring.io/spring-ai](https://docs.spring.io/spring-ai/reference/) | [Guide](/develop/java/integrations/spring-ai) |
+
+These integrations are built on the Temporal Java SDK's [Plugin system](/develop/plugins-guide), which you can also use to build your own integrations.
diff --git a/docs/develop/java/integrations/spring-ai.mdx b/docs/develop/java/integrations/spring-ai.mdx
new file mode 100644
index 0000000000..0df221c0cc
--- /dev/null
+++ b/docs/develop/java/integrations/spring-ai.mdx
@@ -0,0 +1,394 @@
+---
+id: spring-ai
+title: Spring AI integration - Java SDK
+sidebar_label: Spring AI
+slug: /develop/java/integrations/spring-ai
+toc_max_heading_level: 2
+keywords:
+ - ai
+ - agents
+ - spring ai
+ - java
+ - integrations
+tags:
+ - Spring AI
+ - Java SDK
+ - Temporal SDKs
+ - Integrations
+ - AI Frameworks
+description: Build durable AI agents in Java with the Temporal Spring AI integration.
+---
+
+[Spring AI](https://docs.spring.io/spring-ai/reference/) 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](https://central.sonatype.com/artifact/io.temporal/temporal-spring-ai) 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](/develop/plugins-guide) and is distributed as the `io.temporal:temporal-spring-ai` module alongside the existing [Spring Boot integration](/develop/java/integrations/spring-boot-integration).
+
+:::info
+
+The Spring AI Integration is in Public Preview. Refer to the
+[Temporal product release stages guide](/evaluate/development-production-features/release-stages) for more information.
+
+:::
+
+## Compatibility
+
+| Dependency | Minimum version |
+| ----------------- | --------------- |
+| Java | 17 |
+| Spring Boot | 3.x |
+| Spring AI | 1.1.0 |
+| Temporal Java SDK | 1.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](https://maven.apache.org/):**
+
+```xml
+
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](/develop/java/nexus), 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](https://github.com/temporalio/samples-java/blob/main/springai/basic/src/main/java/io/temporal/samples/springai/chat/TimestampTools.java) +```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. + * + *
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](https://github.com/temporalio/samples-java/blob/main/springai/basic/src/main/java/io/temporal/samples/springai/chat/StringTools.java) +```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`: + +```java +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](https://github.com/temporalio/samples-java/blob/main/springai/multimodel/src/main/java/io/temporal/samples/springai/multimodel/ChatModelConfig.java) +```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](https://github.com/temporalio/samples-java/blob/main/springai/multimodel/src/main/java/io/temporal/samples/springai/multimodel/MultiModelWorkflowImpl.java) +```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. + +```java +// 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: + +```java +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 + +- [`temporal-spring-ai` README](https://github.com/temporalio/sdk-java/blob/master/temporal-spring-ai/README.md) — full reference for the module +- [Spring Boot integration](/develop/java/integrations/spring-boot-integration) — required companion module +- [Plugin system](/develop/plugins-guide) — how integrations are registered with Workers and Clients diff --git a/sidebars.js b/sidebars.js index c43cbbf0aa..d0cd6bfe8a 100644 --- a/sidebars.js +++ b/sidebars.js @@ -346,6 +346,7 @@ module.exports = { }, items: [ 'develop/java/integrations/spring-boot', + 'develop/java/integrations/spring-ai', ], }, ],