Approval Methods

Human-in-the-loop approval workflows for Go agents

Approval Methods

Human-in-the-loop approval workflows for Go agents

The approval methods on the Go SDK client let an agent pause execution and wait for a human decision before continuing. The control plane transitions the execution to a waiting state, notifies reviewers, and resumes the agent once a response arrives.

Approval requests are tied to a specific execution. The agent must pass both nodeID and executionID so the control plane can route the request to the correct workflow run.

RequestApproval

Submits an approval request to the control plane and transitions the execution to waiting. Returns the generated request ID and a URL where reviewers can respond.

package main

import (
    "context"
    "fmt"

    agentfield "github.com/Agent-Field/agentfield/sdk/go/client"
)

func requestReview(ctx context.Context, c *agentfield.Client, nodeID, executionID string) error {
    resp, err := c.RequestApproval(ctx, nodeID, executionID, agentfield.RequestApprovalRequest{
        Title:          "Approve deployment to production",
        Description:    "Agent has prepared a deployment plan. Please review before proceeding.",
        TemplateType:   "generic",
        ProjectID:      "proj-abc123",
        ExpiresInHours: 24,
        Payload: map[string]interface{}{
            "environment": "production",
            "version":     "v2.4.1",
            "changes":     []string{"database migration", "config update"},
        },
    })
    if err != nil {
        return fmt.Errorf("failed to request approval: %w", err)
    }

    fmt.Printf("Approval request created: %s\n", resp.ApprovalRequestID)
    fmt.Printf("Review URL: %s\n", resp.ApprovalRequestURL)
    return nil
}

Parameters

RequestApproval accepts a RequestApprovalRequest struct:

Prop

Type

Return value

Prop

Type


GetApprovalStatus

Fetches the current status of an approval request for an execution. Use this when you want to poll manually or check status once without blocking.

status, err := c.GetApprovalStatus(ctx, nodeID, executionID)
if err != nil {
    return fmt.Errorf("failed to get approval status: %w", err)
}

switch status.Status {
case "approved":
    fmt.Println("Approved at:", status.RespondedAt)
    fmt.Println("Reviewer response:", status.Response)
case "rejected":
    fmt.Println("Rejected at:", status.RespondedAt)
case "pending":
    fmt.Println("Still waiting for a decision")
case "expired":
    fmt.Println("Request expired without a response")
}

Parameters

Prop

Type

Return value: ApprovalStatusResponse

Prop

Type


WaitForApproval

Blocks until the approval resolves or the context is cancelled. Internally polls GetApprovalStatus with exponential backoff, so it won't hammer the control plane while waiting for a human.

ctx, cancel := context.WithTimeout(context.Background(), 24*time.Hour)
defer cancel()

status, err := c.WaitForApproval(ctx, nodeID, executionID, &agentfield.WaitForApprovalOptions{
    PollInterval:  5 * time.Second,
    MaxInterval:   60 * time.Second,
    BackoffFactor: 2.0,
})
if err != nil {
    return fmt.Errorf("approval wait failed: %w", err)
}

if status.Status == "approved" {
    // safe to proceed
    return proceedWithAction(ctx, status.Response)
}

return fmt.Errorf("approval not granted: status=%s", status.Status)

Parameters

Prop

Type

WaitForApprovalOptions

Prop

Type

WaitForApproval returns an error if the context is cancelled or times out. It does not return an error for rejected or expired statuses — check status.Status after a successful return.


Complete examples

Request approval and block until a human responds:

package main

import (
    "context"
    "fmt"
    "time"

    agentfield "github.com/Agent-Field/agentfield/sdk/go/client"
)

func runWithApproval(ctx context.Context, c *agentfield.Client, nodeID, executionID string) error {
    // 1. Submit the approval request
    req, err := c.RequestApproval(ctx, nodeID, executionID, agentfield.RequestApprovalRequest{
        Title:          "Approve data export",
        Description:    "Agent wants to export 50,000 customer records to S3.",
        TemplateType:   "generic",
        ProjectID:      "proj-abc123",
        ExpiresInHours: 8,
        Payload: map[string]interface{}{
            "record_count": 50000,
            "destination":  "s3://exports/customers-2026-03-03.csv",
        },
    })
    if err != nil {
        return fmt.Errorf("request approval: %w", err)
    }

    fmt.Printf("Waiting for approval: %s\n", req.ApprovalRequestURL)

    // 2. Block until resolved (8-hour deadline matches expiry)
    waitCtx, cancel := context.WithTimeout(ctx, 8*time.Hour)
    defer cancel()

    status, err := c.WaitForApproval(waitCtx, nodeID, executionID, nil)
    if err != nil {
        return fmt.Errorf("approval timed out or cancelled: %w", err)
    }

    // 3. Act on the decision
    if status.Status != "approved" {
        return fmt.Errorf("export not approved (status: %s)", status.Status)
    }

    return exportRecords(ctx)
}

Poll on your own schedule when you need custom retry logic or want to do other work between checks:

package main

import (
    "context"
    "fmt"
    "time"

    agentfield "github.com/Agent-Field/agentfield/sdk/go/client"
)

func pollUntilResolved(ctx context.Context, c *agentfield.Client, nodeID, executionID string) (*agentfield.ApprovalStatusResponse, error) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    deadline := time.Now().Add(4 * time.Hour)

    for {
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case t := <-ticker.C:
            if t.After(deadline) {
                return nil, fmt.Errorf("approval deadline exceeded")
            }

            status, err := c.GetApprovalStatus(ctx, nodeID, executionID)
            if err != nil {
                // Log and continue — transient errors are expected
                fmt.Printf("status check failed (will retry): %v\n", err)
                continue
            }

            if status.Status != "pending" {
                return status, nil
            }

            fmt.Printf("Still pending, checked at %s\n", t.Format(time.RFC3339))
        }
    }
}

Distinguish between network errors, timeouts, and non-approved decisions:

package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    agentfield "github.com/Agent-Field/agentfield/sdk/go/client"
)

func handleApproval(ctx context.Context, c *agentfield.Client, nodeID, executionID string) error {
    waitCtx, cancel := context.WithTimeout(ctx, 2*time.Hour)
    defer cancel()

    status, err := c.WaitForApproval(waitCtx, nodeID, executionID, &agentfield.WaitForApprovalOptions{
        PollInterval:  10 * time.Second,
        MaxInterval:   2 * time.Minute,
        BackoffFactor: 1.5,
    })
    if err != nil {
        // Context cancelled or deadline exceeded — the approval is still pending
        if errors.Is(err, context.DeadlineExceeded) {
            return fmt.Errorf("timed out waiting for approval after 2 hours")
        }
        if errors.Is(err, context.Canceled) {
            return fmt.Errorf("approval wait cancelled")
        }
        return fmt.Errorf("unexpected error waiting for approval: %w", err)
    }

    // WaitForApproval only returns once status != "pending"
    switch status.Status {
    case "approved":
        fmt.Printf("Approved at %s\n", status.RespondedAt)
        return nil
    case "rejected":
        reason, _ := status.Response["reason"].(string)
        return fmt.Errorf("approval rejected: %s", reason)
    case "expired":
        return fmt.Errorf("approval request expired without a response")
    default:
        return fmt.Errorf("unexpected approval status: %s", status.Status)
    }
}

Status values

StatusMeaning
pendingWaiting for a human to respond. WaitForApproval keeps polling.
approvedReviewer accepted the request. Safe to proceed.
rejectedReviewer denied the request. Check Response for a reason.
expiredNo response before ExpiresInHours elapsed.