Skip to content
⚠️ Before You Start
Risk: Medium
Duration: 1-2 days for the first agent
Last Validated: Not yet validated (draft)
Kind: How-to
Risk Note: Medium (missing stop rules cause loops and runaway spend)

Build a task agent from the template with state, retry and termination

Draft outline: not validated

Drafted with AI from official sources on 16 Oct 2026; updated on 22 Oct 2026 for task AGT-01-T05. Do not use on a live system until it is written, dry-run by someone other than the author and signed off (planned 2026-12-08). Author: Sanjay Sarvesh · dry-run: Yasar · sign-off: James. Work items: AGT-01-T05, AGT-01-T06, AGT-01-T07, AGT-01-T08.

Template scope

This playbook builds the task agent template only. The task agent is one of four templates in the library (task, planner, supervisor, tool). Use PB-AGT-01-01 to choose the right template and runtime before you start here.

1. Summary

This how-to shows how to build a production-grade single-task agent using the standardised LangGraph task agent template. It implements a typed state schema with explicit channel reducers, transient failure resilience using node-level RetryPolicy, and two-tier termination consisting of an in-state soft step budget and a hard recursion_limit. Applying this template ensures agents execute reliably, recover from transient errors, and terminate gracefully without runaway loops or uncontrolled API token spend.

Risk: Medium (missing stop rules cause loops and runaway spend). Typical duration: 1-2 days for the first agent.

Facts checked against LangChain and Microsoft documentation on 6 November 2026.

2. When to use and when not to

Use it when:

  • Creating a new task-oriented agent or standardising an existing ad hoc agent script to enterprise production standards.
  • Building an agent that performs iterative, multi-step tasks (such as document extraction, multi-source research, data transformation, or automated remediation).
  • Integrating external model calls or APIs that require automatic retries on transient network and rate-limit errors without duplicating side effects.
  • Persisting state across execution turns or worker restarts using a durable database checkpointer.

Do not use it when:

  • The task is a single-turn prompt-response or simple deterministic pipeline without branching or retry requirements; use a fixed workflow instead; choose it with PB-AGT-03-01.
  • You are designing multi-agent topologies (such as supervisor-worker or hierarchical peer teams); use PB-AGT-06-01.
  • You are evaluating framework choices between LangGraph, Microsoft Agent Framework, or Microsoft Foundry Agent Service: use PB-AGT-01-01.
  • The agent executes sensitive or irreversible actions (such as financial transactions or data deletion) requiring human sign-off: use PB-AGT-05-01.
  • Diagnosing or stopping an agent actively stuck in a runaway loop in production: follow the emergency runbook PB-AGT-09-01.
  • Running pre-production technical and operational sign-off: use the checklist in PB-AGT-01-05.

3. What you must know first

  • State schema and reducers: LangGraph channels use typed definitions (such as Python TypedDict or Pydantic); fields annotated with Annotated[T, reducer_fn] (e.g. operator.add) determine how node updates are merged rather than overwritten (LangGraph: Use the graph API).
  • Node idempotency and RetryPolicy: RetryPolicy handles transient exceptions (such as network drops or HTTP 5xx) with configurable max_attempts and exponential backoff; nodes with external side effects must use idempotency keys to prevent duplicate execution during retries (LangGraph: Use the graph API).
  • Two-tier termination boundaries: Always separate the soft termination budget from the hard safety stop. An in-state step budget (e.g., max_steps = 8) allows the agent router to exit gracefully to END with a user-friendly explanation, whereas recursion_limit caps the number of super-steps and raises GraphRecursionError when hit (LangGraph: GRAPH_RECURSION_LIMIT error). The default changes between versions (10,007 super-steps from 1.1.4), so always set recursion_limit explicitly in the run config and never rely on the default (LangGraph: Graph API overview).
  • Durable thread checkpoints: LangGraph scopes conversation and agent memory by thread_id in {"configurable": {"thread_id": "..."}}. For production multi-process environments, initialise tables once via checkpointer.setup() (LangGraph: Checkpointers). Use InMemorySaver only in local development; it loses all state on process restart (LangGraph: Persistence).
  • State trimming: Avoid unbounded list growth in state channels with append reducers; prune or summarise conversation history in a dedicated node to prevent model context window exhaustion.
  • Human approval needs a durable checkpointer. If this agent will later need a human to approve an action, the interrupt pattern requires a persistent checkpointer. PostgresSaver is the Orion default for LangGraph (see PB-AGT-01-01). See PB-AGT-05-01 and PB-AGT-04-02. (LangGraph: Interrupts)
  • Foundry hosted agent context. This template is designed to run as a Foundry hosted agent (the default) or on Azure Container Apps/AKS. The Foundry Agent Service deletes hosted sessions after 30 days of inactivity; keep durable state in the checkpointer, not in the session. Check current limits before deployment. (Hosted agents in Foundry Agent Service)

