Steps and workflows are the plumbing. Agents are the product — the thing you expose to HTTP endpoints, MCP servers, message listeners, and other agents. The annotation model gives you a programming model for declaring agents, naming them, describing them, handling their errors, and looking them up by name at runtime.If you’ve used @Controller + @ControllerAdvice in Spring MVC, you already know how this works. @Agent is to AgentHandler what @Controller is to your handler methods.
Marks a class as a named agent and registers it as a Spring bean:
@Agent("code-review")public class CodeReviewAgent implements AgentHandler<String, ReviewReport> { // ...}
@Agent is a Spring @Component stereotype — classes annotated with it are picked up by component scanning. The value() is the unique agent name used for registry lookup and protocol addressing.
@StepName("analyze-diff")@Description("Parses a PR diff and extracts changed files and hunks")public class AnalyzeDiffStep implements Step<PrEvent, String> { // ...}
@StepName pins a stable name for checkpoint and Temporal keys. When you rename the class, the checkpoint key stays the same — crash recovery and Temporal replay still match the right execution record. Without it, Step.name() (which defaults to the class simple name) is used.@Description provides a human-readable description for traces, visualization, and protocol tool listings. Can be placed on types or methods.Both are pure Java annotations — no Spring dependency, no runtime cost.
The registry is defensive-copied at construction (Map.copyOf()). Subsequent changes to the source map don’t affect it.
Auto-configuration that scans @Agent beans and populates AgentRegistry automatically is planned but not yet implemented. For now, construct the registry manually or in a @Configuration class.
In a Spring Boot application, wire the registry and resolver as beans in a @Configuration class:
@Configuration(proxyBeanMethods = false)public class AgentConfig { @Bean AgentExceptionHandlerResolver agentExceptionHandlerResolver( MyErrorAdvice errorAdvice) { // @AgentAdvice bean return new AgentExceptionHandlerResolver(List.of(errorAdvice)); } @Bean AgentRegistry agentRegistry(PrReviewAgent prReviewAgent) { return new AgentRegistry(Map.of("pr-review", prReviewAgent)); }}
Spring injects the @Agent and @AgentAdvice beans by type. The registry and resolver become injectable anywhere you need them — HTTP controllers, message listeners, MCP tool handlers.
When two steps share the same Step<I, O> signature, Spring can’t tell them apart by type alone. Use @Qualifier on both the bean definition and the injection point:
@Component@Qualifier("assess-code-quality")@StepName("assess-code-quality")public class AssessCodeQualityStep implements Step<PrContext, AssessmentResult> { // ...}@Component@Qualifier("assess-backport")@StepName("assess-backport")public class AssessBackportStep implements Step<PrContext, AssessmentResult> { // ...}
Then inject by qualifier in the agent constructor:
By default, Workflow.run() creates a WorkflowExecutor with LocalStepRunner (in-process, no persistence). Use withExecutor() to inject a configured executor with a durable StepRunner:
// Configure executor with JDBC checkpointingWorkflowExecutor executor = new WorkflowExecutor( checkpointingStepRunner, jdbcTraceRecorder);// Inject into workflow — both run() and build() paths workString result = Workflow.<String, String>define("durable-review") .withExecutor(executor) .step(fetchDiff) .then(analyze) .run(prUrl, ctx);// Or build a reusable Workflow that remembers the executorWorkflow<String, String> workflow = Workflow.<String, String>define("review") .withExecutor(executor) .step(fetchDiff) .then(analyze) .build();workflow.execute(ctx, prUrl); // uses the injected executor
This works on both WorkflowBuilder and SupervisorBuilder. See Durability for the full graduation path from LocalStepRunner → CheckpointingStepRunner → TemporalStepRunner.
A complete PR review agent using all 5 annotations:
// -- Domain types --record PrEvent(String prUrl, String diff) {}record ReviewReport(String summary, String recommendation) {}// -- Steps with stable names --@StepName("analyze-diff")@Description("Parses a PR diff and extracts changed files")public class AnalyzeDiffStep implements Step<PrEvent, String> { @Override public String execute(AgentContext ctx, PrEvent event) { return llm.analyze(event.diff()); } @Override public String name() { return "AnalyzeDiffStep"; }}@StepName("produce-review")@Description("Produces a structured review report")public class ProduceReviewStep implements Step<String, ReviewReport> { @Override public ReviewReport execute(AgentContext ctx, String analysis) { return new ReviewReport(analysis, "LGTM"); } @Override public String name() { return "ProduceReviewStep"; }}// -- Agent --@Agent("pr-review")@Description("Reviews a pull request and produces a structured report")public class PrReviewAgent implements AgentHandler<PrEvent, ReviewReport> { private final Step<PrEvent, String> analyze = new AnalyzeDiffStep(); private final Step<String, ReviewReport> review = new ProduceReviewStep(); @Override public ReviewReport handle(AgentContext ctx, PrEvent event) { return Workflow.<PrEvent, ReviewReport>define("pr-review") .step(analyze) .then(review) .run(event, ctx); } @ExceptionHandler(IllegalStateException.class) public ReviewReport handleBudget(IllegalStateException ex) { return new ReviewReport("Budget exceeded: " + ex.getMessage(), "SKIP"); }}// -- Cross-cutting error handling --@AgentAdvicepublic class GlobalErrorHandler { @ExceptionHandler(Exception.class) public Object handleAny(Exception ex, AgentContext ctx) { return new ReviewReport("Error: " + ex.getMessage(), "ERROR"); }}// -- Wiring --AgentRegistry registry = new AgentRegistry(Map.of( "pr-review", new PrReviewAgent()));var resolver = new AgentExceptionHandlerResolver( List.of(new GlobalErrorHandler()));
A runnable version of this example — with real LLM calls, AgentRegistry lookup, StepNames resolution, and exception handler tests — is in AnnotationModelIT.java in the workflow-dsl-examples repo.
Spring stereotype for cross-cutting error handlers
The first three are pure Java — zero Spring dependency, portable across any runtime. The last two are Spring stereotypes that enable component scanning and auto-discovery.