# How to Implement Goal Approval Using the GoalChoiceApprover Interface in Embabel

> Implement Embabel's GoalChoiceApprover interface to intercept, approve, reject, or modify goal proposals before activation. Control your agent's goals effectively.

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

---

**Implement the `GoalChoiceApprover` interface and register it as a Spring bean to intercept, approve, reject, or modify goal proposals before they become active goals in the Embabel agent framework.**

The Embabel agent framework (`embabel/embabel-agent`) evaluates potential actions by generating **goal proposals** during the planning phase. Before a proposal becomes an active goal, it passes through the `GoalChoiceApprover` hook, allowing you to enforce safety policies, adjust metadata, or filter inappropriate suggestions.

## Understanding the GoalChoiceApprover Interface

The `GoalChoiceApprover` interface lives in the core API module at [`embabel-agent-api/src/main/java/com/embabel/agent/api/goal/GoalChoiceApprover.java`](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/java/com/embabel/agent/api/goal/GoalChoiceApprover.java). It defines a single contract that the planning engine invokes for every candidate goal.

### Interface Method Signature

```java
package com.embabel.agent.api.goal;

/**
 * Called by the planning engine when a new {@link GoalProposal} is created.
 * Implementations may approve, transform, or reject the proposal.
 */
public interface GoalChoiceApprover {

    /**
     * Decide whether the supplied proposal should be turned into a concrete {@link Goal}.
     *
     * @param proposal the candidate goal proposal generated by the agent’s plan
     * @return an {@link ApproverResult} describing the outcome
     */
    ApproverResult approve(GoalProposal proposal);
}

```

The `approve()` method receives a `GoalProposal` object containing the task description, confidence score, and originating tool metadata. Your implementation must return an `ApproverResult` that instructs the engine how to proceed.

### The ApproverResult Outcome

The `ApproverResult` class (located in the same package) provides three static factory methods:

- **`ApproverResult.approved(GoalProposal proposal)`** – Accepts the proposal unchanged and converts it into a concrete goal.
- **`ApproverResult.rejected(String reason)`** – Discards the proposal; the planner attempts to generate a new one. The reason string logged for debugging.
- **`ApproverResult.modified(GoalProposal proposal)`** – Approves a transformed version of the proposal, useful for scrubbing sensitive data or shortening descriptions.

If multiple approver beans are registered, the engine invokes them sequentially. A `REJECTED` result halts the chain immediately, while `APPROVED` or `MODIFIED` allows the next approver to further refine the proposal.

## Implementing Custom Approval Logic

You can implement `GoalChoiceApprover` as a standard Spring component. The framework auto-discovers these beans at runtime and injects them into the planning pipeline.

### Simple Always-Approve Pattern

Use this skeleton for testing or when you want to observe proposals without blocking them:

```java
package com.myapp.approvers;

import com.embabel.agent.api.goal.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * A no-op approver that always approves the incoming proposal.
 */
@Component
@Order(1)  // low order value = runs early
public class PermitAllGoalApprover implements GoalChoiceApprover {

    @Override
    public ApproverResult approve(GoalProposal proposal) {
        System.out.println("Approving: " + proposal.getDescription());
        return ApproverResult.approved(proposal);
    }
}

```

### Conditional Rejection for Safety

Block proposals containing forbidden keywords before they reach execution:

```java
package com.myapp.approvers;

import com.embabel.agent.api.goal.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
@Order(10)  // runs after generic approvers
public class SafeWordGoalApprover implements GoalChoiceApprover {

    private static final Set<String> BLOCKED = Set.of("delete", "shutdown", "format");

    @Override
    public ApproverResult approve(GoalProposal proposal) {
        String lower = proposal.getDescription().toLowerCase();
        for (String word : BLOCKED) {
            if (lower.contains(word)) {
                return ApproverResult.rejected("Contains unsafe keyword: " + word);
            }
        }
        return ApproverResult.approved(proposal);
    }
}

```