4. Prerequisites and access

  • Python 3.11 or later, with langgraph, langgraph-checkpoint-postgres and psycopg[binary,pool] installed from the template lock file.
  • Sandbox Azure Database for PostgreSQL Flexible Server for the checkpointer (Amazon RDS or local PostgreSQL on the AWS/OSS path).
  • Model deployment in a sandbox Foundry project (Amazon Bedrock on the AWS path), with managed identity or API key credentials.
  • Task-agent template repository cloned.
  • Decision record from PB-AGT-01-01 with template: task-agent, chosen: langgraph and hosting set.

5. Steps

Follow these steps to build and verify the task agent:

  1. Define typed state schema with explicit reducers and step budgets. Copy the task-agent template and rename the state schema fields for your domain. Annotate cumulative history fields with operator.add, but keep status flags, step counters, and outputs as overwrite channels. Check: Verify that updating the state updates scalar fields and appends to annotated list fields as expected.

  2. Categorise nodes and isolate side effects with idempotency keys. Separate read-only reasoning nodes from nodes that execute external side effects (such as sending emails, writing database rows, or charging cards). !!! warning "Never retry non-idempotent side effects" Applying a RetryPolicy to an external API call without an idempotency key can cause duplicate payments, multiple tickets, or duplicated emails if network acknowledgments drop. Always supply a deterministic idempotency key ${THREAD_ID}:${STEP_NUMBER} to external endpoints. Check: Inspect node functions and verify all mutating calls accept an idempotency key derived from the thread configuration.

  3. Attach RetryPolicy to transient network and model nodes. Apply RetryPolicy() with the documented defaults (max_attempts=3, backoff_factor=2.0), or a value the team records as an Orion default, to nodes that invoke LLMs or external read-only APIs. Do not attach retries to internal deterministic validation nodes. Set retry_on explicitly to the transient exception types your model or tool client raises; the default excludes OSError and its subclasses (such as built-in ConnectionError or TimeoutError). Check: With retry_on set to include ConnectionError, simulate a mock ConnectionError in a unit test and confirm the node retries up to max_attempts before raising the exception.

  4. Implement two-tier termination routing (soft budgets vs hard limit). Track step_count and tokens_used in state against max_steps and max_tokens. The routing function returns END when done is true and routes to a finalise node when either budget is reached. The finalise node writes status and a clear final message to state, then goes to END. Do not set state inside the routing function: LangGraph only applies updates returned by nodes (or a Command), so values set in a router are lost. Check: Invoke the graph with an unsolvable goal and confirm that, for both the step budget and the token budget, the run reaches END with status: budget_exceeded and the final message persisted, without raising GraphRecursionError.

  5. Configure execution parameters and set recursion_limit. Set the runtime recursion_limit in the configuration dictionary above the soft step budget (for example, max_steps = 8, recursion_limit = 25) to provide a hard circuit breaker against runaway execution. Check: Confirm the graph executes normally when within budget and halts if recursion_limit is intentionally set lower than the steps required.

  6. Initialise the persistent checkpointer schema. Open the checkpointer with with PostgresSaver.from_conn_string("${POSTGRES_CONN_STRING}") as checkpointer: and call checkpointer.setup() once in the sandbox (and in the deployment migration job for later environments). Check: Connect to PostgreSQL with psql and confirm tables checkpoints, checkpoint_blobs, and checkpoint_writes exist.

  7. Compile the graph with checkpointer and test execution. Compile the StateGraph with the initialised checkpointer and run an end-to-end task using a unique thread_id. Check: Inspect state snapshot using graph.get_state(config) and verify state fields and execution steps are persisted.

  8. Execute test cases for retry recovery, budget termination, and crash recovery. Run unit tests covering standard completion, transient failure recovery, soft-budget termination, and process restart recovery. Check: Confirm all test cases pass and emit structured trace logs with matching thread_id.

  9. Check the stopped-run trace. Open the trace for the budget-stop test run from step 8. Check: The last span shows status: budget_exceeded and the final message text.

