Skip to main content
This document defines the canonical structure for AdCP responses transmitted over the A2A protocol.

Required Structure

Final Responses (status: “completed”)

AdCP responses over A2A MUST:
  • Include at least one DataPart (kind: ‘data’) containing the task response payload
  • Use single artifact with multiple parts (not multiple artifacts)
  • Use the last DataPart as authoritative when multiple data parts exist
  • NOT wrap AdCP payloads in custom framework objects (no { response: {...} } wrappers)
Recommended pattern:
{
  "status": "completed",
  "taskId": "task_123",
  "contextId": "ctx_456",
  "artifacts": [{
    "name": "task_result",
    "parts": [
      {
        "kind": "text",
        "text": "Found 12 video products perfect for pet food campaigns"
      },
      {
        "kind": "data",
        "data": {
          "products": [...],
          "total": 12
        }
      }
    ]
  }]
}
  • TextPart (kind: ‘text’): Human-readable summary - recommended but optional
  • DataPart (kind: ‘data’): Structured AdCP response payload - required
  • FilePart (kind: ‘file’): Optional file references (previews, reports)
Multiple artifacts: Only for fundamentally distinct deliverables (e.g., creative asset + separate trafficking report). Rare in AdCP - prefer single artifact with multiple parts.

Interim Responses (working, submitted, input-required)

Interim status responses can include optional AdCP-structured data for progress tracking.
{
  "status": "working",
  "taskId": "task_123",
  "contextId": "ctx_456",
  "artifacts": [{
    "parts": [
      {
        "kind": "text",
        "text": "Processing your request. Analyzing 50,000 inventory records..."
      },
      {
        "kind": "data",
        "data": {
          "percentage": 45,
          "current_step": "analyzing_inventory"
        }
      }
    ]
  }]
}
Interim response characteristics:
  • TextPart is recommended for human-readable status
  • DataPart is optional but follows AdCP schemas when provided
  • Interim status schemas (*-async-response-working.json, *-async-response-input-required.json, etc.) are work-in-progress and may evolve
  • Implementors may choose to handle interim data more loosely given schema evolution
When final status is reached (status: "completed" or status: "failed"), DataPart with full AdCP task response becomes required in .artifacts.

Framework Wrappers (NOT PERMITTED)

CRITICAL: DataPart content MUST be the direct AdCP response payload, not wrapped in framework-specific objects.
// ❌ WRONG - Wrapped in custom object
{
  "kind": "data",
  "data": {
    "response": {           // ← Framework wrapper
      "products": [...]
    }
  }
}

// ✅ CORRECT - Direct AdCP payload
{
  "kind": "data",
  "data": {
    "products": [...]       // ← Direct schema-compliant response
  }
}
Why this matters:
  • Breaks schema validation (clients expect products at root, not response.products)
  • Adds unnecessary nesting layer
  • Violates protocol-agnostic design (wrapper is framework-specific)
  • Complicates client extraction code
If your implementation adds wrappers, this is a bug that should be fixed in the framework layer, not worked around in client code.

Canonical Client Behavior

This section defines EXACTLY how clients MUST extract AdCP responses from A2A protocol responses.

Quick Reference

StatusWebhook TypeData LocationSchema Required?Returns
workingTaskStatusUpdateEventstatus.message.parts[]✅ Yes (if present){ status, taskId, message, data? }
submittedTaskStatusUpdateEventstatus.message.parts[]✅ Yes (if present){ status, taskId, message, data? }
input-requiredTaskStatusUpdateEventstatus.message.parts[]✅ Yes (if present){ status, taskId, message, data? }
completedTask.artifacts[] only✅ Required{ status, taskId, message, data }
failedTask.artifacts[] only✅ Required{ status, taskId, message, data }
Key Insights:
  • Final statuses use Task object with data in .artifacts
  • Interim statuses use TaskStatusUpdateEvent with optional data in status.message.parts[]
  • All statuses use AdCP schemas when data is present
  • Interim status schemas are work-in-progress and may evolve

Rule 1: Status-Based Handling

