How to Create Agents Using Annotation-Based Programming in Embabel
Embabel enables you to build AI agents by adding declarative annotations to standard Spring beans, eliminating boilerplate while maintaining full dependency injection and testing capabilities.
The embabel/embabel-agent framework provides an annotation-driven programming model that mirrors Spring MVC, allowing you to define complete agent behaviors using Java or Kotlin classes. By applying metadata annotations to methods and classes, the framework automatically constructs planning graphs, handles LLM orchestration, and manages tool execution without requiring imperative configuration code.
Core Annotation Concepts
Embabel’s programming model revolves around four primary annotations that declaratively define agent structure and behavior.
The @Agent Annotation
The @Agent annotation—defined in [embabel-agent-api/src/main/java/com/embabel/agent/api/annotation/Agent.java](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/java/com/embabel/agent/api/annotation/Agent.java)—marks a Spring bean as an autonomous agent. This annotation accepts parameters for description, planner type (GOAP or Utility), and metadata.
@Agent(
description = "Generate a funny write-up for a user",
planner = PlannerType.GOAP
)
public class FunnyWriteupAgent {
// ...
}
Actions, Goals, and Conditions
Inside an agent class, you declare executable units using @Action ([Action.java](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/java/com/embabel/agent/api/annotation/Action.java)). Actions can specify toolGroups for external capabilities like web search.
@Goal ([Goal.java](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/java/com/embabel/agent/api/annotation/Goal.java)) and @Condition ([Condition.java](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/java/com/embabel/agent/api/annotation/Condition.java)) provide the planning framework with targets and guardrails. The default Goal-Oriented Action Planning (GOAP) engine evaluates pre-conditions and post-conditions to dynamically sequence actions.
Building Your First Agent
A minimal agent requires only a class annotated with @Agent and a single method marked with @Action and @AchievesGoal. The framework handles all infrastructure concerns including LLM client initialization and prompt construction.
package com.example.agent;
import com.embabel.agent.api.annotation.Agent;
import com.embabel.agent.api.annotation.Action;
import com.embabel.agent.api.annotation.AchievesGoal;
import com.embabel.agent.api.annotation.Export;
import com.embabel.agent.api.Ai;
import com.embabel.agent.api.LlmOptions;
import com.embabel.agent.api.models.OpenAiModels;
@Agent(description = "Generate a funny write-up for a user")
public class FunnyWriteupAgent {
@AchievesGoal(
description = "Produce a markdown write-up",
export = @Export(remote = true, name = "funnyWriteup")
)
@Action
public Writeup writeup(UserInput input, Ai ai) {
var llm = LlmOptions.withModel(OpenAiModels.GPT_41_MINI)
.withTemperature(0.9);
var prompt = """
Take the following user request and craft a humorous markdown write-up:
%s
Include a witty sign-off.
""".formatted(input.getContent());
return ai.withLlm(llm).createObject(prompt, Writeup.class);
}
}
In this example from the test suite, the @AchievesGoal declaration signals that the agent completes its mission when writeup() returns successfully. The Ai parameter provides automatic dependency injection for LLM interactions, while LlmOptions configures model selection and inference parameters.
Multi-Action Agents with Tool Integration
Complex agents utilize multiple @Action methods that the planner chains automatically. The Kotlin example below demonstrates dependency injection, parameterized configuration, and MCP-based tool integration.
package com.example.agent
import com.embabel.agent.api.annotation.Agent
import com.embabel.agent.api.annotation.Action
import com.embabel.agent.api.annotation.AchievesGoal
import com.embabel.agent.api.Ai
import com.embabel.agent.api.LlmOptions
import com.embabel.agent.api.models.OpenAiModels
@Agent(description = "Find news for a star sign")
class StarNewsFinder(
private val horoscopeService: HoroscopeService,
@Value("\${star-news-finder.story.count:5}") private val storyCount: Int = 5
) {
@Action
fun extractPerson(userInput: UserInput, ai: Ai): StarPerson =
ai.withDefaultLlm()
.createObject("""Create a person from this input: $userInput""", StarPerson::class.java)
@Action
fun retrieveHoroscope(starPerson: StarPerson): Horoscope =
Horoscope(horoscopeService.dailyHoroscope(starPerson.sign))
@Action(toolGroups = [ToolGroup.WEB])
fun findNewsStories(person: StarPerson, horoscope: Horoscope, ai: Ai): RelevantNewsStories =
ai.withDefaultLlm().createObject(
"""
${person.name} is a ${person.sign}. Their horoscope is:
<horoscope>${horoscope.summary}</horoscope>
Use web tools to fetch $storyCount relevant news stories.
""".trimIndent()
)
@AchievesGoal(description = "Write an amusing write-up")
@Action
fun writeup(person: StarPerson, news: RelevantNewsStories, horoscope: Horoscope, ai: Ai): Writeup {
val llm = LlmOptions.withModel(OpenAiModels.GPT_41_MINI).withTemperature(0.9)
val prompt = """
Summarize ${person.name}'s horoscope and the following news items:
${news.items.joinToString("\n") { "- ${it.url}: ${it.summary}" }}
Produce markdown with links.
""".trimIndent()
return ai.withLlm(llm).createObject(prompt, Writeup::class.java)
}
}
The toolGroups attribute on findNewsStories() requests specific capability groups (in this case, web search tools) from the tool registry. The framework automatically orchestrates these tools during plan execution, handling authentication, rate limiting, and result parsing.
Planning and Runtime Execution
AgentPlatform Discovery
The AgentPlatform implementation—located in [embabel-agent-starter/src/main/java/com/embabel/agent/platform/AgentPlatform.java](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-starter/src/main/java/com/embabel/agent/platform/AgentPlatform.java)—scans the Spring application context at startup to discover all beans annotated with @Agent. Because agents are standard Spring components, they participate fully in the dependency injection container, supporting AOP, transaction management, and configuration properties.
Planner Configuration
By default, the framework uses Goal-Oriented Action Planning (GOAP) to construct action sequences that satisfy declared goals. You can switch to the Utility-AI planner by modifying the @Agent annotation:
@Agent(
description = "Utility-based decision agent",
planner = PlannerType.UTILITY
)
public class UtilityAgent {
// Actions evaluated by utility score rather than goal distance
}
The planner evaluates the method signatures and return types of @Action methods to determine data flow between steps, automatically passing outputs from one action as inputs to the next.
Testing and Observability
Unit Testing Agent Logic
Because agents are plain Java/Kotlin classes with injected dependencies, you can unit test them without framework overhead. The example below from [StarNewsFinderTest.java](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/test/java/com/embabel/example/simple/horoscope/java/StarNewsFinderTest.java) verifies prompt construction using a fake context:
@Test
void writeupPromptMustContainKeyData() {
HoroscopeService horoscopeService = mock(HoroscopeService.class);
StarNewsFinder finder = new StarNewsFinder(horoscopeService, 5);
var context = new FakeOperationContext();
var person = new StarPerson("Lynda", "Scorpio");
var news = new RelevantNewsStories(List.of(
new NewsStory("https://fake.com", "Cockatoo behavior", "Cockatoos are eating cabbages"),
new NewsStory("https://morefake.com", "Emu movements", "Emus are massing")
));
var horoscope = new Horoscope("Good day for you");
finder.writeup(person, news, horoscope, context);
var prompt = context.getLlmInvocations().getFirst().getPrompt();
assertTrue(prompt.contains(person.getName()));
assertTrue(prompt.contains(person.sign()));
assertTrue(prompt.contains("Cockatoos are eating cabbages"));
}
Execution Tracing with @Tracked
Add the @Tracked annotation—defined in [embabel-agent-observability/src/main/java/com/embabel/agent/observability/annotation/Tracked.java](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-observability/src/main/java/com/embabel/agent/observability/annotation/Tracked.java)—to any method to automatically generate trace spans. This captures execution time, LLM token usage, and tool call latency within the agent's execution hierarchy.
@Tracked
@Action
public AnalysisResult analyzeData(DataInput input, Ai ai) {
// Automatically appears in execution traces
}
Summary
- Annotation-driven declaration: Use
@Agentto mark Spring beans as agents,@Actionfor executable methods, and@Goalor@AchievesGoalfor planning targets. - Automatic discovery: The
AgentPlatformscans and wires agents at runtime, supporting both GOAP and Utility-AI planners via theplannerattribute. - Tool integration: Request external capabilities using
toolGroupson@Action, with automatic orchestration of LLM and MCP-based tools. - Spring-native architecture: Agents support full dependency injection, configuration properties, and AOP, with testability via standard mocking frameworks.
- Built-in observability: Apply
@Trackedto methods for automatic distributed tracing of agent execution paths.
Frequently Asked Questions
What is the difference between @Goal and @AchievesGoal?
@Goal defines a desired state or objective that the planner works toward, often used with explicit condition annotations. @AchievesGoal (used in the examples above) is a convenience annotation that marks a specific @Action method as completing the agent's mission when it executes successfully. According to the Embabel source code, @AchievesGoal combines goal declaration with action completion semantics.
Can I mix Java and Kotlin agents in the same application?
Yes. The AgentPlatform discovers agents through Spring's component scanning, which operates at the bytecode level after compilation. You can define some agents in Java and others in Kotlin within the same Spring Boot application, and the planner will treat them identically when building execution graphs.
How do I switch from GOAP to Utility-AI planning?
Set the planner attribute on the @Agent annotation to PlannerType.UTILITY. The default is PlannerType.GOAP. This change affects how the runtime selects the next action: GOAP uses forward search to satisfy pre-conditions, while Utility-AI evaluates action scores based on current world state and selects the highest-scoring valid action.
Do agents support reactive programming or async execution?
The current implementation in AgentPlatform focuses on synchronous execution models for action methods, though the underlying LLM calls and tool invocations are handled asynchronously by the framework. Methods annotated with @Action should return domain objects directly; the framework manages the concurrency of external I/O operations internally.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →