Pre-requisites

  • Basic understanding of Agentic AI
  • Implementation: basics of Embabel

For quick refresher, please refer to previous post.

The Problem

Typically, Agentic workflows require conditional logic. We need the ability to execute different paths based on specific criteria. This dynamic execution makes Agentic AI a powerful tool for automation.

Consider following Order Replacement Service Request workflow.

The workflow branches into different execution paths based on criteria such as replacement eligibility and stock availability.

How can we handle this dynamically within an agentic application?

The Solution

The Embabel Agent Framework provides convenient features to manage conditional agentic workflows. By setting pre-conditions and post-conditions on actions, Embabel can determine the workflow path dynamically. We can use the @Condition annotation to define these rules.

Implementation

Let’s consider a scenario where an order is not eligible for replacement.

If order is ineligible, the Customer Representative should inform the customer immediately. The workflow should skip the stock-checking action entirely.

Note: The complete code for this implementation is shared here.

Each action includes pre and post conditions. These conditions, combined with input and output types of action methods, help Embabel decide the path to the “Goal” dynamically. This process is known as GOAP (Goal Oriented Action Planning).

Defining Conditions

Conditions are implemented using @Condition annotation.

This will be used as pre-condition in @Action annotated methods.


@Condition
boolean isEligible(ReplacementReport replacementReport) {
    return replacementReport.isEligible();
}

Setting Post-Conditions

Some actions will have resulting conditions which will be used as pre-conditions in further actions. In the com.embabel.template.ecommerce.agent.OrderSupportAgent, the replacementReport action sets the isEligible flag based on the item details and rule book.


@Action(description = "Receives order replacement request from customer", post = {"isEligible"})
public ReplacementReport replacementReport(UserInput userInput, OperationContext operationContext) {
    ReplacementReport replacementReport =
            operationContext.ai()
                    .withAutoLlm()
                    .withToolObjects(List.of(ruleBook, catalogue, orders))
                    .createObject(String.format("""
                            Verify eligibility for replacement of items in the order: %s
                            Check item details from inventory and check whether they are eligible for replacement from rule book.
                            STRICT: Set `isEligible` to true if item is eligible for replacement from rule book.
                            """, userInput.getContent()), ReplacementReport.class);

    operationContext.set("isEligible", replacementReport.isEligible());

    return replacementReport;
}

Using Pre-Conditions for Branching

If the order is ineligible, the orderNotEligible action is selected because its pre-condition (defined via SpEL - Spring Expression Language) evaluates to true.


@AchievesGoal(description = "Item not eligible for replacement. Customer representative composes a mail to customer regarding this.")
@Action(pre = {"spel:replacementReport.isEligible == false"})
CustomerReportOrderNotEligible orderNotEligible(ReplacementReport replacementReport, OperationContext operationContext) {
    return operationContext.ai()
            .withAutoLlm()
            .withPromptContributor(Personas.CUSTOMER_REPRESENTATIVE)
            .createObject(String.format("""
                    Reply mail to customer detailing that order is not eligible for replacement with reason.
                    Replacement report: %s
                    """, replacementReport.text()), CustomerReportOrderNotEligible.class);
}

Conversely, if the order is eligible, the stockReport action becomes the next logical step.


@Action(description = "Check stock of items in the oder", pre = {"isEligible"}, post = {"hasStock"})
StockReport stockReport(ReplacementReport replacementReport, OperationContext operationContext) {
    StockReport stockReport = operationContext.ai()
            .withAutoLlm()
            .withPromptContributor(Personas.STORE_REPRESENTATIVE)
            .withToolObjects(List.of(orders, inventory, catalogue))
            .createObject(String.format("""
                    Check stock for: %s
                    STRICT: Set `hasStock` to true if stock is available.
                    """, replacementReport.text()), StockReport.class);

    operationContext.set("hasStock", stockReport.hasStock());

    return stockReport;
}

Similarly, we can implement conditional logic for stock availability.

Testing

  • Customer wants to return an order which is eligible for replacement
starwars> x "I want to return ORD013"
...
{
  "text" : "Dear Customer,\n\nWe are pleased to inform you that the item 'Monitor 24in' (item ID ITM004) is currently in stock with 30 units available. It is eligible for replacement and we have initiated the delivery processing for your convenience.\n\nThank you for your patience. Should you have any further questions or require additional assistance, please do not hesitate to contact us.\n\nBest regards,\nCustomer Service Team"                                                                                                                                                                          
}                                                                                                                                                                                                        

In this case, we can see the order of actions executed using runs command:

starwars> runs
Recent runs:
        [nice_khorana] Goal: [com.embabel.template.ecommerce.agent.OrderSupportAgent.customerConfirmationReport, com.embabel.template.ecommerce.agent.OrderSupportAgent.orderNotEligible, com.embabel.template.ecommerce.agent.OrderSupportAgent.customerReportNoStock]; usage - LLMs: [gpt-4.1-mini] across 4 calls; prompt tokens: 4,751; completion tokens: 324; cost: $0.0024
                com.embabel.template.ecommerce.agent.OrderSupportAgent.replacementReport(4,005ms)
                com.embabel.template.ecommerce.agent.OrderSupportAgent.stockReport(2,369ms)
                com.embabel.template.ecommerce.agent.OrderSupportAgent.deliveryReport(1,710ms)
                com.embabel.template.ecommerce.agent.OrderSupportAgent.customerConfirmationReport(2,736ms)

All actions are visited in a positive flow.

  • Customer wants to return an order which is ineligible for replacement
starwars> x "I want to return ORD003"
...
{
  "text" : "Dear Customer,\n\nThank you for reaching out regarding your order ORD003, which includes the Laptop Dell XPS. As per our replacement policy, electronics items are eligible for replacement. However, we regret to inform you that we currently do not have sufficient stock available for the replacement of this item.\n\nWe apologize for the inconvenience this may cause and are actively working to replenish our inventory. We will notify you as soon as the item becomes available for replacement.\n\nThank you for your understanding and patience.\n\nBest regards,\nCustomer Service Team"        
} 

In this case, order of actions is:

starwars> runs
Recent runs:
        [bold_rubin] Goal: [com.embabel.template.ecommerce.agent.OrderSupportAgent.orderNotEligible, com.embabel.template.ecommerce.agent.OrderSupportAgent.customerReportNoStock, com.embabel.template.ecommerce.agent.OrderSupportAgent.customerConfirmationReport]; usage - LLMs: [gpt-4.1-mini] across 2 calls; prompt tokens: 1,896; completion tokens: 247; cost: $0.0012
                com.embabel.template.ecommerce.agent.OrderSupportAgent.replacementReport(2,791ms)
                com.embabel.template.ecommerce.agent.OrderSupportAgent.customerReportNoStock(2,237ms)

As expected, execution ended immediately without executing verifying stock action.

The Blackboard

Embabel’s agentic flow is powered by the Blackboard design pattern.

While you don’t need to understand the internal mechanics to implement a workflow, knowing about it helps during debugging. The Blackboard acts as a “scratchpad” for the model, storing all context (inputs, outputs, domain objects involved in prompts) throughout the execution.

You can inspect the Blackboard via the Spring Shell using the bb or blackboard command.

Example output after running blackboard command:

starwars> blackboard
InMemoryBlackboard: id=e9ab410f-a546-4647-9540-9ff429144476
map:
  hasStock=true, isEligible=true, it=CustomerConfirmationReport[text=Dear Customer,
We are pleased to inform you that the item ITM002 (Mouse Wireless) is confirmed to be in stock with a sufficient quantity of 120 units available for replacement. Please let us know if you would like to proceed with the replacement or if you need any further assistance.
Best regards,
Customer Service Team]
entries:
  UserInput(content=I want to return ORD007, timestamp=2026-01-26T00:31:36.734738Z), true, ReplacementReport[text=The item in order ORD007 is ITM002 (Mouse Wireless), which belongs to the Electronics category. According to the rules, non-eatable items such as Electronics are eligible for replacement., isEligible=true], true, StockReport[text=The item ITM002 (Mouse Wireless) is in stock with sufficient quantity available for replacement., hasStock=true], DeliveryReport[text=The item ITM002 (Mouse Wireless) is confirmed to be in stock with a sufficient quantity of 120 units available for replacement.], CustomerConfirmationReport[text=Dear Customer,
We are pleased to inform you that the item ITM002 (Mouse Wireless) is confirmed to be in stock with a sufficient quantity of 120 units available for replacement. Please let us know if you would like to proceed with the replacement or if you need any further assistance.
Best regards,
Customer Service Team]

By default, the Blackboard is stored in-memory but it can be configured to use database or file storage for persistence.

Congratulations! 🎉 We have conditional flow set in Order Replacement Service Request workflow.

Summary

Embabel aims to make Agentic AI feel more like Engineering and less like Alchemy. Engineering needs predictability and accuracy. Conditional execution enabled by pre- and post-conditions achieves it to some extent. However, how to evaluate and improve accuracy of agentic AI workflows? We will discuss it in upcoming posts. Please do share your thoughts/suggestions in the comments below.