Clients MUST branch on status field to determine the correct data extraction location:
function handleA2aResponse(response) {
  const status = response.status;

  // INTERIM STATUSES - Extract from status.message.parts (TaskStatusUpdateEvent)
  if (['working', 'submitted', 'input-required'].includes(status)) {
    return {
      status: status,
      taskId: response.taskId,
      contextId: response.contextId,
      message: extractTextPartFromMessage(response),
      data: extractDataPartFromMessage(response),  // Optional AdCP data
    };
  }

  // FINAL STATUSES - Extract from .artifacts (Task object)
  if (['completed', 'failed'].includes(status)) {
    return {
      status: status,
      taskId: response.taskId,
      contextId: response.contextId,
      message: extractTextPartFromArtifacts(response),
      data: extractDataPartFromArtifacts(response),  // Required AdCP payload
    };
  }

  throw new Error(`Unknown A2A status: ${status}`);
}
Critical:
  • Interim statuses use TaskStatusUpdateEvent → extract from status.message.parts[]
  • Final statuses use Task object → extract from .artifacts[0].parts[]

Rule 2: Data Extraction Helpers

Extract data from the appropriate location based on webhook type:
// For FINAL statuses (Task object) - extract from .artifacts
function extractDataPartFromArtifacts(response) {
  const dataParts = response.artifacts?.[0]?.parts
    ?.filter(p => p.kind === 'data') || [];

  if (dataParts.length === 0) {
    throw new Error('Final response (completed/failed) missing required DataPart in artifacts');
  }

  // Use LAST data part as authoritative
  const lastDataPart = dataParts[dataParts.length - 1];
  const payload = lastDataPart.data;

  // CRITICAL: Payload MUST be direct AdCP response
  if (payload.response !== undefined && typeof payload.response === 'object') {
    throw new Error(
      'Invalid response format: DataPart contains wrapper object. ' +
      'Expected direct AdCP payload (e.g., {products: [...]}) ' +
      'but received {response: {products: [...]}}. ' +
      'This is a server-side bug that must be fixed.'
    );
  }

  return payload;
}

function extractTextPartFromArtifacts(response) {
  const textPart = response.artifacts?.[0]?.parts?.find(p => p.kind === 'text');
  return textPart?.text || null;
}

// For INTERIM statuses (TaskStatusUpdateEvent) - extract from status.message.parts
function extractDataPartFromMessage(response) {
  const dataPart = response.status?.message?.parts?.find(p => p.data);
  return dataPart?.data || null;
}

function extractTextPartFromMessage(response) {
  const textPart = response.status?.message?.parts?.find(p => p.text);
  return textPart?.text || null;
}

Rule 3: Schema Validation

All AdCP responses use schemas, but validation approach varies by status:
function validateResponse(response, taskName) {
  const status = response.status;
  let data, schemaName;

  // Extract data and determine schema based on status
  if (['working', 'submitted', 'input-required'].includes(status)) {
    // INTERIM: Optional data from status.message.parts
    data = extractDataPartFromMessage(response);

    if (data) {
      // Interim status has its own schema (work-in-progress)
      schemaName = `${taskName}-async-response-${status}.json`;

      // Optional: Implementors may skip interim validation as schemas evolve
      if (STRICT_VALIDATION_MODE) {
        validateAgainstSchema(data, loadSchema(schemaName));
      }
    }
  } else if (['completed', 'failed'].includes(status)) {
    // FINAL: Required data from .artifacts
    data = extractDataPartFromArtifacts(response);
    schemaName = `${taskName}-response.json`;

    // Required: Final responses must validate
    if (!validateAgainstSchema(data, loadSchema(schemaName))) {
      throw new Error(
        `Response payload does not match ${taskName} schema. ` +
        `Ensure DataPart contains direct AdCP response structure.`
      );
    }
  }
}
Schema Evolution Note: Interim status schemas (*-async-response-working.json, etc.) are work-in-progress. Implementors may choose to handle these more loosely while schemas stabilize.

Complete Example

Putting it all together with proper handling of both Task and TaskStatusUpdateEvent payloads:
async function executeTask(taskName, params) {
  const response = await a2aClient.send({
    task: taskName,
    params: params
  });

  // 1. Status-based handling (extracts from correct location)
  const result = handleA2aResponse(response);

  // 2. Schema validation
  validateResponse(response, taskName);

  return result;
}

// Usage
const result = await executeTask('get_products', {
  brief: 'CTV inventory in California'
});

