A state handoff is the moment one agent stops being responsible for a piece of work and another starts. A triage agent passes a customer case to a billing agent; a research agent passes a half-finished investigation to a writing agent; a failing agent's work moves to a healthy replica in another organisation. Delegation is different: in A2A task delegation the caller keeps ownership and waits for a result. In a handoff, ownership moves, and the receiver needs enough state to continue without the sender.

The Agent2Agent protocol does not define a handoff operation. It gives you messages made of parts, tasks with a lifecycle, contexts and task references. A safe handoff is a convention you build on those primitives: what goes in the payload, what stays behind a reference, what must never cross, and how both sides agree on who owns the work at every instant. This article builds that convention, with code, against the A2A 1.0 specification.

Advertisement

What A2A carries, and what it leaves to you

Everything that crosses an A2A boundary travels in a Message. In 1.0 a message has a required messageId, role and parts, and optional contextId, taskId, referenceTaskIds, metadata and extensions. A part carries exactly one kind of content, text, raw bytes, a url or structured data, with an optional mediaType, filename and metadata. Results come back as artifacts on the task, covered in A2A artifacts.

Three things are missing, and every handoff design has to supply them. There is no shared memory: the receiver knows only what the message says or points to. A contextId is not portable: the specification lets an agent generate one when a message arrives without it, and lets it accept or reject a client-supplied one, so a context id minted by agent A means nothing to agent B. And there is no ownership concept: the protocol tracks task state, not who is responsible for the business object the task is about. The lifecycle states themselves are described in A2A task state.

The shape of a handoff

Agent A (current owner)triage, holds the caseAgent B (new owner)billing specialistHandoff messagedata part: envelope v2url parts: evidence by referenceSendMessageOwnership ledgercase id, owner, epochValidate + acceptschema, policy, epochShared evidence storesigned, expiring URLsrefsfetchTask COMPLETEDartifact: acceptance recordepoch + 1, owner = BA keeps ownership until B's acceptance artifact arrives; only then does the ledger move.Every later write carries the epoch, so a stale owner's writes are rejected.
Agent A sends a handoff message with a versioned envelope as a data part and evidence as URL references. Agent B validates and accepts by completing the task with an acceptance artifact. Only then does the ownership ledger move, with a new epoch.

The pattern has four steps. The sender writes a handoff intent to its own store before sending, so a crash cannot lose it. It sends one message containing a structured envelope and references to bulky evidence. The receiver validates the envelope and either completes the task with an acceptance artifact or rejects it. On acceptance, the ownership ledger moves to the receiver with an incremented epoch, and the sender stops acting on the case.

The order matters. If the sender released ownership when it sent the message, a rejection or a lost message would leave the case with no owner. If the receiver started acting before acceptance was recorded, two agents could act at once. Accept-then-release keeps exactly one owner at every point, at the cost of one extra round trip.

Advertisement

Designing the envelope

The envelope is a JSON document in a data part with a media type you control. A versioned vendor media type such as application/vnd.acme.handoff.v2+json is a local convention, not part of A2A, but it lets the receiver reject versions it does not understand before parsing anything. The fields that earn their place:

  • Identity: a stable case id independent of any A2A id, the handoff id, the sender agent and the epoch being handed over.
  • Goal and status: what the work is for, what is done, and what remains, as short structured fields rather than prose.
  • Facts: established values the receiver may rely on, each with a source and a confidence or verification flag, for example a verified account id versus an amount the customer claimed.
  • Open questions and constraints: what the sender could not resolve, deadlines, and limits such as spending caps.
  • Summary: a short natural-language narrative for the receiving model, clearly labelled as a summary.
  • References: URLs to transcripts, documents and logs, with media type, size and a content hash.
