Learn A2A - Advanced Task Patterns
Series/Learn A2A/Episode 17
Episode 17 of 23

Learn A2A - Advanced Task Patterns

Learn advanced task patterns for multi-agent production: fan-out and fan-in orchestration, human-in-the-loop workflows with the input-required state and resubmission, and state management like idempotency, retry, long-running tasks, and checkpoint-resume.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 16 we made agents able to find and choose each other through registries and routing. Now the tasks they exchange can become more complex than simple question-and-answer. In production, a single user request often becomes a tree of tasks: split across many agents, waiting for human decisions, then recombined.

The A2A protocol is designed for these patterns. Tasks have a full lifecycle with the submitted, working, input-required, completed, failed, and canceled states. This episode uses that lifecycle to build advanced task patterns: fan-out/fan-in, human-in-the-loop, resubmission, idempotency, and checkpoint-resume.

This episode's roadmap: we start with fan-out and fan-in orchestration, then human-in-the-loop with the input-required state, resubmission, idempotency and retry, and close with long-running tasks along with checkpoint-resume.

Fan-Out and Fan-In

Fan-out means one task is split into many sub-tasks sent to several agents in parallel. Fan-in means collecting all the results and combining them into a single output. This pattern is very common: check prices from three vendors at once, ask for the weather in five cities, or summarize documents from many sources.

An orchestrator implementation with fan-out then fan-in:

Pythonorchestrator.py — fan-out then fan-in
import asyncio
 
async def fan_out_and_fan_in(agent_clients, sub_tasks):
    pending = [
        client.send_task(task) for client, task in zip(agent_clients, sub_tasks)
    ]
    results = await asyncio.gather(*pending, return_exceptions=True)
    ok = [r for r in results if not isinstance(r, Exception)]
    merged = merge_results(ok)
    return await aggregator_client.send_task({"parts": [merged]})

Some practical notes on fan-out/fan-in:

  • Don't wait for all sub-tasks before starting aggregation; consider partial results if some agents are slow.
  • Each sub-task is an independent A2A task with its own taskId — correlation with the parent task happens through trace metadata as in episode 15.
  • If one sub-task fails, decide wisely: fail everything, or continue with partial results and flag a warning.

Human-in-the-Loop with the input-required State

Some decisions must not be made by an agent alone — money transfers, legal document approval, or destructive actions. A2A provides the input-required state for this pattern: the agent stops working and waits for input from a human or an external system.

The flow: a task comes in, the agent works briefly, finds it needs approval, then returns the task in the input-required state with details of what's needed. The client shows the question to a human, then sends a new message/send with the answer; the agent continues the task.

Example response when the agent needs approval:

Task response in the input-required state
{
  "jsonrpc": "2.0",
  "id": "7",
  "result": {
    "id": "task-4821",
    "status": "input-required",
    "artifacts": [
      {
        "parts": [
          { "kind": "text", "text": "Transaksi di atas limit otomatis. Persetujuan dibutuhkan." }
        ]
      }
    ],
    "metadata": {
      "inputModal": { "type": "approval" }
    }
  }
}

Warning

Don't let a task hang in the input-required state forever. Set a timeout: if the human doesn't respond within a certain limit, cancel the task or return it to the queue. This kind of task leak is often the root of a "queue that never finishes".

Resubmission

Sometimes tasks fail due to temporary factors — an LLM timeout, an overloaded server, or a network glitch. Resubmission is the pattern of resending a failed task, hoping the next attempt succeeds. The key is deciding: when can it be retried, and how many times.

Rules of thumb:

  • Retry if the failure is temporary (timeout, 5xx, network). Don't retry if the failure is permanent (invalid input, authorization denied).
  • Limit the number of attempts, for example a maximum of 3, then escalate to a human.
  • Keep the same taskId when retrying, so the server knows this is a retry of the same task.

The resubmission flow in orchestrator code:

Pythonresubmit.py — retry a temporarily failed task
MAX_ATTEMPTS = 3
 
async def send_with_resubmit(client, task):
    for attempt in range(1, MAX_ATTEMPTS + 1):
        result = await client.send_task(task)
        if result.status == "failed" and result.retryable:
            await asyncio.sleep(2 ** attempt)
            continue
        return result
    return escalate_to_human(task)

Idempotency and Retry

Idempotency guarantees that repeating the same action doesn't produce a double effect. Imagine a client times out after sending message/send, then sends it again. If the server processes it twice, the agent could book a double ticket or send a duplicate. The solution: the client includes a consistent task identity, and the server detects and rejects duplicates.

In A2A, every message carries a messageId, and every task has a taskId. The server must store task history and: if a messageId has already been processed, return the same result without running it again.

The safe retry pattern — and for backoff implementation in Python, libraries like tenacity, installed via pip install tenacity, help a lot:

retry.yaml — client retry configuration
retry:
  max_attempts: 5
  backoff:
    type: exponential
    base_ms: 500
    multiplier: 2
  retryable_statuses:
    - failed
    - timeout
  idempotency_key: messageId

Info

Retry and idempotency are an inseparable pair. Without idempotency, retry is dangerous; without retry, idempotency is useless. Always apply both together, and make sure the server stores enough history to detect duplicates within a reasonable window.

Long-Running Tasks and Checkpoint/Resume

A2A tasks can run for a long time — large data analysis, multi-step pipelines, or processes waiting on many sub-tasks. The risk: the server process can restart, the network can drop, and a half-finished task will be lost. This is where checkpoint/resume comes in.

The concept: periodically save a snapshot of the task state to external storage. When the process restarts, the agent loads the last snapshot and continues from there, instead of repeating from the beginning.

The implementation pattern:

Pythoncheckpoint.py — save and restore task state
import json
 
def save_checkpoint(task_id, state, step):
    snapshot = {"task_id": task_id, "state": state, "step": step}
    state_store.put(f"checkpoint:{task_id}", json.dumps(snapshot))
 
def resume_task(task_id):
    raw = state_store.get(f"checkpoint:{task_id}")
    if not raw:
        return None
    snapshot = json.loads(raw)
    return snapshot

Some good checkpoint practices:

  • Store the minimal state needed to continue — full raw data is wasteful.
  • Write the checkpoint before starting a risky step, not after, so rollback is always safe.
  • Combine with long-running tasks: because A2A separates the task from a single HTTP process, the client can close the connection and re-check the task status later via tasks/get.

Success

The A2A task lifecycle is designed for this: a task doesn't depend on a single connection. Clients can poll for status, servers store task state independently, and with correct checkpoints, a server restart doesn't wipe progress. This is the foundation of truly production-grade tasks.

Conclusion

In this episode we assembled advanced task patterns on top of the A2A lifecycle. Fan-out and fan-in split large tasks into parallel work and recombine it, the input-required state brings humans into the workflow, resubmission handles temporary failures, idempotency and retry keep consistency, and checkpoint-resume makes long tasks survive restarts.

Here's the core takeaway:

  • Fan-out/fan-in split parallel tasks and aggregate the results, with tolerance for partial results.
  • The input-required state stops a task to wait for a human decision, complete with a timeout so it doesn't hang.
  • Resubmission is only for temporary failures, with attempt limits and escalation.
  • Idempotency uses messageId and taskId so retries don't cause double effects.
  • Checkpoint-resume stores task state snapshots so process restarts don't lose progress.

With these patterns, your multi-agent orchestration matches production practice. But another question awaits: when hundreds of agents process thousands of tasks, what does it cost and how fast is it?

In the next episode, episode 18, we'll discuss Performance & Scale: handling many remote agents with connection pooling, agent card caching, load balancing, the streaming versus polling tradeoff, and payload minimization for high throughput. See you there!

Learn A2A - Advanced Task Patterns | Learn A2A