Skip to main content
Every AdCP response includes a status field that tells you exactly what state the operation is in and what action you should take next. This is the foundation for handling any AdCP operation.

Status Values

AdCP uses the same status values as the A2A protocol’s TaskState enum:
StatusMeaningYour Action
submittedTask queued for executionShow “queued” indicator, wait for updates
workingAgent actively processingShow progress, poll frequently for updates
input-requiredNeeds information from youRead message field, prompt user, send follow-up
completedSuccessfully finishedProcess data, show success message
canceledUser/system canceled taskShow cancellation notice, clean up
failedError occurredShow error from message, handle gracefully
rejectedAgent rejected the requestShow rejection reason, don’t retry
auth-requiredAuthentication neededPrompt for auth, retry with credentials
unknownIndeterminate stateLog for debugging, may need manual intervention

Response Structure

Every AdCP response uses a flat structure where task-specific fields are at the top level:
{
  "status": "completed",           // Always present: what state we're in
  "message": "Found 5 products",   // Always present: human explanation
  "context_id": "ctx-123",         // Session continuity
  "context": {                     // Application-level context echoed back
    "ui": "buyer_dashboard"
  },
  "products": [...]                // Task-specific fields at top level
}

Status Handling

Basic Pattern

function handleAdcpResponse(response) {
  switch (response.status) {
    case 'completed':
      // Success - process the data (task fields are at top level)
      showSuccess(response.message);
      return processData(response);

    case 'input-required':
      // Need more info - prompt user
      const userInput = await promptUser(response.message);
      return sendFollowUp(response.context_id, userInput);

    case 'working':
      // In progress - show progress and wait
      showProgress(response.message);
      return pollForUpdates(response.context_id);

    case 'failed':
      // Error - show message and handle gracefully
      showError(response.message);
      return handleError(response.errors);

    case 'auth-required':
      // Authentication needed
      const credentials = await getAuth();
      return retryWithAuth(credentials);

    default:
      // Unexpected status
      console.warn('Unknown status:', response.status);
      showMessage(response.message);
  }
}

Clarification Flow

When status is input-required, the message tells you what’s needed:
{
  "status": "input-required",
  "message": "I need more information about your campaign. What's your budget and target audience?",
  "context_id": "ctx-123",
  "products": [],
  "suggestions": ["budget", "audience", "timing"]
}
Client handling:
if (response.status === 'input-required') {
  // Extract what's needed from the message
  const missingInfo = extractRequirements(response.message);

  // Prompt user with specific questions
  const answers = await promptForInfo(missingInfo);

  // Send follow-up with same context_id
  return sendMessage(response.context_id, answers);
}

Approval Flow

Human approval is a special case of input-required:
{
  "status": "input-required",
  "message": "Media buy exceeds auto-approval limit ($100K). Please approve to proceed with campaign creation.",
  "context_id": "ctx-123",
  "approval_required": true,
  "amount": 150000,
  "reason": "exceeds_limit"
}
Client handling:
if (response.status === 'input-required' && response.approval_required) {
  // Show approval UI
  const approved = await showApprovalDialog(response.message, response);

  // Send approval decision
  const decision = approved ? "Approved" : "Rejected";
  return sendMessage(response.context_id, decision);
}

Long-Running Operations

Async operations start with working or submitted and provide updates:
{
  "status": "working",
  "message": "Creating media buy. Validating inventory availability...",
  "context_id": "ctx-123",
  "task_id": "task-456",
  "progress": 25,
  "step": "inventory_validation"
}
Protocol-specific polling:
  • MCP: Poll with context_id for updates
  • A2A: Subscribe to SSE stream for real-time updates

Status Progression

Tasks progress through predictable states:
submitted → working → completed
    ↓          ↓         ↑
input-required → → → → →

  failed
  • submitted: Task queued for execution, provide webhook or poll
  • working: Agent actively processing, poll frequently
  • input-required: Need user input, continue conversation
  • completed: Success, process results
  • failed: Error, handle appropriately

Polling Patterns

Status-Based Polling Intervals

Different statuses require different polling frequencies:
const POLLING_INTERVALS = {
  working: 5000,      // 5 seconds - should complete within 120s
  submitted: 60000,   // 1 minute - long-running operations
  'input-required': null  // Don't poll - wait for user input
};

async function pollForUpdates(taskId, currentStatus) {
  const interval = POLLING_INTERVALS[currentStatus];
  if (!interval) return;

  await sleep(interval);

  const response = await adcp.call('tasks/get', {
    task_id: taskId,
    include_result: true
  });

  if (['completed', 'failed', 'canceled'].includes(response.status)) {
    return response;
  }

  return pollForUpdates(taskId, response.status);
}

Timeout Handling

Set reasonable timeouts based on operation type:
const TIMEOUTS = {
  sync: 30_000,        // 30 seconds for immediate operations
  interactive: 300_000, // 5 minutes for human input
  working: 120_000,    // 2 minutes for working tasks
  submitted: 86_400_000 // 24 hours for submitted tasks
};

function setTimeoutForStatus(status) {
  switch (status) {
    case 'working': return TIMEOUTS.working;
    case 'submitted': return TIMEOUTS.submitted;
    case 'input-required': return TIMEOUTS.interactive;
    default: return TIMEOUTS.sync;
  }
}

Task Reconciliation

Use tasks/list to recover from lost state:
// Find all pending operations
const pending = await session.call('tasks/list', {
  filters: {
    statuses: ["submitted", "working", "input-required"]
  }
});

// Reconcile with local state
const missingTasks = pending.tasks.filter(task =>
  !localState.hasTask(task.task_id)
);

// Resume tracking missing tasks
for (const task of missingTasks) {
  startPolling(task.task_id);
}

Best Practices

  1. Always check status first - Don’t assume success
  2. Handle all statuses - Include a default case for unknown states
  3. Preserve context_id - Required for conversation continuity
  4. Use task_id for tracking - Especially for long-running operations
  5. Implement timeouts - Don’t wait forever
  6. Log status transitions - Helps with debugging and auditing

Next Steps

  • Async Operations: See Async Operations for handling different operation types
  • Webhooks: See Webhooks for push notification patterns
  • Error Handling: See Error Handling for error categories and recovery