How to Integrate Domain Objects with Embabel Agents: A Complete Guide

Integrate domain objects with Embabel agents by registering instances via AgentBuilder.withToolObject(), placing them on the blackboard for dependency injection, and annotating callable methods with @Tool so the LLM can invoke your business logic directly.

Domain objects serve as the bridge between your business logic and LLM-driven agents in the Embabel framework. In embabel/embabel-agent, these objects function as more than simple data carriers—they expose behavior, resolve from the shared blackboard context, and drive planning decisions. This guide demonstrates how to integrate domain objects with Embabel agents using the AgentBuilder API and the DefaultActionMethodManager resolution mechanism.

Architecture Overview

Domain objects connect to the agent runtime through three core components that handle registration, storage, and resolution.

The Agent Builder and Tool Registration

The AgentBuilder constructs the agent graph and registers tools via withToolObject(domainInstance). This method makes your domain instance available to the agent runtime as both a callable tool and a blackboard entry. According to the framework documentation in Domain page.adoc (line 128), only methods explicitly exposed through this mechanism become visible to the LLM.

The Blackboard as Shared Context

The Blackboard acts as a shared mutable context that actions read from and write to during execution. When you place domain objects on the blackboard using blackboard.put(obj), they become discoverable for planning and available for automatic injection into action parameters.

Parameter Resolution via DefaultActionMethodManager

The DefaultActionMethodManager resolves method parameters when actions are invoked. As implemented in DefaultActionMethodManager.kt (lines 170-197), this component detects parameters annotated with or typed as domain objects and pulls matching instances from the blackboard. If no matching object exists, the framework gracefully injects null rather than throwing an exception.

Step-by-Step Integration

Follow these steps to wire your business logic into the Embabel agent runtime.

1. Define a Domain Class with Exposed Methods

Create a class that encapsulates both state and behavior. Annotate any methods you want the LLM to invoke with @Tool.

// src/main/kotlin/com/example/Order.kt
import com.embabel.agent.api.annotation.Tool

data class Order(
    val id: String,
    var status: String,
    val items: List<String>
) {
    /** Exposed to the LLM when the object is added as a tool. */
    @Tool
    fun ship() = apply { status = "shipped" }
}

2. Register the Instance and Populate the Blackboard

Instantiate your domain object and register it with the AgentBuilder. The builder automatically places tool objects on the blackboard, making them available for parameter resolution.

val order = Order("ORD-001", "pending", listOf("widget", "gadget"))
val agent = AgentBuilder()
    .withToolObject(order)               // registers as a tool
    .blackboard.put(order)               // ensures discoverability for planning
    .build()

3. Consume Domain Objects in Actions

Write actions that accept domain objects as parameters. The framework resolves these automatically from the blackboard based on type.

import com.embabel.agent.api.annotation.Action
import com.embabel.agent.api.annotation.Domain

@Action
fun fulfillOrder(@Domain order: Order) {
    order.ship()               // business logic execution
}

The @Domain annotation (or implicit domain typing) instructs DefaultActionMethodManager to resolve the order argument from the blackboard entries, as validated in CostAnnotationJavaTest (lines 75-105).

4. Control Method Visibility

By default, all public methods annotated with @Tool are exposed to the LLM. To keep an object on the blackboard for internal actions while hiding its methods from the planning engine, use the exposure flag:

AgentBuilder()
    .withToolObject(myObj, expose = false)  // blackboard-only, no LLM access
    .build()

Practical Implementation Example

This complete example demonstrates a customer loyalty domain model integrated with an agent, mirroring the validation patterns found in SemanticsAnnotationJavaTest (lines 191-205).

// ---------------------------------------------------------------
// 1️⃣ Domain object with business logic
// ---------------------------------------------------------------
package com.example.domain

import com.embabel.agent.api.annotation.Tool

data class Customer(
    val id: String,
    var loyaltyPoints: Int = 0
) {
    @Tool               // ← visible to the LLM
    fun addPoints(points: Int) = apply { loyaltyPoints += points }
}

// ---------------------------------------------------------------
// 2️⃣ Agent wiring and registration
// ---------------------------------------------------------------
import com.embabel.agent.builder.AgentBuilder

fun main() {
    val customer = Customer("CUST-42")
    val agent = AgentBuilder()
        .withToolObject(customer)   // registers Customer as a tool
        .build()

    // The LLM can now call customer.addPoints(10) or any action
    // that accepts a Customer parameter.
    agent.run()
}

// ---------------------------------------------------------------
// 3️⃣ Action that consumes the domain object
// ---------------------------------------------------------------
import com.embabel.agent.api.annotation.Action
import com.embabel.agent.api.annotation.Domain

class RewardService {
    @Action
    fun grantWelcomeBonus(@Domain customer: Customer) {
        // `customer` is resolved from the blackboard automatically
        // based on DefaultActionMethodManager resolution logic
        customer.addPoints(100)
    }
}

Key Implementation Files

Understanding these source files provides deeper insight into the domain object lifecycle:

  • DefaultActionMethodManager.kt (lines 170-197): Contains the core resolution logic that injects domain objects from the blackboard into action parameters.

  • Domain page.adoc (lines 86-143): Conceptual documentation explaining domain object purpose, blackboard interaction, and visibility rules.

  • CostAnnotationJavaTest.java (lines 75-105): Demonstrates null-handling and injection of domain objects into cost calculation methods.

  • SemanticsAnnotationJavaTest.java (lines 191-205): Validates nested domain-type properties and metadata handling.

  • PackageVisibleTests.java (line 136): Tests visibility rules for domain classes on the blackboard.

  • README.md (lines 791-811): High-level description of the Domain model and its architectural role in the framework.

Summary

  • Define domain classes containing both state and behavior, annotating LLM-callable methods with @Tool.
  • Register instances using AgentBuilder.withToolObject() to expose them as tools to the planning engine.
  • Populate the blackboard with your domain objects to enable automatic dependency injection during action execution.
  • Resolve domain objects in action methods via type hints or @Domain annotations; DefaultActionMethodManager handles the wiring automatically.
  • Handle missing objects gracefully—unresolved domain parameters receive null rather than causing runtime exceptions.

Frequently Asked Questions

How does Embabel resolve domain objects when an action is called?

The DefaultActionMethodManager inspects action method signatures and automatically injects matching instances from the blackboard. According to the source code in DefaultActionMethodManager.kt (lines 170-197), the system checks parameter types against blackboard entries and resolves the appropriate object, or null if no match exists.

Can I prevent the LLM from calling certain methods on my domain object?

Yes. Only methods annotated with @Tool are exposed to the LLM. You can also pass expose = false to withToolObject() to keep the object available on the blackboard for internal actions while completely hiding its methods from the planning engine, as documented in Domain page.adoc (line 128).

What happens if a domain object is not found on the blackboard during action execution?

The parameter resolves to null. The framework gracefully handles missing domain objects by injecting null into the action method parameter, allowing you to implement null-safe logic or conditional checks within your action, validated by test cases in CostAnnotationJavaTest.

Are domain objects only for data, or can they contain business logic?

Domain objects in Embabel are designed to carry both state and behavior. Unlike simple DTOs, they can contain methods that modify internal state—such as ship() or addPoints()—which the LLM can invoke directly when properly annotated with @Tool, enabling rich domain models within your agent architecture.

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 →