> ## Documentation Index
> Fetch the complete documentation index at: https://agenticadvertisingorg-feature-feedback.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Async Operations

> AdCP async operations guide: handling synchronous, asynchronous, and interactive (input-required) task types with polling, SSE streaming, and timeout strategies.

AdCP operations can take seconds, hours, or days. The server decides how to respond based on how long the operation will take and what's blocking it.

## The 30-second rule

Any AdCP task can return one of these statuses. The server chooses based on what it knows about the work involved:

| Expected duration                           | Status                 | What the caller does                                                                                                                      |
| ------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Under 30 seconds                            | `completed` / `failed` | Result is inline — done                                                                                                                   |
| Over 30 seconds, server actively processing | `working`              | Out-of-band progress signal. Connection stays open, result arrives when ready. Caller just waits                                          |
| Blocked on external dependency              | `submitted`            | Truly async — poll by `task_id`; `push_notification_config` can also deliver notifications when configured. Result may take hours or days |
| Blocked on human input                      | `input-required`       | Caller provides the requested input to continue                                                                                           |

**`working` is not async.** It's a progress signal the server sends out-of-band (via MCP status notifications or SSE) while it continues processing. The caller holds the connection and receives the result when it's ready — no polling, no webhooks. Think of it as "this is taking a moment, but I'm on it."

**`submitted` is async.** The operation is blocked on something outside the server's control — publisher approval, human review, third-party processing. The caller can always poll the AdCP task status surface with `task_id`. A configured webhook is an additional notification channel for background workflows, not a replacement for polling.

:::tip Webhooks for `submitted` operations
**Webhooks** are recommended for background `submitted` operations — they work with any transport (MCP, A2A, REST) and handle operations that outlive a single session. For MCP/REST, the webhook channel is requested with `push_notification_config` on the task request. For A2A, it remains transport configuration at `configuration.pushNotificationConfig`, not a snake\_case skill parameter. When a request includes a webhook channel and the server accepts the task by returning `submitted`, the server MUST deliver at least the terminal completion or failure notification to that channel. Intermediate progress notifications are optional unless another operation-specific contract requires them. If the server cannot honor the requested webhook channel, it MUST reject the request with a structured error instead of silently downgrading delivery. See [Push Notifications](/docs/building/by-layer/L3/webhooks).