Draft snippet: LangGraph task agent with RetryPolicy and two-tier termination

import operator
from typing import Annotated, Literal, TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import RetryPolicy


# 1. State schema with explicit reducer and step budget
class TaskState(TypedDict):
    task: str
    steps: Annotated[list[str], operator.add]
    step_count: int
    max_steps: int
    done: bool
    status: str
    final_output: str


# 2. Worker node with transient error retry
def execute_step(state: TaskState) -> dict:
    current_count = state.get("step_count", 0) + 1
    task_desc = state["task"]

    # Simulated worker logic: marks done when step 3 is reached
    is_done = current_count >= 3
    output_message = f"Completed step {current_count} for '{task_desc}'"

    return {
        "steps": [output_message],
        "step_count": current_count,
        "done": is_done,
        "final_output": "Task successfully completed." if is_done else "",
    }


# 3. Router implementing soft termination budget
def route_next(state: TaskState) -> Literal["execute_step", "__end__"]:
    if state["done"]:
        return END

    # Soft budget check: terminates gracefully before hitting recursion_limit
    if state["step_count"] >= state["max_steps"]:
        state["status"] = "budget_exceeded"
        state["final_output"] = (
            f"Terminated gracefully after reaching maximum step budget of {state['max_steps']}."
        )
        return END

    return "execute_step"


# 4. Graph assembly
builder = StateGraph(TaskState)

# Node configured with exponential backoff retry policy
builder.add_node(
    "execute_step",
    execute_step,
    retry_policy=RetryPolicy(
        max_attempts=3,
        initial_interval=0.5,
        backoff_factor=2.0,
        jitter=True,
    ),
)

builder.add_edge(START, "execute_step")
builder.add_conditional_edges("execute_step", route_next, ["execute_step", END])

# 5. Compilation with checkpointer
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

# 6. Invocation with hard recursion_limit safety net
config = {
    "configurable": {"thread_id": "task-thread-42"},
    "recursion_limit": 25,  # Hard ceiling well above soft max_steps (8)
}

initial_state: TaskState = {
    "task": "Extract customer sentiment from feedback logs",
    "steps": [],
    "step_count": 0,
    "max_steps": 8,
    "done": False,
    "status": "in_progress",
    "final_output": "",
}

result = graph.invoke(initial_state, config)
print("Final status:", result.get("status"))
print("Step count:", result.get("step_count"))
print("Final output:", result.get("final_output"))

Snippets to add:

  • snippets/templates/task_agent/graph.py: Complete task agent template with PostgreSQL checkpointer and Pydantic validation (python)
  • snippets/tests/test_task_agent.py: Pytest suite exercising standard completion, transient retry, and budget stop (python)
  • snippets/config/agent.yaml: Configuration file for timeout, step budgets, and retry limits (yaml)

6. Verify it works

To verify the task agent template implementation, execute the test suite in the sandbox:

  1. Happy Path Test: Invoke the graph with max_steps: 8. Confirm the agent iterates until done == True, completes in 3 steps, and sets final_output cleanly.
  2. Transient Failure Retry Test: Simulate a transient error that the node's retry_on matches (for example ConnectionError) on attempt 1 in execute_step. Confirm RetryPolicy catches the error, retries automatically, and succeeds on attempt 2 without failing the thread.
  3. Soft Budget Termination Test: Set max_steps: 2 with an objective requiring 5 steps. Confirm execution routes to END cleanly, marks status: budget_exceeded, and does not throw GraphRecursionError.
  4. Hard Recursion Safety Test: Set recursion_limit: 3 and max_steps: 10. Confirm that if a soft stop condition is omitted, LangGraph halts execution and raises GraphRecursionError at step 3.
  5. Checkpoint Persistence Test: Invoke the graph with a thread ID. Query graph.get_state(config) and confirm that the persisted state contains the full history of executed steps and counters.

7. Known issues and fixes

Symptom Likely cause Fix
GraphRecursionError: Recursion limit of 25 reached Execution cycle without a reachable stop condition Add an explicit soft step budget check (step_count >= max_steps) in conditional routing edges and test it. Raise the limit only for genuinely long graphs.
Duplicate side effects after transient error recovery Mutating action executed inside a retried node without idempotency Move side effects to dedicated downstream nodes and supply idempotency key ${THREAD_ID}:${STEP_NUMBER}
State grows continuously and model calls become slow Channels with operator.add accumulate conversation history without pruning Implement a dedicated pruning or summarisation node to trim state history
ProgrammingError or missing tables with PostgreSQL checkpointer checkpointer.setup() was not run before live graph invocations Execute checkpointer.setup() during application deployment or migration setup
Transient connection errors or timeouts are not retried, or a custom retry_on retries client errors default_retry_on excludes OSError and its subclasses (including built-in ConnectionError and TimeoutError); an over-broad custom retry_on can retry 4xx or validation errors Set retry_on explicitly to the transient exception types your client raises; keep 4xx and validation errors non-retryable

8. Monitoring and alerts

Thresholds are Orion team defaults, not platform recommendations. Tune them against a measured baseline.

Signal Alert when Action
GraphRecursionError exceptions in application logs Greater than 0 in any window Trigger emergency triage via PB-AGT-09-01 and inspect edge termination logic
Node retry rate per thread Greater than 3x the 7-day median Check upstream LLM endpoint availability, network stability, rate-limit quotas, and the node's retry_on settings
Runs terminating due to soft budget exhaustion (budget_exceeded) Above the baseline rate measured in the sandbox dry-run (threshold TO VERIFY before go-live) Review prompt complexity, increase step budget if justified, or refine planner decomposition
Checkpoint write latency Above the p95 baseline measured in the sandbox (threshold TO VERIFY) Check PostgreSQL database CPU, connection pool sizing, and table index health
Production agents not built from the task-agent template Any new one in the monthly inventory Agent lead reviews and either adopts the template or records an exception in the decision record

9. AWS and open-source equivalents

  • AWS: The same LangGraph code runs on AgentCore Runtime or any container host. Amazon Bedrock AgentCore Runtime is a serverless runtime for deploying and scaling agents; it fills the same role as Foundry hosted agents. (What is Amazon Bedrock AgentCore?)
  • OpenAI Agents SDK: Run-level turn limit (max_turns, raises MaxTurnsExceeded), guardrails and tool error handling; check current names in its docs. Foundry hosted agents and AgentCore both run agents built with it. (OpenAI Agents SDK: Running agents, OpenAI Agents SDK: Guardrails)
  • Amazon Bedrock Agents Classic: New sign-ups are closed. Do not use it for new work.

10. Validation record

Pending. Filled in by the dry-runner (see docs/validation-protocol.md).

Sandbox scenario: A second engineer builds a new task agent from the playbook in a sandbox, forces a tool failure and a looping condition, and passes when the retry recovers, the budget stops the loop before recursion_limit and all tests are green.

Pass criteria: The retry recovers, the budget stops the loop before recursion_limit and all tests are green.

Record

Item Detail
Playbook PB-AGT-01-02 at commit [hash]
Dry-run by [Name] (must not be author or reviewer)
Date YYYY-MM-DD
Sandbox [Resource group or tag, tier and region]
Scenario [Scenario followed]
Time taken [Hands-on time] hands-on, [waiting time] waiting
Result [PASS / PASS WITH NOTES / FAIL]
Notes 1. [Step number: defect or explanation; blocking/non-blocking; fixed/logged]
Signed off by [Reviewer name and date]

11. Sources

Checked on 6 November 2026.