def handoff_message(case, epoch, evidence, related_task_ids):
    envelope = {
        "version": 2,
        "case_id": case.id,
        "handoff_id": f"{case.id}:{epoch + 1}",
        "from_agent": SELF_URL,
        "epoch": epoch,
        "goal": case.goal,
        "done": case.completed_steps,
        "remaining": case.next_steps,
        "facts": [{"key": f.key, "value": f.value, "source": f.source,
                   "verified": f.verified} for f in case.facts],
        "open_questions": case.open_questions,
        "constraints": {"deadline": case.deadline_iso, "refund_cap": case.cap},
        "summary": case.summary[:2000],
    }
    parts = [
        {"text": "Handoff of case " + case.id + ". Accept or reject."},
        {"data": envelope, "mediaType": "application/vnd.acme.handoff.v2+json"},
    ]
    for ev in evidence:   # bulky state goes by reference, never inline
        parts.append({"url": ev.signed_url, "mediaType": ev.media_type,
                      "filename": ev.name,
                      "metadata": {"sha256": ev.sha256, "bytes": ev.size}})
    return {
        "messageId": envelope["handoff_id"],   # derived: a resend is recognisable
        "role": "ROLE_USER",
        "parts": parts,
        "referenceTaskIds": related_task_ids,
    }

Deriving the messageId from the case and epoch means a resend after a crash carries the same id, which a receiver that deduplicates can recognise; see A2A idempotency. referenceTaskIds tells the receiver which earlier tasks it may already know about, which is useful only when those tasks were on the receiving agent.

Value or reference

Every piece of state is either copied into the envelope or left behind a reference. Values are self-contained, survive the sender going away, and can be validated on arrival, but they grow messages and copy sensitive data. References keep messages small and let access be revoked, but they make the receiver depend on the store staying up and the link staying valid.

StateSend asReason
Case id, goal, next steps, constraintsValueSmall, needed immediately, must survive the sender
Verified factsValue with sourceReceiver must not re-derive them
Full conversation transcriptReference with hashLarge, sensitive, rarely needed in full
Documents and attachmentsReference with hashSize; access can expire after acceptance
Model reasoning or scratchpadsNeitherUnverified, often wrong, invites prompt injection
Credentials or tokensNeverReceiver obtains its own scoped authority

Signed URLs should expire, but not before the receiver can plausibly fetch them; a day is a common choice for a handoff that may sit in a queue. The content hash lets the receiver verify it got what the sender meant, and a fetch that fails or mismatches is a reason to reject the handoff rather than to continue with partial state.

Redaction: send what the receiver needs, nothing more

A handoff crosses a trust boundary even inside one company, because the receiver may log, cache or forward what it gets. Apply a field-level allow list per receiving skill: the billing agent gets the account id, invoice ids and the disputed amount, not the customer's support history about unrelated products. Strip personal data that the receiver's purpose does not require, and replace it with references the receiver can resolve only if its own authorisation allows. Authentication between agents is covered in A2A security; the point here is that authentication tells you who is asking, and redaction decides what they get.

Summaries need the same care. A model-written summary can carry personal data, guesses stated as facts, or instructions copied from a user message. Generate summaries from the structured fields, not from the raw transcript, and label them as summaries so the receiver weights them below verified facts.

The receiving side: validate, then accept

The receiver treats every handoff as untrusted input. It checks the media type and version, validates the envelope against a schema, checks that the sender is allowed to hand this kind of case to this skill, fetches and verifies references, and checks the epoch against its own record of the case. Only when all of that passes does it record ownership and return an acceptance artifact.

ACCEPTED_TYPES = {"application/vnd.acme.handoff.v2+json"}

def handle_handoff(msg, peer):
    env_parts = [p for p in msg["parts"]
                 if "data" in p and p.get("mediaType") in ACCEPTED_TYPES]
    if len(env_parts) != 1:
        return reject("expected exactly one supported handoff envelope")
    env = env_parts[0]["data"]
    errors = SCHEMA_V2.validate(env)
    if errors:
        return reject(f"schema: {errors[:3]}")
    if not POLICY.may_hand_off(peer.identity, env["case_id"], skill="billing"):
        return reject("sender not permitted to hand off this case")
    for p in msg["parts"]:
        if "url" in p:
            digest = (p.get("metadata") or {}).get("sha256")
            if digest is None or not fetch_and_verify(p["url"], digest):
                return reject("evidence missing hash, unavailable or mismatched")
    with ledger.transaction() as tx:
        current = tx.get(env["case_id"])
        if current and current.epoch != env["epoch"]:
            return reject(f"stale epoch {env['epoch']}, current {current.epoch}")
        tx.put(env["case_id"], owner=SELF_URL, epoch=env["epoch"] + 1)
    return complete_with_artifact({"accepted": True, "case_id": env["case_id"],
                                   "epoch": env["epoch"] + 1})