// Handle different response types
if (result.status === 'working') {
  // TaskStatusUpdateEvent - data from status.message.parts
  console.log('Processing:', result.message);
  if (result.data) {
    console.log('Progress:', result.data.percentage + '%');
  }
} else if (result.status === 'input-required') {
  // TaskStatusUpdateEvent - data from status.message.parts
  console.log('Input needed:', result.message);
  console.log('Reason:', result.data?.reason);
} else if (result.status === 'completed') {
  // Task object - data from .artifacts
  console.log('Success:', result.message);
  console.log('Products:', result.data.products); // Full AdCP response
}

Last Data Part Authority Pattern

Test Cases

✅ Correct Behavior

// Test 1: Working status (TaskStatusUpdateEvent) - extract from status.message.parts
const workingResponse = {
  status: 'working',
  taskId: 'task_123',
  contextId: 'ctx_456',
  status: {
    state: 'working',
    message: {
      role: 'agent',
      parts: [
        { text: 'Processing inventory...' },
        { data: { percentage: 50, current_step: 'analyzing' } }
      ]
    }
  }
};

const result1 = handleA2aResponse(workingResponse);
assert(result1.data.percentage === 50, 'Should extract data from status.message.parts');
assert(result1.message === 'Processing inventory...', 'Should extract text from status.message.parts');

// Test 2: Completed status (Task) - extract from .artifacts
const completedResponse = {
  status: 'completed',
  taskId: 'task_123',
  contextId: 'ctx_456',
  status: {
    state: 'completed',
    timestamp: '2025-01-22T10:30:00Z'
  },
  artifacts: [{
    parts: [
      { kind: 'text', text: 'Found 3 products' },
      { kind: 'data', data: { products: [...], total: 3 } }
    ]
  }]
};

const result2 = handleA2aResponse(completedResponse);
assert(result2.data !== undefined, 'Completed status must have data');
assert(Array.isArray(result2.data.products), 'Data should be direct AdCP payload');

// Test 3: Wrapper detection (should reject)
const wrappedResponse = {
  status: 'completed',
  taskId: 'task_123',
  artifacts: [{
    parts: [
      { kind: 'data', data: { response: { products: [...] } } }
    ]
  }]
};

assert.throws(() => {
  extractDataPartFromArtifacts(wrappedResponse);
}, /Invalid response format.*wrapper/);

❌ Incorrect Behavior (Common Mistakes)

// WRONG: Extracting from wrong location for interim status
function badHandleWorking(response) {
  // ❌ TaskStatusUpdateEvent doesn't have .artifacts - data is in status.message.parts
  const data = response.artifacts?.[0]?.parts?.find(p => p.kind === 'data')?.data;
  return { status: 'working', data }; // Will be null/undefined!
}

// WRONG: Extracting from wrong location for completed status
function badHandleCompleted(response) {
  // ❌ Task object has data in .artifacts, not in status.message.parts
  const data = response.status?.message?.parts?.find(p => p.data)?.data;
  return { status: 'completed', data }; // Will be null/undefined!
}

// WRONG: Not checking for wrappers
function badExtraction(response) {
  const payload = response.artifacts[0].parts[0].data;
  // ❌ Returns { response: { products: [...] } } instead of { products: [...] }
  return payload; // Client receives wrong structure!
}

// WRONG: Accessing nested response field
function badClientUsage(result) {
  // ❌ Client code shouldn't need to do this
  const products = result.data.response.products;
  // Should be: result.data.products
}

Error Handling

Task-Level Errors (Partial Failures)

Task executed but couldn’t complete fully. Use errors array in DataPart with status: "completed":
{
  "status": "completed",
  "taskId": "task_123",
  "artifacts": [{
    "parts": [
      {
        "kind": "text",
        "text": "Signal discovery completed with partial results"
      },
      {
        "kind": "data",
        "data": {
          "signals": [...],
          "errors": [{
            "code": "NO_DATA_IN_REGION",
            "message": "No signal data available for Australia",
            "field": "deliver_to.countries[1]",
            "details": {
              "requested_country": "AU",
              "available_countries": ["US", "CA", "GB"]
            }
          }]
        }
      }
    ]
  }]
}
When to use errors array:
  • Platform authorization issues (PLATFORM_UNAUTHORIZED)
  • Partial data availability
  • Validation issues in subset of data

Protocol-Level Errors (Fatal)

