Google ADK basic workflow example
Example of a basic Google ADK workflow with an Activity-backed tool:
func AgentWorkflow(ctx workflow.Context, question string) (string, error) {
weatherTool, err := googleadk.ActivityAsTool(GetWeather, googleadk.ActivityToolOptions{
Name: WeatherToolName,
Description: "Get the current weather for a city.",
})
if err != nil {
return "", err
}
root, err := llmagent.New(llmagent.Config{
Name: "assistant",
Description: "a helpful weather assistant",
Model: googleadk.NewModel(ModelName),
Instruction: "Answer the user's question. Use the get_weather tool when asked about the weather.",
Tools: []tool.Tool{weatherTool},
})
if err != nil {
return "", err
}
r, err := runner.New(runner.Config{
AppName: "weather",
Agent: root,
SessionService: session.InMemoryService(),
AutoCreateSession: true,
})
if err != nil {
return "", err
}
adkCtx := googleadk.NewContext(ctx)
msg := genai.NewContentFromText(question, genai.RoleUser)
var answer string
for ev, err := range r.Run(adkCtx, "user-1", "session-1", msg, agent.RunConfig{}) {
if err != nil {
return "", err
}
if ev != nil && ev.Content != nil {
for _, p := range ev.Content.Parts {
if p != nil && p.Text != "" {
answer = p.Text
}
}
}
}
return answer, nil
}
Google ADK activity tool example
Example of a simple Activity tool for Google ADK:
func GetWeather(ctx context.Context, in GetWeatherInput) (GetWeatherOutput, error) {
return GetWeatherOutput{City: in.City, Conditions: "sunny, 72°F"}, nil
}
Spring AI integration module for Java SDK
The Temporal Spring AI integration (io.temporal:temporal-spring-ai) makes Spring AI agents durable by running model calls through Temporal Activities recorded in Event history. Tools are dispatched per their type: Activity stubs and Nexus stubs as durable operations, @SideEffectTool classes wrapped in Workflow.sideEffect, and plain tools running directly in Workflow code. The integration is built on the Temporal Java SDK's Plugin system.
Spring AI integration prerequisites and minimum versions
The Temporal Spring AI integration requires: Java 17, Spring Boot 3.x, Spring AI 1.1.0, and Temporal Java SDK 1.35.0. The temporal-spring-boot-starter and a Spring AI model starter (such as spring-ai-starter-model-openai) are also required; temporal-spring-ai does not pull in a model provider on its own.
Auto-registered Activities with Spring AI integration
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: VectorStoreActivity (requires spring-ai-rag), EmbeddingModelActivity (requires spring-ai-rag), and McpClientActivity (requires spring-ai-mcp).
ActivityChatModel usage in workflows
Use ActivityChatModel as a Spring AI ChatModel inside a Workflow. Wrap ActivityChatModel in a TemporalChatClient to build prompts and register tools. 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("modelName"). Every call goes through a Temporal Activity, so model responses are durable and retried per Activity options.
Activity stubs as Spring AI tools
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. Methods are decorated with @Tool (making them available to the AI model), @ActivityMethod, and @ToolParam annotations for parameters.
@SideEffectTool for non-deterministic operations
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.
Plain tools in Spring AI integration
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 needed, for example calling multiple Activities, child Workflows, wait conditions, or other Temporal durable primitives.
Nexus service stubs as Spring AI tools
Nexus service stubs with @Tool methods are auto-detected and invoked as Nexus operations, enabling cross-Namespace tool calls.
Default Activity options for ChatModelActivity
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.
Custom Activity options for chat models
Pass an ActivityOptions directly to ActivityChatModel when you need finer control such as a specific Task Queue, heartbeats, priority, or a custom RetryOptions. For configuration-driven per-model overrides, declare a ChatModelActivityOptions bean. Use the special key ChatModelTypes.DEFAULT_MODEL_NAME (the literal "default") as a global catch-all that applies to any model not explicitly listed.
Provider-specific chat options pass-through
Provider-specific ChatOptions subclasses (such as AnthropicChatOptions or OpenAiChatOptions) 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. This relies on the ChatOptions subclass overriding copy() to return its own type.
Media in chat messages and 1 MiB default cap
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; the plugin enforces a 1 MiB default cap on inline bytes and fails fast with a non-retryable ApplicationFailure pointing at the URI alternative. Override the cap by setting the system property io.temporal.springai.maxMediaBytes before your worker starts (positive integer; 0 disables the check).
ActivityMcpClient for MCP tool calls
ActivityMcpClient.create() and create(ActivityOptions) work the same way as ActivityChatModel for MCP tool calls, with a 30-second default timeout. ActivityMcpClient wraps a Spring AI MCP client so that remote MCP tool calls become durable Activity executions.
Vector stores, embeddings, and MCP auto-registration
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. You can also register these plugins explicitly using: new VectorStorePlugin(vectorStore), new EmbeddingModelPlugin(embeddingModel), and new McpPlugin().
Spring AI integration streaming not supported
Streaming responses are not currently supported in the Temporal Spring AI integration.
Spring AI integration custom Activity options example
Example of passing custom ActivityOptions to ActivityChatModel for a specific Task Queue:
ActivityChatModel chatModel = ActivityChatModel.forDefault(
ActivityOptions.newBuilder(ActivityChatModel.defaultActivityOptions())
.setTaskQueue("chat-heavy")
.build());
Spring AI integration URI-based media example
Example of preferred URI-based media attachment for images:
Media image = new Media(MimeTypeUtils.IMAGE_PNG, URI.create("https://cdn.example.com/pic.png"));
Spring AI integration example: TimestampTools with @SideEffectTool
Example of @SideEffectTool class with non-deterministic operations wrapped in sideEffect():
@SideEffectTool
public class TimestampTools {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(ZoneId.systemDefault());
@Tool(description = "Get the current date and time")
public String getCurrentDateTime() {
return FORMATTER.format(Instant.now());
}
@Tool(description = "Get the current Unix timestamp in milliseconds")
public long getCurrentTimestamp() {
return System.currentTimeMillis();
}
@Tool(description = "Generate a random UUID")
public String generateUuid() {
return UUID.randomUUID().toString();
}
@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'.";
}
}
}
Spring AI integration example: StringTools as plain workflow tool
Example of deterministic plain tool class that runs directly on the Workflow thread:
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);
}
}
Spring AI integration example: WeatherActivity as Activity tool
Example of Activity stub with Spring AI @Tool methods:
@ActivityInterface
public interface WeatherActivity {
@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);
@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);
}
Spring AI integration example: Creating TemporalChatClient with mixed tools
Example of building a TemporalChatClient with Activity stubs, plain workflow tools, and @SideEffectTool classes:
@WorkflowInit
public ChatWorkflowImpl(String systemPrompt) {
ActivityChatModel activityChatModel = ActivityChatModel.forDefault();
WeatherActivity weatherTool =
Workflow.newActivityStub(
WeatherActivity.class,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(30))
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build())
.build());
StringTools stringTools = new StringTools();
TimestampTools timestampTools = new TimestampTools();
ChatMemory chatMemory =
MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20)
.build();
this.chatClient =
TemporalChatClient.builder(activityChatModel)
.defaultSystem(systemPrompt)
.defaultTools(weatherTool, stringTools, timestampTools)
.defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
Spring AI integration example: Custom Activity options per model
Example of declaring a ChatModelActivityOptions bean for per-model Activity option overrides:
@Bean
public ChatModelActivityOptions chatModelActivityOptions() {
return new ChatModelActivityOptions(
Map.of(
"anthropicChatModel",
ActivityOptions.newBuilder(ActivityChatModel.defaultActivityOptions())
.setStartToCloseTimeout(Duration.ofMinutes(5))
.setScheduleToCloseTimeout(Duration.ofMinutes(15))
.build()));
}
Spring AI integration example: Provider-specific chat options
Example of using provider-specific ChatOptions like AnthropicChatOptions with extended thinking:
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());
Spring AI integration Maven dependency
To add the Spring AI integration, include the temporal-spring-ai dependency alongside temporal-spring-boot-starter and a Spring AI model starter:
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-spring-ai</artifactId>
<version>${temporal-sdk.version}</version>
</dependency>
Spring AI integration Gradle dependency
To add the Spring AI integration with Gradle Groovy DSL:
implementation "io.temporal:temporal-spring-ai:${temporalSdkVersion}"
GoogleGenAIPlugin wraps genai.Client for Workflows
GoogleGenAIPlugin ties the Google Gen AI SDK to Temporal by wrapping a real genai.Client with credentials. You build a genai.Client with credentials on the Worker and pass it to GoogleGenAIPlugin, then pass the plugin to Client.connect. The plugin registers Activities for API calls and swaps in the Pydantic payload converter that serializes Gemini types.
TemporalAsyncClient routes Gemini API calls through Activities
Inside a Workflow, construct a TemporalAsyncClient instead of genai.Client. It has the same shape as the SDK's async client but routes every API call through a Temporal Activity. Each call gets its own timeout, retry policy, and Event History entry, and credentials stay on the Worker.
GoogleGenAI plugin minimum version requirement
The GoogleGenAI integration requires temporalio version 1.31.0 or later. Install with uv add "temporalio[google-genai]>=1.31.0" or pip install "temporalio[google-genai]>=1.31.0".
MCP package installation for MCP tool servers
To use MCP tool servers with GoogleGenAI, the mcp package must be installed separately; the google-genai extra does not include it.
activity_as_tool wraps Activities as Gemini tools
Use activity_as_tool to wrap a Temporal Activity so the Gemini model can call it. The wrapped function keeps its name, docstring, and type signature, which the model uses to decide when to call it. The activity_config parameter must set start_to_close_timeout or schedule_to_close_timeout; there is no default and the tool call fails without one.
Plain Workflow methods as deterministic Gemini tools
A plain Workflow method can be passed directly to Gemini as a tool without wrapping with activity_as_tool. It runs in the Workflow with no Activity dispatch, so it must be deterministic. This is useful for lightweight decisions that don't do I/O.
Multi-turn conversations with client.chats in Workflows
client.chats works inside a Workflow and keeps chat history in Workflow state. Each send_message call runs as its own Activity, so a multi-turn conversation spanning hours or days survives a Worker restart.
Structured output via Pydantic models in GoogleGenAI
The plugin installs Temporal's Pydantic payload converter, so Pydantic models pass through Temporal payloads unchanged. Pass the model as response_schema to generate_content and read the parsed result from response.parsed. If response.parsed is None, the model returned malformed JSON.
MCP server registration with GoogleGenAIPlugin
Register MCP servers by passing mcp_servers dict to GoogleGenAIPlugin, where keys are server names and values are async context manager factories that yield connected, initialized mcp.ClientSession objects. The plugin holds connections on the Worker and runs list_tools and call_tool as Activities.
TemporalMcpClientSession for Workflow MCP access
In a Workflow, pass TemporalMcpClientSession with the server name to the tools list. Automatic function calling discovers and calls the server's tools from there. Set cache_tools=True to reuse the first list_tools result instead of listing before every call.
MCP connection idle timeout configuration
The Worker keeps each MCP connection open between uses and disconnects it after five minutes idle by default. Change this with mcp_connection_idle_timeout parameter on the GoogleGenAIPlugin.
Streaming model output with WorkflowStream
Set streaming_topic on TemporalAsyncClient and host a WorkflowStream in the Workflow's @workflow.init to forward chunks to an external subscriber while the Workflow runs. Each chunk is published to the topic as it arrives. The Workflow's own iteration over the stream is unchanged.
WorkflowStreamClient subscribes to model output streams
A consumer subscribes to streaming topics with WorkflowStreamClient.create(client, workflow_id). Published chunks are Pydantic GenerateContentResponse objects, so the subscribing Client needs pydantic_data_converter.
Streaming Activity batching and delivery semantics
The streaming Activity batches published chunks and flushes them every 100 milliseconds by default; adjust with streaming_batch_interval. Delivery is at-least-once per Activity attempt: if the streaming Activity retries, chunks are republished, so subscribers should tolerate duplicates and treat the Workflow result as the source of truth.
File upload and reference in GoogleGenAI Workflows
client.files runs as Activities, so files are read on the Worker rather than in the Workflow. Upload a file with client.files.upload, then pass the returned handle in contents for generate_content. The file path resolves on the Worker.
Extra credentials for GoogleGenAI operations
Operations requiring separate Google Cloud credentials, such as files.register_files, use the extra_credentials parameter passed to GoogleGenAIPlugin.
client.interactions and client.agents are server-managed
client.interactions and client.agents are server-managed: state lives on Google's backend and each operation runs as its own Activity. create/get return either an Interaction or a streaming response; without stream=True the result is always an Interaction.
Interactions API limitations in Workflows
The Interactions API has no automatic function calling. Declare tools as {"type": "function", ...} dicts and drive the tool loop yourself, running each call with workflow.execute_activity or an activity_as_tool callable. Streamed interactions are batched; the Activity drains the server-sent event stream and the Workflow iterates the collected events.
Vertex AI integration with GoogleGenAI
To use Vertex AI instead of Gemini Developer API, set vertexai=True on both the Workflow's TemporalAsyncClient and the Worker's genai.Client. In the Workflow, pass project and location as arguments rather than reading environment variables to keep the Workflow deterministic. The Worker uses Application Default Credentials instead of an API key.
Vertex AI credentials configuration
For Vertex AI, run gcloud auth application-default login or set GOOGLE_APPLICATION_CREDENTIALS to a service account key file. The genai.Client is created with vertexai=True, project, and location parameters.
vertexai setting must match between Workflow and Worker
The vertexai setting must match on both the Workflow's TemporalAsyncClient and the Worker's genai.Client. A Workflow with vertexai=True against a Worker configured for Gemini Developer API sends requests the backend cannot serve.
Default timeouts and retries for GoogleGenAI API calls
Every GoogleGenAI API call defaults to a 60-second start_to_close_timeout and Temporal's default retry policy. Override for all client calls with activity_config parameter on TemporalAsyncClient.
Setting timeouts and retries via activity_config
Pass ActivityConfig with start_to_close_timeout and retry_policy to TemporalAsyncClient to set timeouts and retries for all API calls. activity_as_tool and TemporalMcpClientSession take their own activity_config, allowing tool and MCP calls to use different limits than model calls.
Do not use genai.Client http_options.retry_options with Temporal
The plugin rejects a genai.Client configured with http_options.retry_options because an SDK-internal retry loop hides its attempts inside a single Activity and compounds with the Temporal retry policy. Let Temporal own retries.
Deterministic Workflow requirement for GoogleGenAI
Workflows must remain deterministic. Use Activities for I/O-bound operations and non-deterministic logic. Plain Workflow methods used as Gemini tools must be deterministic.
Client credentials not needed for Workflow invocation
The starting Client that invokes the Workflow does not need the GoogleGenAIPlugin or credentials. Credentials and the plugin are only needed on the Worker.
HelloWorldWorkflow example with TemporalAsyncClient
A simple Workflow that generates content: @workflow.defn class HelloWorldWorkflow: @workflow.run async def run(self, prompt: str) -> str: client = TemporalAsyncClient() response = await client.models.generate_content(model="gemini-2.5-flash", contents=prompt) return response.text or ""
Worker setup with GoogleGenAIPlugin example
import asyncio, os; from google import genai; from temporalio.client import Client; from temporalio.contrib.google_genai import GoogleGenAIPlugin; from temporalio.worker import Worker; genai_client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"]); plugin = GoogleGenAIPlugin(genai_client); client = await Client.connect(os.environ.get("TEMPORAL_ADDRESS", "localhost:7233"), plugins=[plugin]); worker = Worker(client, task_queue="google-genai-hello-world", workflows=[HelloWorldWorkflow]); await worker.run()
Activity tool configuration with activity_as_tool
When wrapping an Activity as a tool with activity_as_tool, the activity_config parameter must set start_to_close_timeout or schedule_to_close_timeout. Example: activity_as_tool(get_weather, activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)))
Structured output Workflow with Recipe model
from pydantic import BaseModel; class Recipe(BaseModel): name: str; ingredients: list[str]; steps: list[str]; @workflow.defn class StructuredOutputWorkflow: @workflow.run async def run(self, prompt: str) -> Recipe: client = TemporalAsyncClient(); response = await client.models.generate_content(model="gemini-2.5-flash", contents=prompt, config=types.GenerateContentConfig(response_mime_type="application/json", response_schema=Recipe)); recipe = response.parsed; if not isinstance(recipe, Recipe): raise ApplicationError(f"Gemini did not return a valid Recipe: {response.text!r}", non_retryable=True); return recipe
MCP workflow session example
session = TemporalMcpClientSession("echo", cache_tools=True, activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30))); response = await client.models.generate_content(model="gemini-2.5-flash", contents=prompt, config=types.GenerateContentConfig(tools=[session]))
Chat Workflow multi-turn conversation example
@workflow.defn class ChatWorkflow: @workflow.run async def run(self, prompts: list[str]) -> list[str]: client = TemporalAsyncClient(); chat = client.chats.create(model="gemini-2.5-flash"); replies: list[str] = []; for prompt in prompts: response = await chat.send_message(prompt); replies.append(response.text or ""); return replies
Streaming Workflow with WorkflowStream
@workflow.defn class StreamingWorkflow: @workflow.init def __init__(self, prompt: str) -> None: self.stream = WorkflowStream(); self._done = False; @workflow.run async def run(self, prompt: str) -> str: client = TemporalAsyncClient(streaming_topic="gemini"); chunks: list[str] = []; async for chunk in await client.models.generate_content_stream(model="gemini-2.5-flash", contents=prompt): chunks.append(chunk.text or ""); try: await workflow.wait_condition(lambda: self._done, timeout=FINISH_TIMEOUT); except asyncio.TimeoutError: workflow.logger.warning("No finish signal after %s; completing without a subscriber.", FINISH_TIMEOUT); return "".join(chunks); @workflow.signal def finish(self) -> None: self._done = True