Spring AI Recipe: Getting Started with Embabel
What if your agent could figure out its own plan instead of following a predefined workflow? That’s the idea behind Embabel. In this recipe, you’ll create a simple goal-driven agent using Embabel.
Embabel is an agentic framework created by Rod Johnson, the creator of Spring, and built on top of Spring AI. It uses Goal-Oriented Action Planning (GOAP) to autonomously determine how to achieve a goal from a given input and the current state of the world.
This differs from workflow-based approaches such as the graph workflows we’ve built in previous recipes using Spring AI Alibaba Graph. Rather than following a predefined sequence of steps, Embabel determines which actions should be performed to achieve a desired outcome.
One of Embabel’s most interesting characteristics is that its planning is deterministic. Given the same input and world state, it will produce the same plan every time. And for Spring developers, the programming model will feel familiar, relying heavily on annotations and strongly-typed objects.
In this recipe, we’ll build a simple Embabel agent that greets someone by name. Despite its simplicity, it demonstrates the key concepts you’ll use when developing more sophisticated Embabel agents.
Creating an Embabel Agent
At the time of writing, Embabel 0.4.0 targets Spring Boot 3.x and Spring AI 1.x. Therefore, create a Spring Boot 3.5.x project rather than a Spring Boot 4 project.
Also, although Embabel itself is developed in Kotlin and provides a rich Kotlin development experience, we’re going to stick with Java to develop this recipe’s agent.
Create a brand new Spring Boot project. Select Spring Boot 3.5.15 (or the latest release in the Spring Boot 3.5.x line). No additional dependencies are required from the Initializr. We’ll add the Embabel dependencies manually.
Once the project has been created, add the following dependencies to the build:
ext {
embabelVersion = '0.4.0'
}
dependencies {
implementation("com.embabel.agent:embabel-agent-starter:$embabelVersion")
implementation "com.embabel.agent:embabel-agent-starter-openai:$embabelVersion"
implementation "com.embabel.agent:embabel-agent-starter-shell:$embabelVersion"
//...dependencies from the Initializr go here ...
}Next create a new agent class:
package com.example.embabelagent;
import com.embabel.agent.api.annotation.AchievesGoal;
import com.embabel.agent.api.annotation.Action;
import com.embabel.agent.api.annotation.Agent;
import com.embabel.agent.api.common.Ai;
import com.embabel.agent.domain.io.UserInput;
@Agent(name = "helloAgent",
description = "Says hello to someone.")
public class HelloAgent {
@Action(description = "Derive Person from input")
public Person derivePerson(UserInput userInput, Ai ai) {
var prompt = String.format(
"Derive the person from the input: %s",
userInput.getContent());
return ai
.withDefaultLlm()
.createObject(prompt, Person.class);
}
@Action(description = "Say hello to someone")
@AchievesGoal(
description = "Someone has been greeted with a friendly hello")
public Greeting sayHello(Person person) {
return new Greeting(
String.format("Hello, %s!", person.name()));
}
}This agent contains two actions:
derivePerson()uses an LLM to extract aPersonobject from the user's input.sayHello()creates a greeting for that person.
The sayHello() method is annotated with @AchievesGoal, indicating that it fulfills the goal of greeting someone.
This simple example illustrates several core Embabel concepts:
- Methods annotated with
@Actionrepresent actions the agent can perform. - Methods annotated with
@AchievesGoalrepresent goal-achieving actions. - Actions communicate through strongly-typed Java objects rather than strings.
Embabel uses these types to determine dependencies between actions. Because sayHello() requires a Person and derivePerson() produces a Person, Embabel can automatically determine that it must invoke derivePerson() before calling sayHello().
We’ll also need two simple domain types.
Greeting:
public record Greeting(String greetingText) {}Person:
public record Person(String name) {}Trying It Out
Run the application. After startup completes, you’ll be presented with the Embabel shell prompt:
embabel>From this prompt you can explore the agent and its capabilities.
- Type
agentsto see the available agents. - Type
actionsto see available actions and their preconditions and postconditions. - Type
goalsto see the goals that can be achieved. - Type
helpto see additional commands. - Type
exitorquitto leave the shell.
You can also interact with the agent using the execute command:
embabel> execute "Say hello to Rod"Or use the shorter x alias:
embabel> x "Say hello to Rod"Before executing the request, Embabel determines which actions are required to achieve the goal. In this case, it first derives a Person from the input and then invokes the action that produces the greeting.
You’ll see quite a bit of logging as Embabel plans and executes the request. Eventually you’ll receive the final result:
{
"greetingText" : "Hello, Rod!"
}We won’t examine the log output in detail in this recipe, but it’s worth spending some time looking through it. The logs provide valuable insight into how Embabel plans and executes actions to achieve a goal.
Using the Agent via MCP
Working with agents through Embabel’s shell is useful during development, but in a real-world environment you’ll typically expose them through MCP.
To do that, replace the shell dependency with Embabel’s MCP server dependency:
implementation(
"com.embabel.agent:embabel-agent-starter-mcpserver:$embabelVersion")This allows Embabel to expose selected goals as MCP tools that can be invoked by any MCP-compatible client.
Next, configure the MCP server to use the Streamable HTTP transport and listen on port 3000:
server.port=3000
spring.ai.mcp.server.protocol=streamableFinally, update the @AchievesGoal annotation on the sayHello() method:
@Action(description = "Say hello to someone")
@AchievesGoal(
description = "Someone has been greeted with a friendly hello",
export = @Export(
name = "sayHello",
remote = true,
startingInputTypes = Person.class))
public Greeting sayHello(Person person) {
return new Greeting(
String.format("Hello, %s!", person.name()));
}The @Export configuration tells Embabel to expose this goal as a remote MCP tool named sayHello. The startingInputTypes attribute identifies the type that MCP clients should provide when invoking the tool.
Start the application again. This time you won’t see the Embabel shell prompt. Instead, the application is running as an MCP server. To interact with it, connect using an MCP client such as MCP Inspector or MCPJam. In either case, connect to the MCP server at the following URL: http://localhost:3000/mcp.
Once connected, you can inspect the available tools. In MCP Inspector you’ll see something like this:
Notice that several tools are available. For now, we’ll focus on the sayHello tool. The others support more advanced Embabel functionality that we'll explore in future recipes.
Select the sayHello tool and enter a value into the name field. Then click Run Tool. The response should look something like this:
Summary
In this recipe, you created a simple Embabel agent, executed it through Embabel’s shell interface, and then exposed it as an MCP tool. Although the example was intentionally simple, it introduced the key concepts behind Embabel: actions, goals, strongly-typed planning, and deterministic goal-oriented execution.
In future recipes, we’ll build more sophisticated agents that perform multi-step planning and reason over richer world models.
Get the code for this and other Spring AI recipes at https://github.com/habuma/spring-ai-recipes.
If you’re exploring Spring AI, I’ve written a book (Spring AI in Action) that covers the core foundations — and I’ll continue sharing newer patterns like this here as the ecosystem evolves. You can find my book on the publisher’s website, at Amazon, or anywhere you buy technical books.