Task couldn’t execute. Use status: "failed" with message:
{
  "taskId": "task_456",
  "status": "failed",
  "message": {
    "parts": [{
      "kind": "text",
      "text": "Authentication failed: Invalid or expired API token"
    }]
  }
}
When to use status: failed:
  • Authentication failures (invalid credentials, expired tokens)
  • Invalid request parameters (malformed JSON, missing required fields)
  • Resource not found (unknown taskId, expired context)
  • System errors (database unavailable, internal service failure)

Status Mapping

AdCP uses A2A’s TaskState enum directly:
A2A StatusPayload TypeData LocationAdCP Usage
completedTask.artifactsTask finished successfully, data in DataPart, optional errors array
failedTask.artifactsFatal error preventing completion, optional error details
input-requiredTaskStatusUpdateEventstatus.message.partsNeed user input/approval, data + text explaining what’s needed
workingTaskStatusUpdateEventstatus.message.partsProcessing (< 120s), optional progress data
submittedTaskStatusUpdateEventstatus.message.partsLong-running (hours/days), minimal data, use webhooks/polling

Webhook Payloads

Async operations (status: "submitted") deliver the same artifact structure in webhooks:
POST /webhook-endpoint
{
  "taskId": "task_123",
  "status": "completed",
  "timestamp": "2025-01-22T10:30:00Z",
  "artifacts": [{
    "parts": [
      {"kind": "text", "text": "Media buy approved and live"},
      {"kind": "data", "data": {
        "media_buy_id": "mb_456",
        "packages": [...],
        "creative_deadline": "2025-01-30T23:59:59Z"
      }}
    ]
  }]
}
Extract AdCP data using the same last-DataPart pattern. For webhook authentication, retry patterns, and security, see Core Concepts - Webhook Reliability.

File Parts in Responses

Creative operations MAY include file references:
{
  "status": "completed",
  "artifacts": [{
    "parts": [
      {"kind": "text", "text": "Creative uploaded and preview generated"},
      {"kind": "data", "data": {
        "creative_id": "cr_789",
        "format_id": {
          "agent_url": "https://creatives.adcontextprotocol.org",
          "id": "video_standard_30s"
        },
        "status": "ready"
      }},
      {"kind": "file", "uri": "https://cdn.example.com/cr_789/preview.mp4", "name": "preview.mp4", "mimeType": "video/mp4"}
    ]
  }]
}
File part usage: Preview URLs, generated assets, trafficking reports. Not for raw AdCP response data (always use DataPart).

Retry and Idempotency

TaskId-Based Deduplication

A2A’s taskId enables retry detection. Agents SHOULD:
  • Return cached response if taskId matches a completed operation (within TTL window)
  • Reject duplicate taskId submission if operation is still in progress
// Duplicate taskId during active operation
{
  "taskId": "task_123",
  "status": "failed",
  "message": {
    "parts": [{
      "kind": "text",
      "text": "Task 'task_123' is already in progress. Use tasks/get to check status."
    }]
  }
}

Examples

Implementation Checklist

When implementing A2A responses for AdCP: Final Responses (status: “completed” or “failed”) - Use Task object:
  • Always include status field from TaskState enum
  • Use .artifacts array with at least one DataPart containing AdCP response payload
  • Include TextPart with human-readable message (recommended for UX)
  • Use single artifact with multiple parts (not multiple artifacts)
  • Use last DataPart as authoritative if multiple exist
  • Never nest AdCP data in custom wrappers (no { response: {...} } objects)
  • DataPart content MUST match AdCP schemas (validate against [task]-response.json)
Interim Responses (status: “working”, “submitted”, “input-required”) - Use TaskStatusUpdateEvent:
  • Use status.message.parts[] for optional data (not .artifacts)
  • TextPart is recommended for human-readable status updates
  • DataPart is optional but follows AdCP schemas when provided ([task]-async-response-[status].json)
  • Interim schemas are work-in-progress - clients may handle more loosely
  • Include progress indicators when applicable (percentage, current_step, ETA)
Error Handling:
  • Use status: "failed" for protocol errors only (auth, invalid params, system errors)
  • Use errors array for task failures (platform auth, partial data) with status: "completed"
General:
  • Include taskId and contextId for tracking
  • Follow discriminated union patterns for task responses (check schemas)
  • Use correct payload type: Task for final states, TaskStatusUpdateEvent for interim
  • Support taskId-based deduplication for retry detection

See Also