# How to Create Agents Using Annotation-Based Programming in Embabel

> Learn to create agents with annotation-based programming in Embabel. Add declarative annotations to Spring beans for easy AI agent development, leveraging dependency injection.

- Repository: [Embabel/embabel-agent](https://github.com/embabel/embabel-agent)
- Tags: how-to-guide
- Published: 2026-08-08

---

**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)](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.

```java
@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/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/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/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.

```java
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.

```kotlin
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)](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:

```java
@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/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:

```java
@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)](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.

```java
@Tracked
@Action
public AnalysisResult analyzeData(DataInput input, Ai ai) {
    // Automatically appears in execution traces
}

```

## Summary

- **Annotation-driven declaration**: Use `@Agent` to mark Spring beans as agents, `@Action` for executable methods, and `@Goal` or `@AchievesGoal` for planning targets.
- **Automatic discovery**: The `AgentPlatform` scans and wires agents at runtime, supporting both GOAP and Utility-AI planners via the `planner` attribute.
- **Tool integration**: Request external capabilities using `toolGroups` on `@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 `@Tracked` to 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.