# How to Implement the GoalChoiceApprover Interface in Embabel Agent

> Learn to implement the GoalChoiceApprover interface in Embabel Agent. Inject custom veto logic into goal selection by creating a function that approves or denies goals based on user intent and scores.

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

---

**The `GoalChoiceApprover` interface allows you to inject custom veto logic into the Embabel Agent goal selection process by implementing a single function that returns either `GoalChoiceApproved` or `GoalChoiceNotApproved` based on inspection of the candidate goal, user intent, and ranking scores.**

The `GoalChoiceApprover` interface is a critical extension point in the Embabel Agent framework that governs whether a selected goal should be executed or rejected. Located in the `embabel-agent-api` module, this functional interface enables developers to implement safety guards, confidence thresholds, and business logic filters before the agent acts on a goal. Implementing this interface requires only a single method that evaluates a `GoalChoiceApprovalRequest` and returns a definitive approval response.

## Understanding the GoalChoiceApprover Contract

The interface definition resides in [`embabel-agent-api/src/main/kotlin/com/embabel/agent/api/common/autonomy/GoalChoiceApprover.kt`](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/common/autonomy/GoalChoiceApprover.kt) (lines 49-76). It is declared as a `fun interface`, meaning it supports lambda syntax and single-method implementations.

The contract is straightforward: given a `GoalChoiceApprovalRequest` containing the candidate `Goal`, the original user `intent` string, and a `Rankings<Goal>` object, your implementation must return a `GoalChoiceApprovalResponse`. This design pattern allows the agent to query external validation logic before committing to goal execution.

## Core Response Types

The framework provides two concrete response implementations:

- **GoalChoiceApproved**: Signals acceptance of the goal. Instantiate with `GoalChoiceApproved(request)` where `approved` is automatically set to `true`.
- **GoalChoiceNotApproved**: Signals rejection. Instantiate with `GoalChoiceNotApproved(request, "reason")` to provide an explanation for the veto.

Both classes implement the `GoalChoiceApprovalResponse` sealed interface and are immutable data classes suitable for concurrent environments.

## Implementing the Interface

Because `GoalChoiceApprover` is a functional interface, you can implement it using a lambda expression without creating a named class.

### Basic Lambda Implementation

Create an approver instance directly:

```kotlin
val myApprover = GoalChoiceApprover { request ->
    // Custom validation logic
    if (isValid(request)) {
        GoalChoiceApproved(request)
    } else {
        GoalChoiceNotApproved(request, "Validation failed")
    }
}

```

### Accessing Request Data

The `GoalChoiceApprovalRequest` exposes three key properties:

- `request.goal`: The candidate `Goal` entity selected by the ranking algorithm (defined in [`embabel-agent-api/src/main/kotlin/com/embabel/agent/core/Goal.kt`](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/core/Goal.kt)).
- `request.intent`: The raw user intent string that triggered goal selection.
- `request.rankings`: A `Rankings<Goal>` object containing scored alternatives (see [`embabel-agent-common/embabel-agent-ranking/src/main/kotlin/com/embabel/agent/ranking/Rankings.kt`](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-common/embabel-agent-ranking/src/main/kotlin/com/embabel/agent/ranking/Rankings.kt)).

Inspect these fields to implement context-aware approval logic.

### Score-Based Validation

To approve only when confidence exceeds a threshold, inspect the top ranking:

```kotlin
val topScore = request.rankings.rankings().firstOrNull()?.score ?: 0.0
if (topScore > 0.8) {
    GoalChoiceApproved(request)
} else {
    GoalChoiceNotApproved(request, "Confidence $topScore below threshold")
}

```

## Built-in Factory Methods

The interface provides convenient factory methods for common use cases:

**APPROVE_ALL**: A default implementation that approves every request, defined at line 59:

```kotlin
val approver = GoalChoiceApprover.APPROVE_ALL

```

