Files
Donald Pinckney 812f32ee58 Add integrations catalog + per-language integrations/ layout (#151)
* 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>
2026-05-13 13:46:18 -04:00

288 lines
10 KiB
Markdown

# 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:
```xml
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-spring-boot-starter</artifactId>
<version>[1.0,)</version>
</dependency>
```
Gradle:
```groovy
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`:
```properties
spring.temporal.connection.target=local
spring.temporal.start-workers=true
spring.temporal.workersAutoDiscovery.packages=greetingapp
```
`application.yml` equivalent:
```yaml
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:
```properties
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)
```java
package greetingapp;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
@WorkflowInterface
public interface GreetingWorkflow {
@WorkflowMethod
String greet(String name);
}
```
### Workflow Implementation
```java
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)
```java
package greetingapp;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;
@ActivityInterface
public interface GreetActivities {
@ActivityMethod
String greet(String name);
}
```
### Activity Implementation
```java
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:
1. `@WorkflowImpl(taskQueues = "...")` or `@ActivityImpl(taskQueues = "...")` on the implementation class
2. `spring.temporal.workersAutoDiscovery.packages` pointing 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:
```properties
spring.temporal.workersAutoDiscovery.packages=greetingapp
```
```java
@Component
@ActivityImpl(taskQueues = "greeting-queue")
public class GreetActivitiesImpl implements GreetActivities { ... }
```
Explicit YAML registration (alternative):
```yaml
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`:
```java
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):
```properties
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:
```properties
# src/test/resources/application-test.properties
spring.temporal.test-server.enabled=true
```
```java
@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:
```java
@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.