How to Define and Use Domain Models with LLM Tools in Embabel Agent

Embabel Agent converts plain Java/Kotlin records and POJOs into structured LLM outputs by reflecting on their fields to generate prompts, parsing the model's JSON response back into typed objects, and storing them on the agent's blackboard for downstream use.

Embabel Agent treats domain models as structured Java or Kotlin objects that describe the exact shape of data a large-language model (LLM) should produce or consume. By declaring a class or record that represents your desired output and referencing it in an LLM-tool method signature, the framework automatically validates responses, generates natural-language prompts, and manages object lifecycle through the agent's blackboard. This pattern is implemented throughout the embabel/embabel-agent repository, from the reflection utilities in embabel-agent-api to the execution bridge in embabel-agent-tools.

What Are Domain Models in Embabel Agent?

A domain model in Embabel Agent is any public, non-abstract data holder—typically a Java record or POJO—that the framework introspects at runtime. When the agent starts, it scans for these types and builds metadata definitions using DomainTypePropertyDefinition (demonstrated in SemanticsAnnotationJavaTest.java), capturing each field's name, type, and annotation-driven semantics. This metadata drives three core capabilities:

  • Validation: Type-checking and required/optional property enforcement against LLM responses
  • Prompt enrichment: Automatic generation of natural-language descriptions from field names and Javadoc
  • Blackboard integration: Seamless storage of deserialized objects for cross-tool consumption

Defining a Domain Model

Create a domain model by declaring a simple record or class that represents the data structure you expect the LLM to generate. The framework recognizes these types through reflection without requiring marker interfaces.

Java Record Example

Define a record with descriptive field names and documentation:

// src/main/java/com/example/model/Person.java
package com.example.model;

/**
 * Domain model describing a person that the LLM should create.
 */
public record Person(String name, int age) {}

The Person record is automatically recognized as a domain type because it is a public data holder. The DomainTypePropertyDefinition class (tested in SemanticsAnnotationJavaTest) extracts the name and age properties along with their types to build the schema description sent to the LLM.

POJO Alternative

You can also use traditional classes with getters and setters, though records provide the most concise syntax for immutable domain objects.

Wiring Domain Models to LLM Tools

Reference your domain model in a tool method annotated with @Tool. The ToolExecutionEngine (located in the embabel-agent-tools module) intercepts these method calls to orchestrate LLM communication.

Creating a Tool Method with @Tool

// src/main/java/com/example/tools/PersonTool.java
package com.example.tools;

import com.example.model.Person;
import com.embabel.agent.api.annotation.Tool;
import com.embabel.agent.api.annotation.Param;

public class PersonTool {

    @Tool(name = "createPerson", description = "Ask the LLM to generate a Person")
    public Person createPerson(
            @Param(description = "Any extra context for the LLM") String context) {
        // The body is never executed – the framework intercepts the call,
        // sends the generated prompt to the LLM, parses the response into a Person,
        // and returns it here.
        throw new UnsupportedOperationException("Never called");
    }
}

When the agent invokes createPerson("customer profile"), the framework:

  1. Reflects on the Person return type to build a schema prompt ("Create a Person object with fields name (String) and age (int)")
  2. Sends the enriched prompt to the configured LLM
  3. Deserializes the JSON response into a Person instance using Jackson or Kotlin serialization

How the ToolExecutionEngine Bridges LLM Output

The ToolExecutionEngine serves as the bridge between raw LLM responses and typed domain objects. It detects Domain-typed parameters in tool method signatures and automatically marshals LLM output into those types. If validation fails, the engine transforms errors into LLM-friendly retry messages.

Accessing Domain Objects in Actions

Once a tool produces a domain object, Embabel Agent places it on the agent's blackboard. Subsequent actions can consume this data directly through method parameters annotated with @Param.

Consuming Blackboard Data with @Action

// src/main/java/com/example/actions/PrintPersonAction.java
package com.example.actions;

import com.embabel.agent.api.annotation.Action;
import com.embabel.agent.api.annotation.Param;
import com.example.model.Person;

@Action(name = "printPerson")
public class PrintPersonAction {

    public void execute(@Param Person person) {
        System.out.println("Generated person: " + person);
    }
}

The Person instance produced by PersonTool is automatically injected into the execute method without manual parsing or blackboard lookup code.

Platform Configuration and Auto-Discovery

The Embabel Agent platform provides auto-configuration hooks that integrate domain models into the application lifecycle.

AgentPlatformAutoConfigurationFilter Integration

The AgentPlatformAutoConfigurationFilter (found in embabel-agent-autoconfigure/embabel-agent-platform-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/platform/AgentPlatformAutoConfigurationFilter.java) adds a resilient domain model layer to the platform's configuration pipeline. This filter ensures that domain types are registered and validated during the Spring Boot auto-configuration phase.

ShellEnvironmentPostProcessor Example

For concrete examples of domain objects extracted from configuration, examine ShellEnvironmentPostProcessor in the shell autoconfigure module:


embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/shell/spi/ShellEnvironmentPostProcessor.java

This processor demonstrates how ShellEnvironment domain objects are instantiated from configuration properties and later consumed by LLM-driven tools, mirroring the pattern used for LLM-generated domain models.

Summary

  • Domain models are plain Java/Kotlin records or POJOs that Embabel Agent introspects via DomainTypePropertyDefinition to understand field names, types, and constraints.
  • The ToolExecutionEngine automatically converts between LLM JSON responses and typed domain objects when you declare the model as a return type or parameter in @Tool methods.
  • Prompt generation leverages reflection metadata to create natural-language schema descriptions that guide the LLM to produce valid structured output.
  • The blackboard stores deserialized domain instances, allowing @Action methods to consume LLM-generated data without manual parsing or conversion logic.
  • Auto-configuration classes like AgentPlatformAutoConfigurationFilter and ShellEnvironmentPostProcessor demonstrate how the platform integrates domain models into the Spring Boot lifecycle.

Frequently Asked Questions

How does Embabel Agent handle validation errors when the LLM returns malformed data?

When the LLM returns JSON that doesn't match the domain model schema, the ToolExecutionEngine catches deserialization or validation errors and converts them into LLM-friendly error messages. The framework can then automatically retry the request with the error context, allowing the model to correct its output without manual intervention.

Can I use Kotlin data classes instead of Java records for domain models?

Yes. Embabel Agent supports both Java records and Kotlin data classes, using Jackson or Kotlin serialization to marshal between JSON and objects. The reflection scanner in SemanticsAnnotationJavaTest handles both languages' property conventions, extracting field metadata regardless of whether you use Java records, POJOs, or Kotlin data classes.

What is the blackboard and how does it store domain objects?

The blackboard is the agent's shared state container. When a tool method returns a domain object, the ToolExecutionEngine automatically places that instance on the blackboard under the parameter name. Subsequent actions or tools can reference this object by declaring a parameter of the same type annotated with @Param, and the framework injects the stored instance without requiring explicit lookups or casting.

Where is the domain model metadata extracted in the source code?

The core reflection logic that extracts domain model metadata is demonstrated in SemanticsAnnotationJavaTest.java within the embabel-agent-api test suite. This test shows how DomainTypePropertyDefinition instances are created from class fields, capturing property names, types, and annotation-driven metadata that the prompt builder consumes to generate LLM instructions.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →