Quick Guides
Multi-step human approval that survives restarts
Pause execution mid-run for a human decision. State persists in PostgreSQL, so a crashed server resumes exactly where it left off.
Block an agent for a human decision — for hours, days, or across a server restart. The control plane writes the execution state to PostgreSQL before pausing, then a webhook callback resumes the same execution after the human responds.
from agentfield import Agent
app = Agent(node_id="content-pipeline", version="1.0.0")
@app.reasoner()
async def publish_with_review(brief: str) -> dict:
# Step 1: AI drafts
draft = await app.ai(system="You are a senior copywriter.", user=brief)
# Pause #1 — editor reviews the draft (state is now in Postgres)
review = await app.pause(
approval_request_id="draft-review",
approval_request_url="https://cms.example.com/review/draft",
expires_in_hours=24,
)
if review.decision != "approved":
return {"status": review.decision, "feedback": review.feedback}
# Step 2: AI revises using the editor's feedback
final = await app.ai(
system="Revise based on editorial feedback.",
user=f"Draft: {draft}\nFeedback: {review.feedback}",
)
# Pause #2 — publisher signs off before going live
sign_off = await app.pause(
approval_request_id="final-approval",
approval_request_url="https://cms.example.com/review/final",
expires_in_hours=4,
)
if sign_off.decision == "approved":
await app.call("publisher.publish", content=final)
return {"status": sign_off.decision}
app.run()import { Agent, ApprovalClient } from "@agentfield/sdk";
const agent = new Agent({ nodeId: "content-pipeline", version: "1.0.0" });
agent.reasoner("publishWithReview", async (ctx) => {
const { brief } = ctx.input;
const approvals = new ApprovalClient({
baseURL: agent.config.agentFieldUrl!,
nodeId: agent.config.nodeId,
});
// Step 1: AI drafts
const draft = await ctx.ai(brief, { system: "You are a senior copywriter." });
// Pause #1 — editor reviews the draft (state is now in Postgres)
await approvals.requestApproval(ctx.executionId, {
approvalRequestId: "draft-review",
approvalRequestUrl: "https://cms.example.com/review/draft",
expiresInHours: 24,
});
const review = await approvals.waitForApproval(ctx.executionId, { pollIntervalMs: 5000 });
if (review.status !== "approved") {
return { status: review.status, response: review.response };
}
// Step 2: AI revises using the editor's feedback
const final = await ctx.ai(`Draft: ${draft}\nFeedback: ${JSON.stringify(review.response)}`, {
system: "Revise based on editorial feedback.",
});
// Pause #2 — publisher signs off before going live
await approvals.requestApproval(ctx.executionId, {
approvalRequestId: "final-approval",
approvalRequestUrl: "https://cms.example.com/review/final",
expiresInHours: 4,
});
const signOff = await approvals.waitForApproval(ctx.executionId, { pollIntervalMs: 5000 });
if (signOff.status === "approved") {
await ctx.call("publisher.publish", { content: final });
}
return { status: signOff.status };
});
agent.serve();package main
import (
"context"
"fmt"
"log"
"time"
"github.com/Agent-Field/agentfield/sdk/go/agent"
"github.com/Agent-Field/agentfield/sdk/go/ai"
"github.com/Agent-Field/agentfield/sdk/go/client"
)
func main() {
a, err := agent.New(agent.Config{
NodeID: "content-pipeline",
Version: "1.0.0",
AgentFieldURL: "http://localhost:8080",
})
if err != nil {
log.Fatal(err)
}
a.RegisterReasoner("publish_with_review", func(ctx context.Context, input map[string]any) (any, error) {
execCtx := agent.ExecutionContextFrom(ctx)
c, err := client.New("http://localhost:8080")
if err != nil {
return nil, err
}
// Step 1: AI drafts
draft, err := a.AI(ctx, fmt.Sprintf("%v", input["brief"]),
ai.WithSystem("You are a senior copywriter."))
if err != nil {
return nil, err
}
// Pause #1 — editor reviews the draft (state is now in Postgres)
_, err = c.RequestApproval(ctx, "content-pipeline", execCtx.ExecutionID,
client.RequestApprovalRequest{
ApprovalRequestID: "draft-review",
ApprovalRequestURL: "https://cms.example.com/review/draft",
ExpiresInHours: 24,
})
if err != nil {
return nil, err
}
review, err := c.WaitForApproval(ctx, "content-pipeline", execCtx.ExecutionID,
&client.WaitForApprovalOptions{PollInterval: 5 * time.Second})
if err != nil {
return nil, err
}
if review.Status != "approved" {
return map[string]any{"status": review.Status, "response": review.Response}, nil
}
// Step 2: AI revises using the editor's feedback
final, err := a.AI(ctx, fmt.Sprintf("Draft: %s\nFeedback: %v", draft.Text(), review.Response),
ai.WithSystem("Revise based on editorial feedback."))
if err != nil {
return nil, err
}
// Pause #2 — publisher signs off before going live
_, err = c.RequestApproval(ctx, "content-pipeline", execCtx.ExecutionID,
client.RequestApprovalRequest{
ApprovalRequestID: "final-approval",
ApprovalRequestURL: "https://cms.example.com/review/final",
ExpiresInHours: 4,
})
if err != nil {
return nil, err
}
signOff, err := c.WaitForApproval(ctx, "content-pipeline", execCtx.ExecutionID,
&client.WaitForApprovalOptions{PollInterval: 5 * time.Second})
if err != nil {
return nil, err
}
if signOff.Status == "approved" {
_, _ = a.Call(ctx, "publisher.publish", map[string]any{"content": final.Text()})
}
return map[string]any{"status": signOff.Status}, nil
})
log.Fatal(a.Run(context.Background()))
}If the agent process restarts mid-pause, reconnect to the pending approval:
result = await app.wait_for_resume(
approval_request_id="draft-review",
timeout=24 * 3600,
)What this gives you
- Multiple human gates inside a single execution.
- Crash-safe: the execution state lives in PostgreSQL, not in agent memory.
- Resolutions arrive via HMAC-signed webhook — your reviewer UI just POSTs the decision.
Next