**Polling** via the AdCP task polling surface is always valid for `submitted` tasks. In 3.x, that surface is legacy `tasks/get`, with optional `get_task_status` when the seller advertises the alias. Both names accept the same payload shape; multi-account callers SHOULD include `account` so sellers can scope task visibility to the authenticated account + principal pair. See the [polling pattern](#polling-for-submitted-operations) below.

**Transport-native tasks are not the AdCP lifecycle.** MCP Tasks or A2A task updates may carry or stream an AdCP response, but the durable task state is the AdCP payload: `task_id`, `status`, webhook payloads, and AdCP polling/reconciliation. See [MCP Guide](/docs/building/by-layer/L0/mcp-guide#mcp-tasks-as-a-transport-wrapper).
:::

## Operation examples

### Synchronous (instant)

| Operation                            | Description                         |
| ------------------------------------ | ----------------------------------- |
| `get_adcp_capabilities`              | Agent capability discovery          |
| `list_creative_formats`              | Format catalog                      |
| `build_creative` (library retrieval) | Resolving an existing `creative_id` |

### May need human input

| Operation                     | Description                                          |
| ----------------------------- | ---------------------------------------------------- |
| `get_products`                | When brief is vague or needs clarification           |
| `create_media_buy`            | When approval is required                            |
| `build_creative` (generation) | When creative direction or asset selection is needed |

### May go async (`submitted`)

| Operation                           | Description                                                                |
| ----------------------------------- | -------------------------------------------------------------------------- |
| `create_media_buy`                  | Publisher approval workflows                                               |
| `update_media_buy`                  | Manual seller review for budget, targeting, or creative changes            |
| `get_products` (`brief` / `refine`) | Bespoke curation that depends on upstream inventory queries or HITL review |
| `get_signals` (`brief`)             | Semantic signal discovery that depends on slow provider queries or review  |
| `sync_creatives`                    | Asset review and transcoding pipelines                                     |
| `build_creative` (with review)      | Human creative review before finalizing                                    |
| `sync_catalogs`                     | Large feeds or feeds requiring content policy review                       |
| `activate_signal`                   | Platform deployment pipelines                                              |

These operations integrate with external systems or require human approval. Wholesale feed reads are the exception: `get_products buying_mode: "wholesale"` and `get_signals discovery_mode: "wholesale"` stay synchronous repair/reconciliation reads and report partial completion with `incomplete[]`, not `submitted`. `get_products buying_mode: "open"` is also a synchronous state read; sellers return the unresolved products/proposals they already associate with the caller/account/opportunity rather than routing through `submitted`.

## Timeout Configuration

Set reasonable timeouts based on status:

```javascript theme={null}
const TIMEOUTS = {
  sync: 30_000,         // 30 seconds — most operations complete here
  working: 300_000,      // 5 minutes — server is actively processing
  interactive: 300_000,  // 5 minutes for human input
  submitted: 86_400_000  // 24 hours for external dependencies
};

function getTimeout(status) {
  if (status === 'submitted') return TIMEOUTS.submitted;
  if (status === 'working') return TIMEOUTS.working;
  if (status === 'input-required') return TIMEOUTS.interactive;
  return TIMEOUTS.sync;
}
```

`working` uses a connection timeout (how long to hold open), not a poll interval. The server sends progress out-of-band and delivers the result on the same connection. `submitted` uses a polling or webhook delivery window; a 30-second poll interval is a reasonable default even when webhooks are configured as the primary notification path.

## Human-in-the-Loop Workflows

### Design Principles

1. **Optional by default** - Approvals are configured per implementation
2. **Clear messaging** - Users understand what they're approving
3. **Timeout gracefully** - Don't block forever on human input
4. **Audit trail** - Track who approved what when

The human-in-the-loop patterns in async operations embody the [Embedded Human Judgment](/docs/governance/embedded-human-judgment) framework — human judgment is embedded in system design, not bolted on afterward.

### Approval Patterns

```javascript theme={null}
async function handleApprovalWorkflow(response) {
  if (response.status === 'input-required' && needsApproval(response)) {
    // Show approval UI with context
    const approval = await showApprovalUI({
      title: "Campaign Approval Required",
      message: response.message,
      details: response,  // Task fields are at top level
      approver: getCurrentUser()
    });

    // Send approval decision
    const decision = {
      approved: approval.approved,
      notes: approval.notes,
      approver_id: approval.approver_id,
      timestamp: new Date().toISOString()
    };

    return sendFollowUp(response.context_id, decision);
  }
}
```

### Common Approval Triggers

* **Budget thresholds**: Campaigns over \$100K
* **New advertisers**: First-time buyers
* **Policy-sensitive content**: Certain industries or topics
* **Manual inventory**: Premium placements requiring publisher approval

## Progress Tracking

### Progress Updates

Long-running operations may provide progress information:

```json theme={null}
{
  "status": "working",
  "message": "Processing creative assets...",
  "task_id": "task-456",
  "progress": 45,
  "step": "transcoding_video",
  "steps_completed": ["upload", "validation"],
  "steps_remaining": ["transcoding_video", "thumbnail_generation", "cdn_distribution"]
}
```

### Displaying Progress

```javascript theme={null}
function displayProgress(response) {
  if (response.progress !== undefined) {
    updateProgressBar(response.progress);
  }

  if (response.step) {
    updateStatusText(`Step: ${response.step}`);
  }

  if (response.steps_completed) {
    updateStepsList(response.steps_completed, response.steps_remaining);
  }

  // Always show the message
  updateMessage(response.message);
}
```

## Protocol-Agnostic Patterns

These patterns work with both MCP and A2A.

### Product Discovery with Clarification

```javascript theme={null}
async function discoverProducts(brief) {
  let response = await adcp.send({
    task: 'get_products',
    brief: brief
  });

  // Handle clarification loop
  while (response.status === 'input-required') {
    const moreInfo = await promptUser(response.message);
    response = await adcp.send({
      context_id: response.context_id,
      additional_info: moreInfo
    });
  }

  if (response.status === 'completed') {
    return response.products;  // Task fields are at top level
  } else if (response.status === 'failed') {
    throw new Error(response.message);
  }
}
```

### Campaign Creation with Approval

```javascript theme={null}
async function createCampaign(packages, budget) {
  let response = await adcp.send({
    task: 'create_media_buy',
    packages: packages,
    total_budget: budget
  });

  // Handle approval if needed
  if (response.status === 'input-required') {
    const approved = await getApproval(response.message);
    if (!approved) {
      throw new Error('Campaign creation not approved');
    }

    response = await adcp.send({
      context_id: response.context_id,
      approved: true
    });
  }

  // 'working' means the server is actively processing — result will arrive
  // 'submitted' means blocked on external dependency — poll by task_id;
  // a webhook may also deliver completion if configured.
  if (response.status === 'submitted') {
    response = await pollForResult(response.task_id);
  }

  if (response.status === 'completed') {
    return response.media_buy_id;  // Task fields are at top level
  } else {
    throw new Error(response.message);
  }
}
```

### Polling for `submitted` Operations

Polling is always valid for `submitted` operations. A webhook may also deliver completion when configured, but callers can still reconcile through the task polling surface. Don't poll for `working` — the server delivers the result on the open connection.

```javascript theme={null}
async function pollForResult(taskId, options = {}) {
  const { maxWait = 86_400_000, pollInterval = 30_000 } = options;
  const startTime = Date.now();

  while (true) {
    if (Date.now() - startTime > maxWait) {
      throw new Error('Operation timed out');
    }

    await sleep(pollInterval);

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

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

## Asynchronous-First Design

### Store State Persistently

Don't rely on in-memory state for async operations:

```javascript theme={null}
class AsyncOperationTracker {
  constructor(db) {
    this.db = db;
  }

  async startOperation(taskId, operationType, request) {
    await this.db.operations.insert({
      task_id: taskId,
      type: operationType,
      status: 'submitted',
      request: request,
      created_at: new Date(),
      updated_at: new Date()
    });
  }

  async updateStatus(taskId, status, result = null) {
    await this.db.operations.update(
      { task_id: taskId },
      {
        status: status,
        result: result,
        updated_at: new Date()
      }
    );
  }

  async getPendingOperations() {
    return this.db.operations.find({
      status: { $in: ['submitted', 'working', 'input-required'] }
    });
  }
}
```

### Handle Restarts Gracefully

Resume tracking after orchestrator restarts:

```javascript theme={null}
async function onStartup() {
  const tracker = new AsyncOperationTracker(db);
  const pending = await tracker.getPendingOperations();

  for (const operation of pending) {
    // Check current status on server
    const response = await adcp.call('get_task_status', {
      task_id: operation.task_id,
      include_result: true
    });

    // Update local state
    await tracker.updateStatus(operation.task_id, response.status, response);

    // Resume polling if still pending
    if (['submitted', 'working'].includes(response.status)) {
      startPolling(operation.task_id);
    }
  }
}
```

## Best Practices

1. **Design async first** - Assume any operation could take time
2. **Persist state** - Don't rely on in-memory tracking
3. **Handle restarts** - Resume tracking on startup
4. **Implement timeouts** - Don't wait forever
5. **Show progress** - Keep users informed
6. **Support cancellation** - Let users cancel long operations
7. **Audit trail** - Log all status transitions

## Next Steps

* **Webhooks**: See [Webhooks](/docs/building/by-layer/L3/webhooks) for push notifications instead of polling
* **Task Lifecycle**: See [Task Lifecycle](/docs/building/by-layer/L3/task-lifecycle) for status handling details
* **Orchestrator Design**: See [Orchestrator Design](/docs/building/operating/orchestrator-design) for production patterns