**approveWithScoreOver**: Creates an approver that validates against a minimum `ZeroToOne` score threshold (lines 62-73):

```kotlin
val strictApprover = GoalChoiceApprover.approveWithScoreOver(ZeroToOne.of(0.75))

```

## Integration with Agent Configuration

Once implemented, register your approver with the agent builder. Most `AgentBuilder` implementations (located in [`embabel-agent-api/src/main/kotlin/com/embabel/agent/api/AgentBuilder.kt`](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/AgentBuilder.kt)) accept the approver via a dedicated setter:

```kotlin
val agent = AgentBuilder()
    .goalChoiceApprover(myApprover)
    // ... other configuration
    .build()

```

The approver is invoked during the goal selection phase, after ranking but before execution.

## Complete Implementation Example

Below is a production-ready implementation that combines multiple validation strategies:

```kotlin
val safeApprover = GoalChoiceApprover { request ->
    // Block debugging intents in production
    if ("debug" in request.intent.lowercase()) {
        GoalChoiceNotApproved(
            request,
            reason = "Debug intents are disallowed in production environments."
        )
    } else {
        // Enforce minimum confidence threshold
        val topScore = request.rankings.rankings().firstOrNull()?.score ?: 0.0
        if (topScore <= 0.5) {
            GoalChoiceNotApproved(
                request,
                reason = "Score $topScore is not above 0.5 threshold"
            )
        } else {
            GoalChoiceApproved(request)
        }
    }
}

```

For reusable composition, chain the built-in factory with custom logic:

```kotlin
val composedApprover = GoalChoiceApprover { request ->
    // First apply standard score validation
    val scoreResult = GoalChoiceApprover.approveWithScoreOver(ZeroToOne.of(0.5))(request)
    if (!scoreResult.approved) return@GoalChoiceApprover scoreResult
    
    // Then apply custom business rules
    if (request.goal.id.value.contains("restricted")) {
        GoalChoiceNotApproved(request, "Restricted goal category")
    } else {
        scoreResult
    }
}

```

## Summary

- **GoalChoiceApprover** is a functional interface in `embabel-agent-api` that controls goal execution approval.
- Implementations receive a `GoalChoiceApprovalRequest` containing the goal, intent, and rankings.
- Return **GoalChoiceApproved** to allow execution or **GoalChoiceNotApproved** to veto with a reason.
- Use **lambda syntax** for concise implementations or factory methods like `approveWithScoreOver` for standard validations.
- Register the approver via the **AgentBuilder** configuration to activate it in the agent pipeline.

## Frequently Asked Questions

### What is the difference between GoalChoiceApprover and GoalRanker?

While `GoalRanker` orders candidate goals by relevance, `GoalChoiceApprover` acts as a final gatekeeper that can veto even the top-ranked goal. The ranker determines which goal is best; the approver determines if that goal is acceptable to execute.

### Can I use multiple GoalChoiceApprovers together?

The interface design supports composition through decorator patterns. Wrap multiple approvers in a single implementation that chains their logic, or create a composite that fails fast on the first rejection. The built-in `APPROVE_ALL` constant serves as a convenient identity element for composition.

### How do I access the confidence score of the selected goal?

Inspect `request.rankings.rankings().firstOrNull()?.score` within your implementation. The `Rankings` API (defined in [`embabel-agent-common/embabel-agent-ranking/src/main/kotlin/com/embabel/agent/ranking/Rankings.kt`](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-common/embabel-agent-ranking/src/main/kotlin/com/embabel/agent/ranking/Rankings.kt)) returns a sorted list where the first element represents the highest-scored goal selected by the ranker.

### Is the GoalChoiceApprover thread-safe?

Yes. The interface is stateless by design, and the provided response types (`GoalChoiceApproved` and `GoalChoiceNotApproved`) are immutable data classes. Implementations should avoid maintaining mutable state to ensure safe concurrent execution across multiple agent requests.