Implementing Transitions with @TransitionSet
Overview
Transitions define how to navigate between states. With the new @TransitionSet annotation system (Brobot 1.2.0+), all transitions for a state are grouped together in a single class, providing better organization and clearer intent.
Modern Approach: Unified Transition Classes
PromptTransitions.java
// Note: BrobotProperties must be injected as a dependency
@Autowired
private BrobotProperties brobotProperties;
package com.claude.automator.transitions;
import org.springframework.stereotype.Component;
import com.claude.automator.states.PromptState;
import com.claude.automator.states.WorkingState;
import io.github.jspinak.brobot.action.Action;
import io.github.jspinak.brobot.annotations.FromTransition;
import io.github.jspinak.brobot.annotations.IncomingTransition;
import io.github.jspinak.brobot.annotations.TransitionSet;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* All transitions for the Prompt state.
* Contains FromTransitions from other states TO Prompt,
* and a IncomingTransition to verify arrival at Prompt.
*/
@TransitionSet(state = PromptState.class, description = "Claude Prompt state transitions")
@Component
@RequiredArgsConstructor
@Slf4j
public class PromptTransitions {
private final PromptState promptState;
private final WorkingState workingState;
private final Action action;
/**
* Navigate from Working state back to Prompt.
* This occurs when Claude finishes processing and returns to the prompt.
*/
@FromTransition(from = WorkingState.class, priority = 1, description = "Return from Working to Prompt")
public boolean fromWorking() {
log.info("Navigating from Working to Prompt");
// In mock mode, just return true for testing
if (io.github.jspinak.brobot.config.core.brobotProperties.getCore().isMock()) {
log.info("Mock mode: simulating successful navigation");
return true;
}
// Wait for Claude to finish processing and return to prompt
// This might involve waiting for the working indicator to disappear
return action.find(promptState.getClaudePrompt()).isSuccess();
}
/**
* Verify that we have successfully arrived at the Prompt state.
* Checks for the presence of the Claude prompt input area.
*/
@IncomingTransition(description = "Verify arrival at Prompt state", required = true)
public boolean verifyArrival() {
log.info("Verifying arrival at Prompt state");
// In mock mode, just return true for testing
if (io.github.jspinak.brobot.config.core.brobotProperties.getCore().isMock()) {
log.info("Mock mode: simulating successful verification");
return true;
}
// Check for presence of prompt-specific elements
boolean foundPrompt = action.find(promptState.getClaudePrompt()).isSuccess();
if (foundPrompt) {
log.info("Successfully confirmed Prompt state is active");
return true;
} else {
log.error("Failed to confirm Prompt state - prompt elements not found");
return false;
}
}
}
WorkingTransitions.java
package com.claude.automator.transitions;
import org.springframework.stereotype.Component;
import com.claude.automator.states.PromptState;
import com.claude.automator.states.WorkingState;
import io.github.jspinak.brobot.action.Action;
import io.github.jspinak.brobot.action.ActionResult;
import io.github.jspinak.brobot.action.ObjectCollection;
import io.github.jspinak.brobot.action.basic.click.ClickOptions;
import io.github.jspinak.brobot.action.basic.find.PatternFindOptions;
import io.github.jspinak.brobot.action.basic.type.TypeOptions;
import io.github.jspinak.brobot.annotations.FromTransition;
import io.github.jspinak.brobot.annotations.IncomingTransition;
import io.github.jspinak.brobot.annotations.TransitionSet;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* All transitions for the Working state.
* Contains FromTransitions from other states TO Working,
* and a IncomingTransition to verify arrival at Working.
*/
@TransitionSet(state = WorkingState.class, description = "Claude Working state transitions")
@Component
@RequiredArgsConstructor
@Slf4j
public class WorkingTransitions {
private final PromptState promptState;
private final WorkingState workingState;
private final Action action;
/**
* Navigate from Prompt to Working by submitting a command.
* This transition occurs when the user submits a prompt and Claude begins processing.
*/
@FromTransition(from = PromptState.class, priority = 1, description = "Submit prompt to start working")
public boolean fromPrompt() {
try {
log.info("Navigating from Prompt to Working");
// In mock mode, just return true for testing
if (io.github.jspinak.brobot.config.core.brobotProperties.getCore().isMock()) {
log.info("Mock mode: simulating successful navigation");
return true;
}
// Using the fluent API to chain actions: find -> click -> type
PatternFindOptions findClickType = new PatternFindOptions.Builder()
.setPauseAfterEnd(0.5) // Pause before clicking
.then(new ClickOptions.Builder()
.setPauseAfterEnd(0.5) // Pause before typing
.build())
.then(new TypeOptions.Builder()
.build())
.build();
// Create target objects for the chained action
ObjectCollection target = new ObjectCollection.Builder()
.withImages(promptState.getClaudePrompt()) // For find & click
.withStrings(promptState.getContinueCommand()) // For type (continue with Enter)
.build();
// Execute the chained action
ActionResult result = action.perform(findClickType, target);
if (result.isSuccess()) {
log.info("Successfully triggered transition from Prompt to Working");
return true;
} else {
log.warn("Failed to execute transition: {}", result.getActionDescription());
return false;
}
} catch (Exception e) {
log.error("Error during Prompt to Working transition", e);
return false;
}
}
/**
* Verify that we have successfully arrived at the Working state.
* Checks for the presence of the working indicator.
*/
@IncomingTransition(description = "Verify arrival at Working state", required = true)
public boolean verifyArrival() {
log.info("Verifying arrival at Working state");
// In mock mode, just return true for testing
if (io.github.jspinak.brobot.config.core.brobotProperties.getCore().isMock()) {
log.info("Mock mode: simulating successful verification");
return true;
}
// Check for presence of working-specific elements
boolean foundWorkingIndicator = action.find(workingState.getWorkingIndicator()).isSuccess();
if (foundWorkingIndicator) {
log.info("Successfully confirmed Working state is active");
return true;
} else {
log.error("Failed to confirm Working state - working indicator not found");
return false;
}
}
}
Key Features of @TransitionSet
1. Unified Class Structure
All transitions for a state are in ONE class:
@FromTransition
methods define how to get TO this state FROM other states@IncomingTransition
method (only ONE per class) verifies arrival at this state
2. Clear Annotations
@TransitionSet(state = TargetState.class, description = "Documentation")
- state: The state these transitions belong to (required)
- description: Optional documentation
@FromTransition(from = SourceState.class, priority = 1, description = "Navigation logic")
- from: The source state (required)
- priority: Higher values are preferred when multiple paths exist
- description: Optional documentation
@IncomingTransition(description = "Verification logic", required = true)
- required: Whether verification must succeed (default: false)
- description: Optional documentation
3. Mock Mode Support
Always include mock mode checks for testing:
@FromTransition(from = SourceState.class)
public boolean fromSource() {
if (io.github.jspinak.brobot.config.core.brobotProperties.getCore().isMock()) {
log.info("Mock mode: simulating successful navigation");
return true;
}
// Real navigation logic
return action.click(element).isSuccess();
}
Action Chaining Pattern
The fluent API enables elegant action sequences:
// Chain multiple actions in sequence
PatternFindOptions chainedAction = new PatternFindOptions.Builder()
.setPauseAfterEnd(0.5) // Wait after finding
.then(new ClickOptions.Builder()
.setPauseAfterEnd(0.5) // Wait after clicking
.build())
.then(new TypeOptions.Builder()
.build()) // Type text
.build();
// Execute all actions in sequence
ActionResult result = action.perform(chainedAction, target);
Comparison: Old vs New
Old Approach (Pre-1.2.0)
Multiple separate transition classes:
// Separate file for each transition
@Transition(from = PromptState.class, to = WorkingState.class)
public class PromptToWorkingTransition {
public boolean execute() {
return action.click(promptState.getButton()).isSuccess();
}
}
// Another separate file
@Transition(from = WorkingState.class, to = PromptState.class)
public class WorkingToPromptTransition {
public boolean execute() {
return action.wait(5).isSuccess();
}
}
New Approach (1.2.0+)
All transitions for a state in ONE class:
@TransitionSet(state = WorkingState.class)
@Component
public class WorkingTransitions {
@FromTransition(from = PromptState.class, priority = 1)
public boolean fromPrompt() {
if (brobotProperties.getCore().isMock()) return true;
return action.click(promptState.getButton()).isSuccess();
}
@IncomingTransition(required = true)
public boolean verifyArrival() {
if (brobotProperties.getCore().isMock()) return true;
return action.find(workingState.getIndicator()).isSuccess();
}
}
File Organization
Organize your transitions alongside states:
src/main/java/com/claude/automator/
├── states/
│ ├── PromptState.java
│ └── WorkingState.java
└── transitions/
├── PromptTransitions.java # All transitions for Prompt state
└── WorkingTransitions.java # All transitions for Working state
Best Practices
-
Use Required Annotations:
@TransitionSet(state = MyState.class)
@Component // For Spring dependency injection
@RequiredArgsConstructor // For constructor injection
@Slf4j // For logging -
Descriptive Method Names: Use
fromStateName()
pattern for clarity -
Mock Mode Support: Always include mock mode checks for testing
-
Handle Failures Gracefully:
@FromTransition(from = SourceState.class)
public boolean fromSource() {
try {
// Transition logic
return action.click("button").isSuccess();
} catch (Exception e) {
log.error("Transition failed", e);
return false;
}
} -
Log Appropriately: Info for success, warn for expected failures, error for exceptions
Benefits of @TransitionSet Approach
- Better Organization: All transitions for a state in ONE place
- Clearer Intent: FromTransitions vs IncomingTransition makes flow obvious
- Less Boilerplate: No manual StateTransitions builders
- Compile-Time Safety: IDE immediately shows if states don't exist
- Easier Testing: Each transition method can be tested independently
- Natural Structure: File organization mirrors state structure
Testing Transitions
The new format makes testing straightforward:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestConfiguration.class})
public class WorkingTransitionsTest {
@Autowired
private WorkingTransitions workingTransitions;
@MockBean
private Action action;
@Test
public void testFromPromptTransition() {
// Given
when(action.perform(any(), any()))
.thenReturn(new ActionResult.Builder().setSuccess(true).build());
// When
boolean result = workingTransitions.fromPrompt();
// Then
assertTrue(result);
}
@Test
public void testVerifyArrival() {
// Given
when(action.find(any()))
.thenReturn(new ActionResult.Builder().setSuccess(true).build());
// When
boolean arrived = workingTransitions.verifyArrival();
// Then
assertTrue(arrived);
}
}
Next Steps
With states and transitions defined using @TransitionSet annotations, the entire state machine is automatically configured. The framework handles all registration and wiring - you just focus on your automation logic!