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
| Status | Meaning |
|---|---|
pending | Waiting for a human to respond. WaitForApproval keeps polling. |
approved | Reviewer accepted the request. Safe to proceed. |
rejected | Reviewer denied the request. Check Response for a reason. |
expired | No response before ExpiresInHours elapsed. |
Related
- Human-in-the-Loop - Conceptual overview of approval workflows
- Agent Execution API - REST endpoints backing these methods
- agent.Memory() - Distributed memory for passing state across approval boundaries