When this bean returns `rejected()`, the planning engine abandons the current proposal and attempts to generate an alternative.

### Modifying Proposals Before Approval

Transform proposals by rewriting descriptions or adjusting metadata:

```java
package com.myapp.approvers;

import com.embabel.agent.api.goal.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(20)  // runs after safety checks
public class TruncateDescriptionApprover implements GoalChoiceApprover {

    private static final int MAX_LEN = 120;

    @Override
    public ApproverResult approve(GoalProposal proposal) {
        String desc = proposal.getDescription();
        if (desc.length() > MAX_LEN) {
            String truncated = desc.substring(0, MAX_LEN) + "...";
            GoalProposal modified = proposal.withDescription(truncated);
            return ApproverResult.modified(modified);
        }
        return ApproverResult.approved(proposal);
    }
}

```

The `GoalProposal.withDescription(String)` method creates a shallow copy with the new text, preserving the original confidence score and tool metadata.

## Registering Approvers in the Agent Pipeline

### Spring Bean Auto-Discovery

When using the Embabel Spring Boot starter, the `EmbabelAgentBuilder` automatically collects all beans implementing `GoalChoiceApprover` from the application context. The execution order respects Spring’s `@Order` annotation (lower values execute first).

```java
import com.embabel.agent.api.EmbabelAgentBuilder;

AgentBuilder builder = EmbabelAgentBuilder.builder()
        .withSpringContext(applicationContext)
        .build();

```

### Manual Registration without Spring

If you are not using Spring, register approvers programmatically via the builder:

```java
EmbabelAgentBuilder builder = EmbabelAgentBuilder.builder();
builder.addGoalChoiceApprover(new PermitAllGoalApprover());
builder.addGoalChoiceApprover(new SafeWordGoalApprover());

Agent agent = builder.build();

```

The registration sequence determines execution order; the first registered approver runs first.

## Summary

- The `GoalChoiceApprover` interface in [`embabel-agent-api/src/main/java/com/embabel/agent/api/goal/GoalChoiceApprover.java`](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/java/com/embabel/agent/api/goal/GoalChoiceApprover.java) defines the `approve(GoalProposal)` method for intercepting goal proposals.
- Return `ApproverResult.approved()`, `rejected(String)`, or `modified()` to control whether a proposal becomes an active goal.
- Annotate implementations with `@Component` and `@Order` to participate in Spring’s auto-discovery and control execution priority.
- Use `GoalProposal.withDescription()` to create modified copies when you need to sanitize or truncate text before approval.
- Register approvers manually via `EmbabelAgentBuilder.addGoalChoiceApprover()` when operating outside of Spring.

## Frequently Asked Questions

### What happens when multiple GoalChoiceApprover beans conflict?

The Embabel planning engine chains approvers sequentially according to their `@Order` value or registration order. If any approver returns `REJECTED`, the proposal is discarded immediately and the chain stops. If an earlier approver returns `MODIFIED`, subsequent approvers receive the altered proposal, allowing cumulative transformations.

### Can I modify the goal description before approval?

Yes. Create a new `GoalProposal` instance using `proposal.withDescription(String)` to produce a modified copy, then return `ApproverResult.modified(updatedProposal)`. This technique is useful for trimming verbose LLM outputs, adding prefixes, or removing sensitive information.

### How do I reject a goal proposal with a specific reason?

Invoke `ApproverResult.rejected(String reason)` and pass a descriptive message. While the rejected proposal is discarded, the reason string appears in logs and debugging output, helping you trace why specific goals were blocked by your `GoalChoiceApprover` implementation.

### Does GoalChoiceApprover require Spring?

No. While the `embabel/embabel-agent` repository provides first-class Spring support via `@Component` scanning, you can use the interface in plain Java by calling `addGoalChoiceApprover()` directly on the `EmbabelAgentBuilder`. This is useful for lightweight deployments or custom dependency injection frameworks.