Skip to main content

Why an Annotation Model?

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.

AgentHandler<I, O>

The entry-point contract for agents. A @FunctionalInterface that takes context and input, returns output:
Lambda form β€” for simple cases:
Class-based form β€” for production agents that compose a Workflow internally:
AgentHandler is pure Java β€” no Spring dependency. It lives in workflow-api so it’s portable across any runtime.

@Agent

Marks a class as a named agent and registers it as a Spring bean:
@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 and @Description

Two metadata annotations for steps and agents:
@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.

StepNames utility

StepNames.resolve(step) reads the @StepName annotation; falls back to step.name() if absent:

AgentRegistry

An immutable map of name β†’ handler. Manual construction β€” you decide what goes in:
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.

Exception Handling

Exception handling mirrors Spring MVC’s @ExceptionHandler + @ControllerAdvice pattern exactly. Two scopes:
  1. Per-agent β€” @ExceptionHandler methods inside an @Agent class handle that agent’s exceptions
  2. Cross-cutting β€” @ExceptionHandler methods inside an @AgentAdvice class handle exceptions from any agent
Per-agent handlers take priority. When multiple handlers match, the most specific exception type wins (subclass beats superclass).

Per-agent handling

Handler methods support two signatures:
  • ReturnType method(ExceptionType ex) β€” just the exception
  • ReturnType method(ExceptionType ex, AgentContext ctx) β€” exception plus context
The exception type can be declared explicitly in @ExceptionHandler(SomeException.class) or inferred from the method’s first parameter.

Cross-cutting handling

@AgentAdvice is a Spring @Component β€” picked up by component scanning, applies to all agents.

Using the resolver

AgentExceptionHandlerResolver ties it together. Pass it the @AgentAdvice instances at construction, then call resolve() on exception:
Resolution order: per-agent handlers on the agent instance first, then cross-cutting advice in registration order.

Spring Boot Wiring

In a Spring Boot application, wire the registry and resolver as beans in a @Configuration class:
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.

Disambiguating steps with @Qualifier

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:
Then inject by qualifier in the agent constructor:
This is the standard Spring pattern β€” @StepName gives you a stable checkpoint key, @Qualifier gives you injection disambiguation.

Injecting Durability with withExecutor()

By default, Workflow.run() creates a WorkflowExecutor with LocalStepRunner (in-process, no persistence). Use withExecutor() to inject a configured executor with a durable StepRunner:
This works on both WorkflowBuilder and SupervisorBuilder. See Durability for the full graduation path from LocalStepRunner β†’ CheckpointingStepRunner β†’ TemporalStepRunner.

Full Example

A complete PR review agent using all 5 annotations:
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.

Annotation Summary

AnnotationTargetModulePurpose
@StepNameTypeworkflow-apiStable checkpoint/Temporal key
@DescriptionType, Methodworkflow-apiHuman-readable description
@ExceptionHandlerMethodworkflow-apiMarks exception handler methods
@AgentTypeworkflow-flowsSpring stereotype for AgentHandler beans
@AgentAdviceTypeworkflow-flowsSpring 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.

What’s Next

Durability

Crash recovery with CheckpointingStepRunner and Temporal integration

DSL Primitives

Branch, loop, parallel, decision, gate, supervisor β€” 10+ composable patterns

API Reference

Step, AgentContext, Gate, WorkflowGraph, StepRunner

Getting Started

Steps, context, your first workflow