* Add integrations catalog and per-language integrations/ layout
Adds references/integrations.md as a single catalog table for third-party
plugins and integrations (one row per integration: language, what it does,
link to a reference file). Reference files live under
references/{language}/integrations/.
Pre-seeds the catalog by moving the existing Spring Boot reference into
the new layout (references/java/integrations/spring-boot.md) and updating
its inbound links.
SKILL.md gains a single 3-line "Third-Party Integrations" section pointing
at the catalog so SKILL.md no longer accrues a line per new integration.
Each language entry-point (java.md, python.md) gets a one-line pointer to
the catalog filtered to its language.
This lets open integration PRs (Spring AI, Google ADK, OpenAI Agents
sandbox) be rebased onto a consistent home: move their reference file
into references/{language}/integrations/ and add one row to
references/integrations.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop HTML contribution comment from integrations.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
Temporal Spring Boot Integration
Overview
temporal-spring-boot-starter auto-configures workers, registers workflow/activity implementations, and exposes WorkflowClient as a Spring bean. This eliminates the manual WorkflowServiceStubs → WorkflowClient → WorkerFactory setup required without Spring.
Dependency Setup
Maven:
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-spring-boot-starter</artifactId>
<version>[1.0,)</version>
</dependency>
Gradle:
implementation 'io.temporal:temporal-spring-boot-starter:1.+'
The starter transitively includes temporal-sdk and the autoconfigure module. You can declare both temporal-sdk and temporal-spring-boot-starter explicitly, but the starter alone is sufficient.
Minimal Configuration
application.properties:
spring.temporal.connection.target=local
spring.temporal.start-workers=true
spring.temporal.workersAutoDiscovery.packages=greetingapp
application.yml equivalent:
spring:
temporal:
connection:
target: local # shorthand for localhost:7233
start-workers: true
workersAutoDiscovery:
packages:
- greetingapp
workers:
- task-queue: greeting-queue
name: greeting-worker
For self-hosted Temporal, replace local with the server address:
spring.temporal.connection.target=temporal.internal:7233
Interface Design + Spring Annotation Layering
The key concept: Temporal SDK annotations go on interfaces, Spring Boot autoconfigure annotations go on implementation classes. This is identical to non-Spring usage at the interface level.
Workflow Interface (unchanged from non-Spring)
package greetingapp;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
@WorkflowInterface
public interface GreetingWorkflow {
@WorkflowMethod
String greet(String name);
}
Workflow Implementation
package greetingapp;
import io.temporal.activity.ActivityOptions;
import io.temporal.spring.boot.WorkflowImpl;
import io.temporal.workflow.Workflow;
import java.time.Duration;
// @WorkflowImpl replaces manual worker.registerWorkflowImplementationTypes()
// No @Component — workflows are NOT Spring beans; Temporal creates a new instance per execution
@WorkflowImpl(taskQueues = "greeting-queue")
public class GreetingWorkflowImpl implements GreetingWorkflow {
// Activity stubs created via Workflow.newActivityStub() as usual
private final GreetActivities activities = Workflow.newActivityStub(
GreetActivities.class,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(30))
.setTaskQueue("greeting-queue")
.build()
);
@Override
public String greet(String name) {
return activities.greet(name);
}
}
Activity Interface (unchanged from non-Spring)
package greetingapp;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;
@ActivityInterface
public interface GreetActivities {
@ActivityMethod
String greet(String name);
}
Activity Implementation
package greetingapp;
import io.temporal.spring.boot.ActivityImpl;
import org.springframework.stereotype.Component;
// @Component makes this a Spring bean — dependencies can be injected normally
// @ActivityImpl replaces manual worker.registerActivitiesImplementations()
@Component
@ActivityImpl(taskQueues = "greeting-queue")
public class GreetActivitiesImpl implements GreetActivities {
private final GreetingService greetingService;
// Constructor injection works because this is a Spring bean
public GreetActivitiesImpl(GreetingService greetingService) {
this.greetingService = greetingService;
}
@Override
public String greet(String name) {
return greetingService.composeGreeting(name);
}
}
Auto-Discovery
Auto-discovery is how the autoconfigure finds and registers implementations without explicit configuration. It requires both of the following:
@WorkflowImpl(taskQueues = "...")or@ActivityImpl(taskQueues = "...")on the implementation classspring.temporal.workersAutoDiscovery.packagespointing to a package that contains those classes
Missing either one results in silent non-registration — no error, nothing polls the task queue.
The taskQueues attribute routes implementations to the right worker when multiple task queues exist. A worker configured with task queue "greeting-queue" only picks up implementations annotated with taskQueues = "greeting-queue".
Important: @ActivityImpl(taskQueues = "greeting-queue") only registers the activity bean with that worker. It does not route individual activity task executions. Inside the workflow, ActivityOptions.setTaskQueue("greeting-queue") must also be set on the activity stub to route activity tasks to the correct queue.
Comparison: Auto-Discovery vs Explicit YAML Registration
Auto-discovery via annotations:
spring.temporal.workersAutoDiscovery.packages=greetingapp
@Component
@ActivityImpl(taskQueues = "greeting-queue")
public class GreetActivitiesImpl implements GreetActivities { ... }
Explicit YAML registration (alternative):
spring:
temporal:
workers:
- task-queue: greeting-queue
name: greeting-worker
activity-beans:
- greetActivitiesImpl
workflow-classes:
- greetingapp.GreetingWorkflowImpl
Use auto-discovery when implementations are colocated in a single package tree (most apps). Use explicit YAML when you need fine-grained control, want to exclude specific classes, or are registering beans defined elsewhere.
WorkflowClient Injection
WorkflowClient is automatically registered as a Spring bean by the autoconfigure. Inject it into any @Service or @RestController:
package greetingapp;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class GreetingStarter {
private final WorkflowClient client;
public GreetingStarter(WorkflowClient client) {
this.client = client;
}
public String startGreeting(String name) {
var stub = client.newWorkflowStub(
GreetingWorkflow.class,
WorkflowOptions.newBuilder()
.setWorkflowId(UUID.randomUUID().toString())
.setTaskQueue("greeting-queue") // must match the worker's task queue
.build()
);
// Synchronous — blocks until workflow completes
return stub.greet(name);
}
public void startGreetingAsync(String name) {
var stub = client.newWorkflowStub(
GreetingWorkflow.class,
WorkflowOptions.newBuilder()
.setWorkflowId(UUID.randomUUID().toString())
.setTaskQueue("greeting-queue")
.build()
);
// Fire-and-forget — returns immediately
WorkflowClient.start(stub::greet, name);
}
}
Worker Lifecycle
Workers start on ApplicationReadyEvent — after the full Spring context is initialized (DB migrations run, all beans wired). This means activity beans are fully ready before any workflow tasks are processed.
To run a client-only app (one that submits workflows but does not execute them):
spring.temporal.start-workers=false
Testing Strategies
See references/java/testing.md for full details on both approaches.
Spring integration tests — uses an embedded Temporal test server wired into the Spring context:
# src/test/resources/application-test.properties
spring.temporal.test-server.enabled=true
@SpringBootTest
@ActiveProfiles("test")
class GreetingIntegrationTest {
@Autowired WorkflowClient client; // points at the embedded test server
@Test
void testWorkflowThroughSpringContext() { ... }
}
Unit tests without Spring — use TestWorkflowEnvironment or TestWorkflowExtension directly. No Spring context, faster startup, full time-skipping support:
@RegisterExtension
static final TestWorkflowExtension testWorkflow = TestWorkflowExtension.newBuilder()
.setWorkflowTypes(GreetingWorkflowImpl.class)
.setDoNotStart(true)
.build();
Do not mix approaches in the same test class — choose one or the other.
Spring-Specific Gotchas
Workflow impls must not have @Component
Temporal creates a new workflow instance per execution via beanFactory.createBean() (not getBean()). Adding @Component means Spring also registers it as a singleton bean, which can cause confusing lifecycle behavior. Leave @WorkflowImpl classes as plain classes with no Spring annotations.
Activity beans are Spring singletons Temporal may invoke activity methods concurrently across many workflow executions. Keep activity implementations stateless — no mutable instance fields. Use injected services (which are themselves stateless or thread-safe) for all state.
@WorkflowImpl / @ActivityImpl without workersAutoDiscovery.packages → silently ignored
This is the most common setup mistake. If auto-discovery packages are not configured, the annotations are never scanned and nothing registers with the worker. Verify with the Temporal UI that the worker is registering the expected workflow/activity types.
ActivityOptions.setTaskQueue(...) is required on activity stubs
@ActivityImpl(taskQueues = "greeting-queue") registers the activity bean with the worker — it does not set the default task queue for activity execution. Inside workflow code, always set .setTaskQueue(...) in ActivityOptions to explicitly route activity tasks to the correct worker.
Multiple DataConverter beans
If you define more than one DataConverter bean (e.g., a custom JSON converter and a default), the autoconfigure fails with an ambiguity error. Name one of them mainDataConverter to designate it as the primary.