> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brew.new/llms.txt
> Use this file to discover all available pages before exploring further.

# Async Jobs & Polling

> Long-running operations on the Brew Public API v1, how to fire automation runs, queue email sends, and poll for completion without webhooks.

Brew's two delivery primitives (campaign sends and automation runs) are **asynchronous by design**. The HTTP call accepts the work, persists an audit row, and returns a job identifier; the actual delivery happens on a durable Vercel Workflow runtime that retries, backs off, and emits per-step logs. This page covers the three async primitives, the canonical poll loop, and the agent-friendly recipes for concurrency.

> Outbound webhooks for run / send / email-event lifecycle are on the [roadmap](/api-reference/api/events-and-webhooks#outbound-webhooks-roadmap). Until they ship, polling is the supported pattern. Both surfaces work today.

## The Three Async Primitives

| Primitive              | Endpoint                                              | Response                                                                               | Poll with                                                 |
| ---------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| **Fire an automation** | `POST /v1/automations/triggers/{triggerEventId}/fire` | `200 { success, status, details: { automationRunIds: [...] } }` (legacy fire envelope) | `GET /v1/automations/runs?automationRunId=…&include=logs` |
| **Test an automation** | `POST /v1/automations/{automationId}/test`            | `202 { automationRunIds: [...], status: 'test_started' }`                              | Same as fire                                              |
| **Send an email**      | `POST /v1/sends`                                      | `202 { status, sendId, runId, scheduledAt? }`                                          | `GET /v1/analytics/sends?sendId=…`                        |

A `POST /v1/emails` (AI generation) is also long-running (\~30-90s) but **blocks the HTTP call until the agent renders the design or refuses**. It is not a job-returning async. You get the artifact back in the response.

## Fire a Trigger, Then Poll the Run

Fire returns a list of `automationRunIds` (one per matched **Published** automation). Each id is a workflow run you can inspect. If no bound automation is Published, the call returns `422 NO_PUBLISHED_AUTOMATION` instead of `202` and there is nothing to poll. Publish at least one bound automation and re-fire.

```ts theme={null}
import { setTimeout as sleep } from 'node:timers/promises'

async function fireAndPoll() {
  // 1. Fire, returns 202 with the run ids (trigger id is in the PATH)
  const fire = await fetch('https://brew.new/api/v1/automations/triggers/tri_signup/fire', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BREW_API_KEY!}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `signup-${userId}-${eventTimestamp}`,
    },
    body: JSON.stringify({
      payload: { email: 'jane@example.com', firstName: 'Jane' },
    }),
  })
  const fireBody = await fire.json() as {
    details: { automationRunIds: string[]; triggerInstanceId?: string }
  }
  const [runId] = fireBody.details.automationRunIds

  // 2. Poll the run until it terminates (logs are always attached)
  for (let attempt = 0; attempt < 60; attempt += 1) {
    const res = await fetch(
      `https://brew.new/api/v1/automations/runs/${runId}`,
      { headers: { Authorization: `Bearer ${process.env.BREW_API_KEY!}` } }
    )
    const body = await res.json() as {
      runs: Array<{
        automationRunId: string
        status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
      }>
      logs?: Array<{ nodeName: string; status: string; durationMs?: number }>
    }
    const run = body.runs[0]!
    if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
      console.log(`Run ${run.automationRunId} → ${run.status}`)
      for (const log of body.logs ?? []) {
        console.log(`  ${log.nodeName}: ${log.status} (${log.durationMs ?? '?'}ms)`)
      }
      return run
    }
    await sleep(2_000) // poll every 2 seconds
  }
  throw new Error('Run did not terminate within 2 minutes')
}
```

### Recommended Polling Cadence

| Latency expectation                                    | Cadence                                                                        | Cap   |
| ------------------------------------------------------ | ------------------------------------------------------------------------------ | ----- |
| Welcome / transactional flow (single `sendEmail` node) | 2s × 60                                                                        | 2 min |
| Multi-step drip with `wait` nodes (`days`)             | Don't poll: read the run when you actually need the answer (e.g. nightly cron) | n/a   |
| QA / test fire                                         | 1s × 30                                                                        | 30s   |

The `automation.runs.read` policy is `100/min` per key. A 2-second poll is well under that for a handful of concurrent agents.

## Queue a Send, Then Verify

`POST /v1/sends` returns `202 { status, sendId, runId, scheduledAt? }`. The `sendId` keys the send row for analytics reads; the `runId` is the Vercel Workflow run id (correlated with delivery analytics on the dashboard). The body takes EITHER an `audienceId` OR an inline `to` (a single email or an array of ≤ 50).

```ts theme={null}
const send = await brew.emails.send({
  emailId: 'eml_promo',
  emailVersionId: 'emv_promo_v2',
  domainId: 'dom_brand_primary',
  audienceId: 'aud_subscribers',
  subject: 'Black Friday, 40% off',
})