The ledger here is whatever system of record both sides trust for case ownership: a shared database, the case-management system, or the sender's store updated on receipt of the acceptance artifact. The epoch is a fencing token. After a handoff every write to the case carries the owner's epoch, and the system of record rejects writes with an older one, so a sender that missed the acceptance and keeps acting is stopped by the data layer rather than by good behaviour.

Worked example: triage to billing

A customer reports a double charge. The triage agent verifies the account, finds two identical card payments 40 seconds apart, and decides a billing specialist must handle the refund. It writes intent to its store at epoch 3, then sends the handoff with facts (account id verified against the auth system, two payment ids from the payments API, a disputed amount the customer stated), a constraint of a 48-hour response target and a refund cap, a summary, and a signed URL to the redacted transcript.

The billing agent validates the envelope, fetches the transcript and checks its hash, confirms epoch 3 matches the case record, and moves the case to itself at epoch 4. It completes the A2A task with an acceptance artifact. Triage receives the completed task, marks its own record as handed off, and from then on answers the customer only with a status pointer. An hour later a retry job in the triage agent, unaware of the handoff because of a stale cache, tries to add a note to the case at epoch 3; the case system rejects it.

Had billing found the second payment id missing from its own system, it would have rejected the task with a reason. Triage, still the owner at epoch 3, would then correct the data or escalate to a person, and the customer would never see a gap in ownership.

Failure modes

  • Release before accept. The sender drops the case when it sends; a rejection or lost message leaves nobody responsible.
  • Two owners. No epoch check, so a sender that missed the acceptance keeps acting in parallel.
  • Context id reuse across agents. Sending A's context id to B and assuming B has the history; B either rejects it or starts an empty context with the same id.
  • Transcript dumping. Inlining the whole conversation as text: large, leaks personal data, and lets instructions in user text steer the receiver.
  • Unverifiable facts. Facts without sources, so the receiver cannot tell a verified account id from a customer's guess.
  • Expired references. URLs that expire before a queued handoff is processed; the receiver acts on partial state instead of rejecting.
  • Version drift. Sender upgrades the envelope and the receiver silently ignores new required fields; version in the media type prevents it.

Trade-offs

Rich envelope versus thin pointer. A thin handoff that sends only a case id and lets the receiver read everything from a shared system of record is simpler and avoids copying, but only works when both agents share that system and its access model. Across organisations, a rich envelope with references is usually the only option.

Synchronous versus queued acceptance. A blocking accept keeps the window of uncertainty short but couples the sender to the receiver's availability. Queued handoffs with push notifications tolerate outages but need deadline handling: if no acceptance arrives in time, the sender keeps the case and tries another agent.

Summary versus full history. Summaries are cheap and focused, and lose detail. Pair a structured summary with a referenced transcript so the receiver can drill in when the summary is not enough.

What to do next

  1. Write down which business objects can change owner between agents, and name the system of record for each one's ownership.
  2. Define a versioned handoff envelope schema with identity, goal, facts with sources, open questions, constraints, summary and references.
  3. Add a per-skill field allow list and redact before building the message.
  4. Implement accept-then-release: persist intent, send, release only on an acceptance artifact.
  5. Add an epoch to the ownership record and reject writes carrying a stale one.
  6. Test the failure paths: rejection, lost acceptance, expired reference, and a stale owner writing after handoff.
Key takeaway: A2A gives you messages, parts and tasks, not handoffs. Build the handoff on top: a versioned envelope as a data part with facts and their sources, bulky evidence by reference with hashes, redaction per receiver, and an accept-then-release protocol fenced by an epoch so exactly one agent owns the work at any moment. Treat every inbound handoff as untrusted until it validates.