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.
The Spring AI Integration is in Public Preview. Refer to the Temporal product release stages guide 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).
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-spring-ai</artifactId>
<version>${temporal-sdk.version}</version>
</dependency>
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:
@WorkflowInit
public MyWorkflowImpl(String goal) {
ActivityChatModel chatModel = ActivityChatModel.forDefault();
WeatherActivity weather = Workflow.newActivityStub(
WeatherActivity.class, activityOptions);
this.chatClient = TemporalChatClient.builder(chatModel)
.defaultSystem("You are a helpful assistant.")
.defaultTools(weather, new TimestampTools())
.build();
}
@Override
public String run(String goal) {
return chatClient.prompt().user(goal).call().content();
}
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").
Streaming responses are not currently supported.
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:
@Bean
ChatModelActivityOptions chatModelActivityOptions() {
ActivityOptions fiveMinute =
ActivityOptions.newBuilder(ActivityChatModel.defaultActivityOptions())
.setStartToCloseTimeout(Duration.ofMinutes(5))
.build();
return new ChatModelActivityOptions(Map.of(
ChatModelTypes.DEFAULT_MODEL_NAME, fiveMinute, // global baseline
"claude", ActivityOptions.newBuilder(fiveMinute) // overrides for "claude"
.setTaskQueue("claude-heavy")
.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:
AnthropicChatOptions thinking = AnthropicChatOptions.builder()
.thinking(AnthropicApi.ThinkingType.ENABLED, 1024)
.temperature(1.0)
.maxTokens(4096)
.build();
ChatClient client = TemporalChatClient.builder(ActivityChatModel.forModel("anthropic"))
.defaultOptions(thinking)
.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.
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.
@ActivityInterface
public interface WeatherActivity {
@Tool(description = "Get weather for a city")
@ActivityMethod
String getWeather(String city);
}
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.
@SideEffectTool
public class TimestampTools {
@Tool(description = "Get current time")
public String now() {
return Instant.now().toString();
}
}
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.
public class MyTools {
@Tool(description = "Process data")
public String process(String input) {
SomeActivity act = Workflow.newActivityStub(SomeActivity.class, opts);
return act.doWork(input);
}
}
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
temporal-spring-aiREADME — full reference for the module- Spring Boot integration — required companion module
- Plugin system — how integrations are registered with Workers and Clients