console.log(send)
// { status: 'queued', sendId: 'snd_…', runId: 'wrun_…' }
```

Poll the send row with `GET /v1/analytics/sends?sendId=…`; add `&include=events` to inline the per-recipient feed (delivered / opened / clicked / bounced) on the same row. For one-shot validations, the dashboard at [brew.new/analytics](https://brew.new/analytics) is also a source of truth.

## Idempotency on Async Retries

Because retries are guaranteed-safe with idempotency keys, your agent / cron loop can re-fire the same operation without double-delivery. Same key + same body → same `automationRunIds[]`, same `runId`, same response payload. See [Idempotency](/api-reference/api/idempotency) for the full contract.

## Agent Concurrency Patterns

### Pattern 1: Fan Out, Fan In

An agent that runs 50 different welcome flows in parallel:

```ts theme={null}
const targets = await loadNewSignups()         // 50 users

const fires = await Promise.all(
  targets.map((user) =>
    brew.automations.triggers.fire(
      {
        triggerEventId: 'tri_signup',
        payload: { email: user.email, firstName: user.firstName },
      },
      { idempotencyKey: `signup-${user.id}-${user.signedUpAt}` }
    )
  )
)

// Each fire returns its own automationRunIds[]; collect for later inspection
const allRunIds = fires.flatMap((f) => f.automationRunIds)
```

The `automation.runs.write` policy is `60/min`. For 50 parallel fires, you're well under; for 200+ you'd batch with `Promise.allSettled` + a `429`-aware retry helper.

### Pattern 2: Long-Poll a Single Run to Terminal

Use a generator so the agent can yield other work between polls:

```ts theme={null}
async function* watchRun(runId: string) {
  while (true) {
    const { data } = await brew.automations.runs.list({
      automationRunId: runId,
      include: 'logs',
    })
    const run = data[0]!
    yield run
    if (['completed', 'failed', 'cancelled'].includes(run.status)) return
    await new Promise((r) => setTimeout(r, 2_000))
  }
}

for await (const snapshot of watchRun(runId)) {
  await mySideEffect(snapshot)   // e.g. update a UI badge
}
```

### Pattern 3: Bulk Inspect by Automation

For dashboards / agents that want "the last 100 runs of this automation":

```ts theme={null}
const { data: runs } = await brew.automations.runs.list({
  automationId: 'auto_abc',
  limit: 100,
})
const failed = runs.filter((r) => r.status === 'failed')
```

## Why No Webhooks Today (and What's Next)

The fastest path to an A-grade `Events & Streaming` score is outbound webhooks; we're shipping them. See the [Webhooks & events](/api-reference/api/events-and-webhooks) page for the planned contract (`webhook.run.completed`, `webhook.send.delivered`, `webhook.send.opened`, …) and the roadmap. Until then, the poll loop on `/v1/automations/runs` is the supported way to wait for terminal status.

## Endpoints That Are Already Async-Friendly

| Endpoint                                              | Job indicator                                             | Status read                                  |
| ----------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------- |
| `POST /v1/automations/triggers/{triggerEventId}/fire` | `details.automationRunIds[]` (one per matched automation) | `GET /v1/automations/runs?automationRunId=…` |
| `POST /v1/automations/{automationId}/test`            | `automationRunIds[]`                                      | Same                                         |
| `POST /v1/sends`                                      | `sendId` + `runId` (Vercel Workflow run id)               | `GET /v1/analytics/sends?sendId=…`           |
| `POST /v1/contacts` (batch)                           | `summary.{inserted,updated,failed}`: synchronous          | n/a                                          |

## See Also

* [Idempotency](/api-reference/api/idempotency): pair with every async retry.
* [Rate limits](/api-reference/api/rate-limits): keep your poll loop under `automation.runs.read` (`100/min`).
* [Webhooks & events](/api-reference/api/events-and-webhooks): inbound model today + outbound roadmap.
* [Pagination](/api-reference/api/pagination): for bulk-inspecting runs.
* [Agentic cookbook](/sdks/typescript/agentic-cookbook): end-to-end recipes built on these primitives.

## Need help?

Our team is ready to support you at every step of your journey with Brew. Choose the option that works best for you:

<Tabs>
  <Tab title="Self-Service Tools">
    <CardGroup cols="2">
      <Card title="Search Documentation" icon="magnifying-glass" color="#c44925">
        Type in the "Ask any question" search bar at the top left to instantly find relevant documentation pages.
      </Card>

      <Card title="ChatGPT/Claude Integration" icon="robot" color="#c44925">
        Click "Open in ChatGPT" at the top right of any page to analyze documentation with ChatGPT or Claude for deeper insights.
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Talk to Our Team">
    <CardGroup cols="2">
      <Card title="Schedule a Call" icon="calendar" color="#c44925" href="https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ1iYoRUG1J792XQpbuQLjSRRDupr7MwraFK-HQRCtTYdBmrQi8nZu2qXfzKQigb8gbKJK3KN3-R">
        Book time with our founders for personalized guidance on strategy, best practices, or complex implementation questions.
      </Card>

      <Card title="Call Us Directly" icon="phone" color="#c44925">
        Need immediate assistance? Reach us at **+1-(332)-203-2145** for urgent issues or time-sensitive questions.
      </Card>

      <Card title="Slack Channel" icon="slack" color="#c44925">
        Our preferred support channel. You'll receive an invite after signup for direct founder support and fast responses.
      </Card>

      <Card title="Email Support" icon="envelope" color="#c44925" href="mailto:support@brew.new">
        Contact us at **[support@brew.new](mailto:support@brew.new)** for detailed inquiries or if you prefer not to use Slack.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>
