Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e80d5f0
Spring AI samples
donald-pinckney Apr 6, 2026
de64249
Fix sample configs and remove runtimeOnly workarounds
donald-pinckney Apr 7, 2026
ddc7720
Update samples for T15: remove @DeterministicTool and sandboxing sample
donald-pinckney Apr 13, 2026
df3ec0e
Clean up Spring AI samples PR for merge
donald-pinckney Apr 20, 2026
7011199
Use mavenLocal for temporal-spring-ai instead of composite build
donald-pinckney Apr 20, 2026
7f4acba
Add TASK_QUEUE.json tracking remaining PR work
donald-pinckney Apr 20, 2026
c941ccc
Updated after changes to Spring AI
donald-pinckney Apr 27, 2026
fbae80a
Add snipsync markers to Spring AI samples
donald-pinckney Apr 28, 2026
5354c42
spring-ai samples: pin to released temporal-spring-ai 1.35.0
donald-pinckney May 1, 2026
bb4f3cf
Merge remote-tracking branch 'origin/main' into d/20260406-164121
donald-pinckney May 1, 2026
a3c05a6
Group spring-ai samples under a single springai/ directory
donald-pinckney May 1, 2026
79a9966
springai/mcp: await initialization in chat signal handler
donald-pinckney May 1, 2026
3a4e2ab
springai/multimodel: drop redundant default: CLI prefix
donald-pinckney May 1, 2026
73bb79a
springai/rag: show usage when add/search are typed without args
donald-pinckney May 1, 2026
1b47bd6
Add ai-sdk to CODEOWNERS for springai
donald-pinckney May 1, 2026
2e02881
springai/multimodel: include "think" in chat() javadoc model names
donald-pinckney May 1, 2026
1e9c367
springai/basic: explain why run()'s systemPrompt parameter is unused
donald-pinckney May 1, 2026
6b8f7ac
gradle/springai: document the Spring Boot plugin/BOM version skew
donald-pinckney May 1, 2026
00ea6f3
.gitignore: collapse per-module build/out entries into globs
donald-pinckney May 1, 2026
bd76c1a
Merge remote-tracking branch 'origin/main' into d/20260406-164121
donald-pinckney May 1, 2026
7d3c067
README: document Spring AI samples and Java 17 requirement
donald-pinckney May 1, 2026
273a2e2
springai samples: address Copilot review nits
donald-pinckney May 1, 2026
903fd2c
Untrack .vscode/ IDE settings
donald-pinckney May 1, 2026
5292b27
undo gitignore change
donald-pinckney May 1, 2026
5f91882
springai/multimodel: use LinkedHashMap for chatClients
donald-pinckney May 1, 2026
4d32397
springai/rag: capture lastResponse before signaling, not after
donald-pinckney May 1, 2026
c3a3c88
springai/multimodel: capture previous response before signaling
donald-pinckney May 1, 2026
c606d2a
Remove TASK_QUEUE.json
donald-pinckney May 1, 2026
24ba8c0
Update README.md
donald-pinckney May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Primary owners

* @tsurdilo @temporalio/sdk @antmendoza
* @tsurdilo @temporalio/sdk @antmendoza

