Strands Graph Approval Agent
Two-node Strands Graph demonstrating MultiAgent persistence and interrupt/resume approval.
Difficulty: Advanced
What You'll Learn
- Building a two-node
GraphwithGraphBuilder - MultiAgent (Graph/Swarm) checkpointing via
serialize_state()/deserialize_state()— restored automatically across a fresh guest process - Interrupt/resume for a tool call nested inside a graph node, using the same
event.interrupt()contract as a plainAgent - LiteLLM-backed model configuration — AgentVisor's proven production route to Bedrock
- Credential brokering for
ANTHROPIC_API_KEY(never exposed in the sandbox)
Setup
agentvisor template create strands/graph-approval-agent
cd graph-approval-agent
export ANTHROPIC_API_KEY=your-api-key-here
temporal server start-dev &
agentvisor serve . --sandbox=none
Test It
THREAD=$(curl -sX POST http://localhost:8090/threads | jq -r '.thread_id')
# Submit a topic — the graph drafts, then pauses for approval before publishing
curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=60s" \
-H "Content-Type: application/json" \
-d '{"input": {"message": "our new dark mode feature"}}' \
| tee /tmp/run1.json | jq '.output'
# Approve, resuming on the same thread
INTERRUPT_ID=$(jq -r '.output.interrupts[0].interruptId' /tmp/run1.json)
curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=60s" \
-H "Content-Type: application/json" \
-d "{\"input\": {\"resume\": [{\"interruptResponse\": {\"interruptId\": \"$INTERRUPT_ID\", \"response\": \"approved\"}}]}}" \
| jq '.output'
How It Works
- Graph:
drafter(anAgent) writes a short announcement from the user's topic;GraphBuilderwires its output as input topublisher(a secondAgent) - Selection:
mav-agent-config.yamlexplicitly setsframework.provider: "strands"— Strands has no discovery file for AgentVisor to auto-detect - Execution:
python3 -m agentvisor.strands.runnerresolvesroot_agent— the sameAgent/Graph/Swarm-agnostic resolution used by every Strands example - Interrupt:
publisher'sApprovalHookcallsevent.interrupt()from aBeforeToolCallEventcallback beforepublish_announcementruns, pausing the whole graph - State persistence: the graph's
serialize_state()— includingdrafter's completed output andpublisher's pending interrupt — is saved viacheckpoint.save()after the run and restored viacheckpoint.load("")/deserialize_state()on the resuming run, in a fresh guest process - Resume: the caller submits a
resumepayload carrying Strands' ownInterruptResponseContentshape;publishercontinues from exactly where it paused
MultiAgent Checkpointing
| Aspect | Detail |
|---|---|
| Mechanism | Graph.serialize_state() / Graph.deserialize_state() (also implemented by Swarm) |
| Storage | agentvisor.checkpoint.save()/load(), Temporal-backed, per thread — the same simple API a plain Agent uses |
| Scope | Not cross-turn conversation memory the way a plain Agent's snapshot is — a Graph/Swarm that has already completed resets every node's message history on the next restore. What persists is in-flight task and interrupt/resume durability, exactly what this example exercises |
| Dispatch | The runner detects whether root_agent exposes take_snapshot/load_snapshot (plain Agent) or serialize_state/deserialize_state (Graph/Swarm) and picks the matching strategy automatically |
| Compatibility | Not cross-compatible with the LangGraph AgentVisorCheckpointer — see Checkpointing |
See the Strands Agents Guide for the full picture, including known limitations (cumulative execution limits across runs, no cross-turn memory after a completed graph run) and what to use instead if your agent needs cross-turn memory.
Project Files
strands/graph-approval-agent/
├── agent.py # Graph: drafter -> publisher, with an approval-gated tool
├── mav-agent-config.yaml # REQUIRED: selects the strands framework provider
├── requirements.txt # Python dependencies
├── agentvisor.yaml # AgentVisor config (proxy credential substitution)
└── policies/
└── domain.yml # Authorization policies
agent.py (key section)
from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent, HookProvider, HookRegistry
from strands.multiagent.graph import GraphBuilder
from agentvisor.strands import register_agent
@tool
def publish_announcement(text: str) -> str:
return f"Published: {text}"
class ApprovalHook(HookProvider):
def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
registry.add_callback(BeforeToolCallEvent, self.approve)
def approve(self, event: BeforeToolCallEvent) -> None:
if event.tool_use["name"] != "publish_announcement":
return
approval = event.interrupt("publish_approval", reason="...")
if approval != "approved":
event.cancel_tool = "approval was not granted"
drafter = Agent(name="drafter", model=..., system_prompt="...")
publisher = Agent(name="publisher", model=..., tools=[publish_announcement], hooks=[ApprovalHook()])
builder = GraphBuilder()
builder.add_node(drafter, "drafter")
builder.add_node(publisher, "publisher")
builder.add_edge("drafter", "publisher")
root_agent = builder.build()
register_agent(root_agent)
Next Steps
- Strands Agents Guide — Full integration details, including MultiAgent checkpointing and interrupt/resume
- Research Agent — Single-agent snapshot checkpointing with MCP tools
- LangGraph Task Agent — Human-in-the-loop with interrupt/resume, LangGraph equivalent
- LangGraph Coordinator Agent — Multi-agent orchestration with sub-agents, LangGraph equivalent