When one agent asks another to do something over the Agent2Agent protocol (A2A), what travels is a message. It is the only thing a client can send to start work, add detail to work in progress, or answer a question the remote agent asked. It is also what the remote agent uses to talk back before, during and around a task. If you get the message wrong, everything built on it goes wrong too: a follow-up starts a new task instead of continuing the old one, a retry books a second flight, or a file arrives as an unreadable blob.

This article covers the Message as defined in the A2A 1.0 specification. It goes field by field, then through the unified Part, the identifier rules that attach a message to a conversation and a task, the two send operations, and the errors you will meet. The wider protocol, the task lifecycle and artifact delivery each have their own pages: A2A protocol architecture, A2A task state and A2A artifact exchange. By the end you should be able to build conformant messages and write the checks that reject bad ones.

Advertisement

What a message is, and what it is not

A2A separates three things that ad-hoc agent integrations tend to blur. A Message is one turn of communication, sent either by the client (role ROLE_USER) or by the agent (role ROLE_AGENT). A Task is a stateful unit of work with an id, a status and a lifecycle, created by the server in response to a message. An Artifact is the output a task produces: the report, the image, the booking confirmation.

Section 3.7 of the specification says messages SHOULD NOT deliver task outputs; results SHOULD be artifacts attached to a task. Messages start tasks, ask for clarification, report status and add input to running tasks. They are not a reliable channel: a reconnecting stream can miss status messages, and the agent decides which messages enter task history. Anything a caller must not lose belongs in an artifact.

A message is communication; a task is the unit of work; artifacts are the outputClient agentbuilds Message, role USERRemote agentvalidates, routes, repliesSendMessage { message, configuration }result: Task or MessageMessagemessageId, role, parts[]contextId (conversation) | taskId (existing work)referenceTaskIds | extensions | metadataPartexactly one of:text | raw | url | data+ mediaType, filename, metadataTaskid, contextId, statushistory: Message[]status.message: agent MessageArtifactartifactId, parts[]the deliverable outputnever a Messagecontainsbinds to / creates
The client sends a Message. The agent replies either with a Message, for a quick answer or a clarification, or with a Task whose status can carry agent messages and whose artifacts hold the output.

The Message object, field by field

In A2A 1.0 the data model is defined in Protocol Buffers, and every binding (JSON-RPC, gRPC, HTTP+JSON) must carry an equivalent structure. In JSON the field names are camelCase:

FieldRequiredMeaning and rules
messageIdyesUnique id, for example a UUID, created by whoever creates the message. Agents MAY use it to detect duplicates.
roleyesROLE_USER for client-to-server messages, ROLE_AGENT for server-to-client ones.
partsyesAn ordered list of Part objects: the content. At least one part is expected.
contextIdnoGroups tasks and messages that belong to one conversation.
taskIdnoBinds the message to an existing task. Must reference a task that exists.
referenceTaskIdsnoOther tasks the message refers to for context, such as a previous result.
extensionsnoURIs of extensions that this message uses.
metadatanoFree-form key/value map. Extensions give keys a typed meaning.

Two design choices stand out. The creator mints messageId, so a client can generate it before sending and reuse it on retry. Task ids go the other way: they are always minted by the server, and a client cannot choose the id of a new task. This asymmetry is the basis of safe retries, which comes up again below.

Advertisement

Parts: one union, four kinds of content

A Part holds exactly one piece of content, chosen from four alternatives, plus optional descriptive fields. In the protobuf it is a oneof named content:

  • text: a string, for natural-language instructions or answers.
  • raw: bytes, base64-encoded in JSON, for small inline files.
  • url: a reference to content stored elsewhere, for large files or data the receiver should fetch.
  • data: any JSON value, for structured input or output.

Alongside the content, mediaType names the format, for example image/png or application/json, filename gives a file a name, and metadata holds anything else. A message can mix parts freely. A request to analyse a photo is typically one text part with the instruction and one raw or url part with the image.

If you have used A2A 0.3, notice what changed. Version 0.3 tagged every part with a kind discriminator: {"kind": "text", "text": "..."}, or a file part with a nested file object carrying mimeType and fileWithBytes. Version 1.0 removes kind. The member that is present identifies the part, files are flattened into raw or url with filename and mediaType, and streaming events are wrapped the same way. The specification's appendix allows servers to accept both forms during a transition, but new code should emit only the 1.0 form.

Choose between raw and url by size. Base64 inflates inline bytes by about a third, and they sit in memory, logs and history on every hop. A URL keeps the message small, but the receiver needs access and the URL must outlive the task.

How identifiers bind a message to work

Section 3.4 of the specification defines what contextId and taskId mean. These rules decide whether a message starts something new or continues something old, so they are worth learning exactly:

  • Neither set. The agent treats the message as new. It MAY generate a contextId and, if it does, MUST return it in the Task or Message it responds with.
  • Only contextId set. The message starts a new task within an existing conversation. Clients SHOULD NOT invent context ids unless they know how the server treats them. The safe pattern is to reuse the one the server returned.
  • Only taskId set. The message continues that task. The agent MUST infer the context from the task, and MUST return TaskNotFoundError if the task does not exist.
  • Both set. They must agree. The agent MUST reject a message whose contextId differs from the referenced task's context.
  • Task already finished. A task in TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED or TASK_STATE_REJECTED cannot accept more messages. The agent returns UnsupportedOperationError. To refine a finished result, start a new task in the same context and point at the old one with referenceTaskIds.

In short, contextId is a conversation and taskId is one job inside it.

Sending: SendMessage and SendStreamingMessage

There are two ways to send. In the JSON-RPC binding the methods are SendMessage and SendStreamingMessage. In 0.3 they were message/send and message/stream. The HTTP+JSON binding uses POST /message:send and POST /message:stream. Both take a SendMessageRequest: the message, an optional configuration and optional request-level metadata. The configuration has four fields:

  • acceptedOutputModes: the media types the client can handle in the response. Agents SHOULD tailor output to it.
  • historyLength: how many recent history messages to include in a returned task. Unset means no client limit, and 0 means none.
  • returnImmediately: false by default. A blocking call waits until the task is terminal or interrupted (TASK_STATE_INPUT_REQUIRED, TASK_STATE_AUTH_REQUIRED). With true, the call returns as soon as the task exists.
  • taskPushNotificationConfig: a webhook for updates, used only if the agent card declares push-notification support.

The response is {"task": {...}} or {"message": {...}}; a direct message suits answers that need no tracked state. A streaming call returns a sequence of StreamResponse objects, each holding exactly one of task, message, statusUpdate or artifactUpdate. If the agent creates a task, the stream starts with the Task and ends when the task reaches a terminal state. Streaming requires capabilities.streaming in the agent card; otherwise the agent returns UnsupportedOperationError. The transport details are covered in A2A streaming architecture.

Worked example: a delegation that needs more input

A planner agent delegates a booking to a travel agent. The first message has no identifiers:

{"jsonrpc": "2.0", "id": 1, "method": "SendMessage",
 "params": {
   "message": {
     "messageId": "0b7c6a52-3f0e-4c8e-9a51-2d8f1c4e7a10",
     "role": "ROLE_USER",
     "parts": [
       {"text": "Book one economy seat to New York on 14 October."},
       {"data": {"traveller": "T-1182", "maxFare": 450, "currency": "USD"},
        "mediaType": "application/json"}
     ]
   },
   "configuration": {"acceptedOutputModes": ["application/json", "text/plain"],
                     "historyLength": 0}
 }}

The travel agent creates a task, finds that it does not know the departure city and pauses. Because the call is blocking and TASK_STATE_INPUT_REQUIRED is an interrupted state, it returns now, with the question as an agent message inside the task status:

{"jsonrpc": "2.0", "id": 1, "result": {"task": {
  "id": "task-5d2e", "contextId": "ctx-91af",
  "status": {"state": "TASK_STATE_INPUT_REQUIRED",
             "message": {"messageId": "m-agent-1", "role": "ROLE_AGENT",
                         "parts": [{"text": "Which city are you departing from?"}]}}}}}

The planner answers with a new messageId and both identifiers, so the reply lands in the paused task rather than starting a new one:

{"message": {"messageId": "4e1f9d27-8b3a-4f5c-a6e2-7c0d9b8a3f61",
             "role": "ROLE_USER", "taskId": "task-5d2e", "contextId": "ctx-91af",
             "parts": [{"text": "From San Francisco (SFO)."}]}}

The task completes and the confirmation arrives as an artifact. To change the booking later, the planner sends a new message in ctx-91af with "referenceTaskIds": ["task-5d2e"], because the completed task accepts no more input.

Client code: building and sending messages

This plain-HTTP client sends the A2A-Version header, which 1.0 requires, and keeps one messageId across retries:

import uuid, httpx

A2A_URL = "https://travel.example.com/a2a"   # JSON-RPC endpoint from the agent card
HEADERS = {"A2A-Version": "1.0", "Authorization": "Bearer <token>"}

def make_message(parts, task_id=None, context_id=None, refs=None):
    msg = {"messageId": str(uuid.uuid4()), "role": "ROLE_USER", "parts": parts}
    if task_id:    msg["taskId"] = task_id
    if context_id: msg["contextId"] = context_id
    if refs:       msg["referenceTaskIds"] = refs
    return msg

def send(message, attempts=3):
    body = {"jsonrpc": "2.0", "id": message["messageId"], "method": "SendMessage",
            "params": {"message": message,
                       "configuration": {"acceptedOutputModes": ["application/json"]}}}
    for i in range(attempts):
        try:
            r = httpx.post(A2A_URL, json=body, headers=HEADERS, timeout=60)
            r.raise_for_status()
            out = r.json()
            if "error" in out:
                raise RuntimeError(out["error"])      # protocol error: do not retry blindly
            return out["result"]                      # {"task": ...} or {"message": ...}
        except (httpx.TransportError, httpx.HTTPStatusError):
            if i == attempts - 1:
                raise                                 # same messageId on every attempt

result = send(make_message([{"text": "Book one economy seat to New York on 14 October."}]))

Deduplication by messageId is only a MAY in the specification, so confirm your peer does it. Server-side registries are covered in A2A idempotency architecture.

Server-side validation

An agent should validate every inbound message before any model sees it. The order below rejects cheap errors first and never creates a task for a message it will refuse:

def accept(req, tenant):
    m = req.message
    require(m.message_id and m.role == ROLE_USER and m.parts)           # InvalidParams (-32602)
    if dedup.seen(tenant, m.message_id):
        return dedup.previous_response(tenant, m.message_id)             # optional, MAY
    for part in m.parts:
        if part.media_type and part.media_type not in card.input_modes:
            raise ContentTypeNotSupported                                # -32005
        if part.raw and len(part.raw) > MAX_INLINE_BYTES:
            raise InvalidParams("inline file too large; send a url part")
    if card.required_extensions - set(req.requested_extensions):
        raise ExtensionSupportRequired                                   # -32008
    if m.task_id:
        task = tasks.get(tenant, m.task_id) or raise_(TaskNotFound)      # -32001
        if m.context_id and m.context_id != task.context_id:
            raise InvalidParams("contextId does not match task")
        if task.state in TERMINAL:
            raise UnsupportedOperation                                   # -32004
        return tasks.append_input(task, m)
    ctx = m.context_id or new_context_id()
    return tasks.create(tenant, ctx, m)                                  # server mints task id

Scope every lookup by tenant or caller identity. A task id that leaks into another tenant's message must behave like an unknown id, not like a way into someone else's work.

Extensions and metadata

Metadata is a free-form map, so two peers can give one key different meanings. Extensions fix this: the message lists extension URIs in extensions, the typed data sits in metadata under that URI, and the client requests extensions with the A2A-Extensions header. Use extensions for anything a peer must interpret, and plain metadata only for hints that are safe to ignore.

Failure modes

SymptomCauseFix
Follow-up starts a fresh taskClient dropped taskId or sent only a stale contextIdStore the ids from the last response and send taskId for continuations.
TaskNotFoundError (-32001)Task expired, purged, or belongs to another tenantTreat it as terminal and restart in the same context with referenceTaskIds.
UnsupportedOperationError (-32004)Message sent to a terminal task, or streaming not supportedStart a new task; check the agent card's capabilities.
ContentTypeNotSupportedError (-32005)A part's mediaType is outside the agent's input modesRead the card's input modes and convert before sending.
VersionNotSupportedError (-32009)Unsupported A2A-Version (no header means 0.3)Send the version your client implements; do not silently fall back.
Duplicate side effects after retryNew messageId per attempt, or a peer that does not deduplicateKeep one id per logical send; make side-effecting skills idempotent server-side.

Operational guidance and trade-offs

  • Put results in artifacts, questions in messages. A result in a status message can be lost on reconnect and may never reach history.
  • Bound inline size. Set a limit for raw parts, for example a few hundred kilobytes, and require url parts above it. Use short-lived, scoped URLs and log that a part was redacted, not its bytes.
  • Prefer data parts over JSON in text. A data part with a mediaType can be validated with a schema, whereas JSON inside a text part invites parsing guesses.
  • Treat inbound text as untrusted. A message from a peer agent is input, not instructions from your operator. Keep it in the data position of your prompts and never let it change tool permissions.

What to do next

  1. Read the A2A 1.0 specification's sections 3.4 (multi-turn) and 3.7 (messages and artifacts), and the Message and Part definitions in a2a.proto.
  2. Audit your client: one messageId per logical send, taskId on every continuation, A2A-Version on every request.
  3. Audit your server against the validation order above, including the tenant scope on task lookups and the terminal-state rejection.
  4. Search your code for "kind" and fileWithBytes. Those are 0.3 shapes, so plan their migration.
  5. Move any result currently delivered in a message into an artifact, and add a size cap that forces large files onto url parts.
  6. Replay the flight example against a staging agent, including a duplicate send and a message to a completed task, and confirm you get the errors in the table above.
Key takeaway: An A2A message is one turn of communication: a creator-minted messageId, a role, and a list of parts that each hold exactly one of text, raw bytes, a URL or JSON data. The contextId groups work into a conversation. The server-minted taskId binds a message to one job, and a terminal task accepts no more input, so refinements start a new task that points back with referenceTaskIds. Send with SendMessage or SendStreamingMessage, keep outputs in artifacts, reuse the messageId on retry, and validate identifiers, media types and extensions before any model sees the content.