> ## 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 campaign 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>
