# Spring AI integration - Java SDK

> 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 Event 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).

> **Public Preview**

## Prerequisites

The integration requires all of the following on your application's classpath. The plugin won't auto-configure if any of these are missing or below the listed minimum:

| Dependency        | Minimum version |
| ----------------- | --------------- |
| Java              | 17              |
| Spring Boot       | 3.x             |
| Spring AI         | 1.1.0           |
| Temporal Java SDK | 1.35.0          |

You also need the [`temporal-spring-boot-starter`](/develop/java/integrations/spring-boot-integration) and a Spring AI model starter (for example, `spring-ai-starter-model-openai`) — `temporal-spring-ai` does not pull in a model provider on its own.

## 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
<dependency>
    <groupId>io.temporal</groupId>
    <artifactId>temporal-spring-ai</artifactId>
    <version>${temporal-sdk.version}</version>
</dependency>
```

**[Gradle Groovy DSL](https://gradle.org/):**

```groovy
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:

| Feature      | Dependency      | Registered Activity      |
| ------------ | --------------- | ------------------------ |
| Vector store | `spring-ai-rag` | `VectorStoreActivity`    |
| Embeddings   | `spring-ai-rag` | `EmbeddingModelActivity` |
| MCP          | `spring-ai-mcp` | `McpClientActivity`      |

## 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:

<!--SNIPSTART samples-java-spring-ai-chat-workflow-init-->
[springai/basic/src/main/java/io/temporal/samples/springai/chat/ChatWorkflowImpl.java](https://github.com/temporalio/samples-java/blob/main/springai/basic/src/main/java/io/temporal/samples/springai/chat/ChatWorkflowImpl.java)
```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();
}

```
<!--SNIPEND-->

`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

In Spring AI, [tools](https://docs.spring.io/spring-ai/reference/api/tools.html) are methods the model can choose to call to fetch data or take action — you make them available to a chat client by registering them, typically through `ChatClient.defaultTools(...)` or per-prompt `tools(...)`. The chat client advertises the methods to the model, the model decides which (if any) to call, and the framework runs the chosen method and feeds the result back into the conversation.

The Temporal integration extends this by inspecting the type of each tool you register and dispatching it to the appropriate Temporal primitive, so you can mix durable and in-Workflow tools in the same chat client. 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.

<!--SNIPSTART samples-java-spring-ai-activity-tool-->
[springai/basic/src/main/java/io/temporal/samples/springai/chat/WeatherActivity.java](https://github.com/temporalio/samples-java/blob/main/springai/basic/src/main/java/io/temporal/samples/springai/chat/WeatherActivity.java)
```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);
}
```
<!--SNIPEND-->

### 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.

<!--SNIPSTART samples-java-spring-ai-side-effect-tool-->
[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.
   *
   * <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'.";
    }
  }
}
```
<!--SNIPEND-->

### 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.

<!--SNIPSTART samples-java-spring-ai-plain-tool-->
[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);
  }
}
```
<!--SNIPEND-->

## 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](/develop/java/activities/execution#heartbeattimeout), [priority](/develop/task-queue-priority-fairness), 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:

<!--SNIPSTART samples-java-spring-ai-per-model-options-->
[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()));
}
```
<!--SNIPEND-->

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:

<!--SNIPSTART samples-java-spring-ai-provider-options-->
[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());
```
<!--SNIPEND-->

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 Event 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.

## Resources

- [`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