# Below are owners for samples for modules
# that are owned by teams other than the SDK team.
# For each one, we add the owning team, as well as
# @temporalio/sdk, so the SDK team can continue to
# manage repo-wide concerns
/springai/ @temporalio/ai-sdk @temporalio/sdk
10 changes: 2 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,8 @@ target
.DS_Store
.idea
.gradle
/build
/core/build
/springboot/build
/springboot-basic/build
/out
/core/out
/springboot/out
/springboot-basic/out
**/build/
**/out/
.classpath
.project
.settings/
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
This repository contains samples that demonstrate various capabilities of
Temporal using the [Java SDK](https://github.com/temporalio/sdk-java).

It contains two modules:
It contains the following modules:
* [Core](/core): showcases many different SDK features.
* [SpringBoot](/springboot): showcases SpringBoot autoconfig integration.
* [SpringBoot Basic](/springboot-basic): Minimal sample showing SpringBoot autoconfig integration without any extra external dependencies.
* [Spring AI](/springai): demonstrates the Temporal Spring AI integration — durable AI agents with chat models, tools, MCP servers, vector stores, and embeddings.

## Learn more about Temporal and Java SDK

Expand All @@ -16,9 +17,7 @@ It contains two modules:

## Requirements

- Java 1.8+ for build and runtime of core samples
- Java 1.8+ for build and runtime of SpringBoot samples when using SpringBoot 2
- Java 1.17+ for build and runtime of Spring Boot samples when using SpringBoot 3
- Java 17+ for build and runtime of all samples
Comment thread
donald-pinckney marked this conversation as resolved.
Outdated
- Local Temporal Server, easiest to get started would be using [Temporal CLI](https://github.com/temporalio/cli).
For more options see docs [here](https://docs.temporal.io/kb/all-the-ways-to-run-a-cluster).

Expand Down Expand Up @@ -213,3 +212,22 @@ To run any of the SpringBoot samples in your Temporal Cloud namespace:
./gradlew bootRun --args='--spring.profiles.active=tc'

3. Follow the previous section from step 2

### Running Spring AI Samples

The Spring AI samples demonstrate the [Temporal Spring AI integration](https://github.com/temporalio/sdk-java/tree/master/temporal-spring-ai), which makes Spring AI agents durable on Temporal — model calls run as Temporal Activities recorded in Workflow history, and tools are dispatched per their type so they fit Workflow execution.

Each sample is its own Spring Boot application with an interactive CLI. Run from the main repo dir:

./gradlew :springai:basic:bootRun
./gradlew :springai:mcp:bootRun
./gradlew :springai:multimodel:bootRun
./gradlew :springai:rag:bootRun

All samples need an `OPENAI_API_KEY` environment variable; some need additional setup (see each sample's source for details).

More info on each sample:
- [**Basic**](/springai/basic): Chat workflow with three tool flavors — activity-backed (`WeatherActivity`), plain workflow tools (`StringTools`), and `@SideEffectTool` (`TimestampTools`) — plus a `PromptChatMemoryAdvisor` for conversation history.
- [**MCP**](/springai/mcp): Connects to a Model Context Protocol server and exposes its tools to the AI through Temporal activities. Defaults to the filesystem MCP server.
- [**Multi-Model**](/springai/multimodel): Two providers in one workflow (OpenAI and Anthropic), per-model `ActivityOptions` overrides via a Spring bean, plus a route that exercises Anthropic's extended-thinking mode through provider-specific `ChatOptions` pass-through. Requires `ANTHROPIC_API_KEY` in addition to `OPENAI_API_KEY`.
- [**RAG**](/springai/rag): Vector store + embeddings for retrieval-augmented generation. Add documents, then ask questions; the workflow searches the vector store and grounds the answer in the retrieved context.
70 changes: 70 additions & 0 deletions TASK_QUEUE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"_comment": "Outstanding tasks on PR #775 (Add Spring AI samples). Delete this file before merge.",
"pr": "https://github.com/temporalio/samples-java/pull/775",
"tasks": [
{
"id": "drop-mavenlocal",
"summary": "Remove mavenLocal() workaround once temporal-spring-ai is on Maven Central",
"blocked_on": "temporal-spring-ai artifact publishing to Maven Central (tracked in temporalio/sdk-java, module merged in 686a62d9)",
"steps": [
"Delete the mavenLocal() block and its comment from build.gradle (subprojects.repositories).",
"In gradle/springai.gradle: delete the springAiSdkVersion ext property and its comment.",
"In gradle/springai.gradle: change the three coordinates back to $javaSDKVersion (temporal-spring-boot-starter, temporal-spring-ai, temporal-sdk).",
"Bump build.gradle's javaSDKVersion to whatever released version first contains temporal-spring-ai (>= 1.35.0).",
"Delete the 'implementation temporal-sdk' line in gradle/springai.gradle if temporal-spring-ai starts depending on temporal-sdk non-compileOnly; otherwise leave it (it's harmless and explicit).",
"Remove the 'Publish sdk-java locally' step from the PR description / sample README sections.",
"Re-run ./gradlew :springai:basic:build :springai:mcp:build :springai:multimodel:build :springai:rag:build to verify."
]
},
{
"id": "mark-ready-for-review",
"summary": "Flip PR from Draft to Ready for Review",
"blocked_on": "drop-mavenlocal (reviewers will ask about the local-publish dance otherwise)",
"steps": [
"gh pr ready 775"
]
},
{
"id": "delete-task-queue-file",
"summary": "Delete this TASK_QUEUE.json before merging",
"blocked_on": "all other tasks",
"steps": [
"git rm TASK_QUEUE.json && git commit"
]
}
],
Comment thread
donald-pinckney marked this conversation as resolved.
Outdated
"open_review_threads_to_resolve_or_reply": [
{
"file": "springai/mcp/src/main/java/io/temporal/samples/springai/mcp/McpApplication.java",
"line": 68,
"reviewer": "brianstrauch",
"question": "What's the significance of a workflow stub vs a regular workflow? Why do we always opt for a stub?",
"status": "unaddressed",
"note": "Reply-only — this is standard Temporal client usage. Either answer in a PR comment explaining that a stub is how clients invoke workflows remotely (the @WorkflowInterface is the contract, stub is the proxy), or add a one-line comment at the call site."
},
{
"file": "springai/mcp/src/main/java/io/temporal/samples/springai/mcp/McpWorkflowImpl.java",
"line": 93,
"reviewer": "brianstrauch",
"question": "Another option could be to poll until it's ready?",
"status": "addressed",
"note": "chat() is a @SignalMethod, so Workflow.await is allowed. Switched chat() to Workflow.await(() -> initialized) instead of returning a placeholder string. listTools() stays placeholder-based because it's a @QueryMethod and queries can't yield."
},
{
"file": "springai/basic/src/main/java/io/temporal/samples/springai/chat/ChatWorkflowImpl.java",
"line": 103,
"reviewer": "copilot-pull-request-reviewer",
"question": "run(String systemPrompt) doesn't use its systemPrompt parameter.",
"status": "addressed",
"note": "Added a comment in run() explaining that @WorkflowInit requires matching parameter lists between the constructor and the @WorkflowMethod, and that the constructor consumed the prompt."
},
{
"file": "springai/multimodel/src/main/java/io/temporal/samples/springai/multimodel/MultiModelApplication.java",
"line": 114,
"reviewer": "brianstrauch",
"question": "Why is there a model name called \"default\", but at the same time it looks like the default is hardcoded to \"openai\"?",
"status": "addressed",
"note": "Real ergonomic wart — both `default:` and `openai:` ended up calling OpenAI (forDefault() resolves @Primary, which ChatModelConfig declares on openAiChatModel). Dropped the explicit `default:` CLI prefix and routed no-prefix input there instead. Added a comment block in MultiModelWorkflowImpl explaining that the dual `default`/`openai` registration is intentional — it demonstrates both forDefault() (@Primary resolution) and forModel(name) (explicit lookup), even though both happen to hit OpenAI in this sample."
}
]
}
61 changes: 61 additions & 0 deletions gradle/springai.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Shared configuration for all Spring AI sample modules.
// Applied via: apply from: "$rootDir/gradle/springai.gradle"
//
// Note on Spring Boot version skew: the root build.gradle pins the
// org.springframework.boot Gradle plugin at $springBootPluginVersion (currently
// 2.7.13) for the legacy springboot/ samples. Spring AI 1.1.0 requires Spring
// Boot 3.5.x, which we get by importing the spring-boot-dependencies BOM at
// $springBootVersionForSpringAi below. The plugin and the BOM are independent —
// the plugin contributes bootJar/bootRun task wiring, the BOM dictates
// dependency versions — so this works in practice even though the two version
// numbers don't match. Long-term fix is to either move the plugin declaration
// out of the root plugins block (so each module applies its own version) or
// migrate the legacy springboot/ samples to Spring Boot 3.x; until one of those
// happens, this skew is intentional.

apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

ext {
springBootVersionForSpringAi = '3.5.3'
springAiVersion = '1.1.0'
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-dependencies:$springBootVersionForSpringAi"
mavenBom "org.springframework.ai:spring-ai-bom:$springAiVersion"
}
Comment thread
donald-pinckney marked this conversation as resolved.
}

dependencies {
implementation "io.temporal:temporal-spring-boot-starter:$javaSDKVersion"
implementation "io.temporal:temporal-spring-ai:$javaSDKVersion"
// temporal-spring-ai declares temporal-sdk as compileOnly, so bring it in explicitly.
implementation "io.temporal:temporal-sdk:$javaSDKVersion"

// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter'

dependencies {
errorproneJavac('com.google.errorprone:javac:9+181-r4173-1')
errorprone('com.google.errorprone:error_prone_core:2.28.0')
}
}

bootJar {
enabled = false
}

jar {
enabled = true
}

bootRun {
standardInput = System.in
}
5 changes: 5 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
rootProject.name = 'temporal-java-samples'
include 'core'
include 'springai:basic'
include 'springai:mcp'
include 'springai:multimodel'
include 'springai:rag'
include 'springboot'
include 'springboot-basic'

5 changes: 5 additions & 0 deletions springai/basic/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apply from: "$rootDir/gradle/springai.gradle"

dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.temporal.samples.springai.chat;

import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import java.util.Scanner;
import java.util.UUID;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
* Example application demonstrating the Spring AI Temporal plugin.
*
* <p>Starts an interactive chat workflow where each AI call is a durable Temporal activity with
* automatic retries and timeout handling.
*/
@SpringBootApplication
public class ChatExampleApplication {

public static void main(String[] args) {
SpringApplication.run(ChatExampleApplication.class, args);
}
}

@Component
class ChatRunner {

private final WorkflowClient workflowClient;

ChatRunner(WorkflowClient workflowClient) {
this.workflowClient = workflowClient;
}

@EventListener(ApplicationReadyEvent.class)
public void run() {
String workflowId = "chat-" + UUID.randomUUID().toString().substring(0, 8);

System.out.println("\n===========================================");
System.out.println(" Spring AI + Temporal Chat Demo");
System.out.println("===========================================");
System.out.println("Workflow ID: " + workflowId);
System.out.println("Type messages, or 'quit' to exit.\n");

// Start the chat workflow
ChatWorkflow workflow =
workflowClient.newWorkflowStub(
ChatWorkflow.class,
WorkflowOptions.newBuilder()
.setWorkflowId(workflowId)
.setTaskQueue("spring-ai-example")
.build());

WorkflowClient.start(workflow::run, "You are a helpful assistant. Be concise.");

// Get stub for the running workflow
ChatWorkflow chat = workflowClient.newWorkflowStub(ChatWorkflow.class, workflowId);

// Interactive loop
try (Scanner scanner = new Scanner(System.in, java.nio.charset.StandardCharsets.UTF_8)) {
while (true) {
System.out.print("You: ");
String input = scanner.nextLine().trim();

if (input.equalsIgnoreCase("quit") || input.equalsIgnoreCase("exit")) {
chat.end();
break;
}

if (input.isEmpty()) {
continue;
}

try {
String response = chat.chat(input);
System.out.println("Assistant: " + response + "\n");
} catch (Exception e) {
System.err.println("Error: " + e.getMessage() + "\n");
}
}
}

System.out.println("Goodbye!");
System.exit(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.temporal.samples.springai.chat;

import io.temporal.workflow.SignalMethod;
import io.temporal.workflow.UpdateMethod;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;

/**
* A chat workflow that maintains a conversation with an AI model.
*
* <p>The workflow runs until explicitly ended via the {@link #end()} signal. Messages can be sent
* via the {@link #chat(String)} update method, which returns the AI's response synchronously.
*/
@WorkflowInterface
public interface ChatWorkflow {

/**
* Starts the chat workflow and waits until ended.
*
* @param systemPrompt the system prompt that defines the AI's behavior
* @return a summary when the chat ends
*/
@WorkflowMethod
String run(String systemPrompt);

/**
* Sends a message to the AI and returns its response.
*
* @param message the user's message
* @return the AI's response
*/
@UpdateMethod
String chat(String message);

/** Ends the chat session. */
@SignalMethod
void end();
}
Loading
Loading