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

# Agentic Cookbook

> End-to-end deterministic recipes for AI agents using the Brew SDK to manage email automations programmatically

## When to use this cookbook

You're building an AI agent (or any LLM-powered workflow) that should
manage Brew automations without the dashboard UI. The Brew SDK exposes
every authoring, lifecycle, and execution path through typed
**deterministic** methods. AI authoring stays scoped to email body
generation (`brew.emails.generate({ prompt })`); the rest
of the surface — triggers, automation graphs, publish, fire — works
on explicit shapes so the agent's intent is always inspectable.

## Mental model

Brew has three primary entities the SDK works with:

| Entity             | What it is                                                                                                 | SDK resource                |
| ------------------ | ---------------------------------------------------------------------------------------------------------- | --------------------------- |
| **Trigger event**  | A schema contract (`payloadSchema.fields`) plus a stable id. Long-lived. Brand-scoped.                     | `brew.automations.triggers` |
| **Automation**     | A graph of nodes (`trigger`, `wait`, `filter`, `split`, `sendEmail`) that fires on one trigger. Versioned. | `brew.automations`          |
| **Automation run** | One workflow run started by a fire / test.                                                                 | `brew.automations.runs`     |

Reads are flat: each resource exposes one `list()`. Pass the id key for a
single row (`list({ automationId })`, `list({ automationRunId })`) and
`include` to embed detail (`include: 'graph,versions'` on automations,
`include: 'logs'` on runs).

The full lifecycle: **define trigger → mint email bodies → assemble automation graph → publish → fire → inspect executions**.

## Recipe 1: End-to-end deterministic flow

The canonical recipe. Define the trigger explicitly, mint the email
bodies in parallel, then assemble and publish the graph.

```ts theme={null}
import { createBrewClient } from '@brew.new/sdk'

const brew = createBrewClient({ apiKey: process.env.BREW_API_KEY! })

// 1. Define the trigger payload schema deterministically.
//    The server hardcodes provider: 'brew_api' — don't send one.
//    create returns the bare trigger row (HTTP 201).
const trigger = await brew.automations.triggers.create({
  title: 'User Signed Up',
  description: 'Fires when a user completes signup.',
  payloadSchema: {
    type: 'object',
    fields: [
      { key: 'email', type: 'string', required: true },
      { key: 'firstName', type: 'string', required: false },
      { key: 'plan', type: 'string', required: false },
    ],
  },
})

// 2. Mint each email body. These rows are referenced by sendEmail nodes
//    in the graph below.
const [welcome, dayTwo] = await Promise.all([
  brew.emails.generate({
    prompt: 'Friendly welcome email for new signups — single CTA to the app.',
  }),
  brew.emails.generate({
    prompt: 'Day-2 nudge — three getting-started tips, light tone.',
  }),
])

// 3. Assemble the automation graph. AutomationNodeInput is a per-kind
//    discriminated union — set node.type and TypeScript narrows
//    node.config automatically.
//
//    A custom sending domain is mandatory for API-authored automations —
//    pick one from `brew.domains.list({ sendableOnly: true })`.
const domainId = 'domain_123' // replace with a real verified domain id
const automation = await brew.automations.create({
  name: 'Welcome flow',
  triggerEventId: trigger.triggerEventId,
  nodes: [
    {
      id: 'trg',
      label: 'On signup',
      type: 'trigger',
      config: { triggerEventId: trigger.triggerEventId },
    },
    {
      id: 'send_welcome',
      label: 'Welcome',
      type: 'sendEmail',
      config: {
        emailId: welcome.emailId,
        // Pin the exact version so later edits don't change what fires.
        emailVersionId: welcome.emailVersionId,
        domainId,
        // {{variable | fallback}} interpolation against the trigger payload.
        subject: 'Welcome to Brew, {{firstName | there}}!',
        previewText: 'Thanks for signing up — here is how to get started.',
      },
    },
    {
      id: 'wait_2d',
      label: 'Wait 2 days',
      type: 'wait',
      config: { duration: 2, unit: 'days' },
    },
    {
      id: 'send_day2',
      label: 'Day 2 nudge',
      type: 'sendEmail',
      config: {
        emailId: dayTwo.emailId,
        emailVersionId: dayTwo.emailVersionId,
        domainId,
        subject: 'Three ways to get more out of Brew',
        previewText: 'A few getting-started tips to help you move faster.',
      },
    },
  ],
  connections: [
    { from: 'trg', to: 'send_welcome' },
    { from: 'send_welcome', to: 'wait_2d' },
    { from: 'wait_2d', to: 'send_day2' },
  ],
})

// 4. Publish (convenience wrapper over patch({ published: true })).
await brew.automations.publish({ automationId: automation.automationId })

console.log('Ready to fire:', {
  triggerEventId: trigger.triggerEventId,
  automationId: automation.automationId,
})
```

## Recipe 2: Reuse an existing trigger

When your codebase already wires a stable trigger id (auth, billing,
or platform webhook), skip step 1 and reuse it across multiple
automations:

```ts theme={null}
const TRIGGER_EVENT_ID = 'tri_order_shipped'

const confirmation = await brew.emails.generate({
  prompt: 'Order-shipped confirmation with tracking link.',
})

// A custom sending domain is mandatory for API-authored automations —
// pick one from `brew.domains.list({ sendableOnly: true })`.
const domainId = 'domain_123' // replace with a real verified domain id

const automation = await brew.automations.create({
  name: 'Order shipped — confirmation',
  triggerEventId: TRIGGER_EVENT_ID,
  nodes: [
    {
      id: 'trg',
      label: 'Order shipped',
      type: 'trigger',
      config: { triggerEventId: TRIGGER_EVENT_ID },
    },
    {
      id: 'send_confirm',
      label: 'Send confirmation',
      type: 'sendEmail',
      config: {
        emailId: confirmation.emailId,
        emailVersionId: confirmation.emailVersionId,
        domainId,
        subject: 'Order #{{orderId}} is on its way',
        previewText: 'Your order has shipped — track it here.',
      },
    },
  ],
  connections: [{ from: 'trg', to: 'send_confirm' }],
})

await brew.automations.publish({ automationId: automation.automationId })
```

## Recipe 3: Discover existing email bodies before authoring

`brew.emails.list()` returns every body already minted for the brand so
the agent can reuse instead of re-generating duplicates:

```ts theme={null}
const existing = await brew.emails.list()
const reusable = existing.data.find((e) =>
  e.title.toLowerCase().includes('welcome')
)

const welcomeEmailId =
  reusable?.emailId ??
  (await brew.emails.generate({
    prompt: 'Welcome email for new signups.',
  })).emailId
```

## Recipe 4: Fire a trigger from your backend

After publishing, your application code calls
`brew.automations.triggers.fire` whenever the real-world event happens.
Always pass an `idempotencyKey` so retries don't double-fire. `metadata`
is NOT accepted — the trigger `payload` carries everything the workflow
runtime sees.

```ts theme={null}
const fire = await brew.automations.triggers.fire({
  triggerEventId: 'tri_order_shipped',
  payload: {
    email: order.customerEmail,
    firstName: order.customerFirstName,
    orderId: order.id,
    trackingUrl: order.trackingUrl,
  },
  idempotencyKey: `order-shipped-${order.id}`,
})
```

The fire endpoint returns the legacy fire envelope; the started
`automationRunIds` live under `fire.details.automationRunIds` so you can
correlate analytics. Retries with the same `idempotencyKey` replay the
original `automationRunIds` without starting new workflow runs.

## Recipe 5: Test an automation against synthetic data

Use `brew.automations.test` to test-run an automation (no real mail is
sent — `sendEmail` nodes simulate). The resulting run is recorded as a
test-mode run you can inspect via `brew.automations.runs.list({ automationRunId, include: 'logs' })`
(pass `include: 'logs'` to attach the per-node `logs[]`).

```ts theme={null}
const test = await brew.automations.test({
  automationId,
  payload: { email: 'qa@example.com', firstName: 'QA' },
})

// Wait a moment for the workflow runtime, then read logs.
await new Promise((r) => setTimeout(r, 2000))

const { data: runs } = await brew.automations.runs.list({
  automationRunId: test.automationRunIds[0]!,
  include: 'logs',
})
const run = runs[0]!

console.log('Status:', run.status)
for (const log of run.logs ?? []) {
  console.log(`${log.nodeName}: ${log.status} (${log.durationMs ?? '?'}ms)`)
}
```

## Recipe 6: Edit a published automation safely

Use `brew.automations.patch({ automationId, nodes, connections })`
to stage a new version of the graph deterministically, then a
second `patch({ published: true })` to promote it. (Lifecycle and
graph edits can't ride the same `patch` — `published` is mutually
exclusive with field updates.) The previously-published row stays
live until you publish the new version explicitly.

```ts theme={null}
const { data: automations } = await brew.automations.list({
  automationId: 'auto_abc',
  include: 'graph',
})
const latest = automations[0]!

const newNodes = latest.nodes.map((node) =>
  node.type === 'wait'
    ? { ...node, config: { duration: 2, unit: 'days' as const } }
    : node
)

const patched = await brew.automations.patch({
  automationId: 'auto_abc',
  nodes: newNodes,
  connections: latest.connections,
})
const nextVersion = patched

await brew.automations.patch({
  automationId: 'auto_abc',
  published: true,
  automationVersionId: nextVersion.automationVersionId,
})
```

## Error matrix

Every method throws `BrewApiError` on transport / auth / business
errors. The error envelope carries `code`, `type`, `message`,
`suggestion`, `docs` URL, and the offending `param` when relevant.

| Code                                                              | When                                                                                                                                                                                                                                                                                                                                                  |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTHENTICATION_REQUIRED` / `INVALID_API_KEY` / `API_KEY_REVOKED` | Auth fails.                                                                                                                                                                                                                                                                                                                                           |
| `BRAND_SCOPE_MISMATCH`                                            | API key brand differs from the resource brand.                                                                                                                                                                                                                                                                                                        |
| `INVALID_REQUEST`                                                 | Body fails Zod validation — typically an unknown field (the removed `provider` on `brew.automations.triggers.create`, `metadata` on `brew.automations.triggers.fire` / `brew.automations.test`, or `emails[]` on `brew.emails.send`), or trying to send the removed `prompt` field on `brew.automations.triggers.create` / `brew.automations.create`. |
| `PAYLOAD_SCHEMA_EMAIL_REQUIRED`                                   | `payloadSchema.fields` is missing the required email field.                                                                                                                                                                                                                                                                                           |
| `TRIGGER_EVENT_NOT_FOUND`                                         | The supplied `triggerEventId` doesn't exist in the brand.                                                                                                                                                                                                                                                                                             |
| `TRIGGER_IMMUTABLE`                                               | Tried to PATCH an integration trigger.                                                                                                                                                                                                                                                                                                                |
| `TRIGGER_HAS_DEPENDENT_AUTOMATIONS`                               | DELETE refused — detach automations first.                                                                                                                                                                                                                                                                                                            |
| `NO_PUBLISHED_AUTOMATION`                                         | `brew.automations.triggers.fire(...)` matched a trigger but no bound automation has `published: true`. Triggers are always-on; whether they fire emails is gated entirely on the bound automation's publish state. Publish at least one bound automation and retry.                                                                                   |
| `AUTOMATION_NOT_FOUND`                                            | Unknown `automationId` in this brand.                                                                                                                                                                                                                                                                                                                 |
| `AUTOMATION_NOT_PUBLISHED`                                        | Tried to unpublish an automation that's not live.                                                                                                                                                                                                                                                                                                     |
| `PUBLISH_VALIDATION_FAILED`                                       | Publish blocked — the message describes the blocker.                                                                                                                                                                                                                                                                                                  |
| `AUTOMATION_RUN_NOT_FOUND`                                        | Unknown `automationRunId` in this brand.                                                                                                                                                                                                                                                                                                              |
| `IDEMPOTENCY_CONFLICT`                                            | Same idempotency key reused with a different request body.                                                                                                                                                                                                                                                                                            |
| `RATE_LIMITED`                                                    | Window exhausted — back off using `Retry-After`.                                                                                                                                                                                                                                                                                                      |

## Determinism vs AI authoring — what lives where

The public SDK / HTTP surface is intentionally deterministic for
graph authoring. AI is scoped to **email body content** only via
`brew.emails.generate({ prompt })`.

| Layer                                                       | AI involved?                                                                                           |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Trigger payload schema (`brew.automations.triggers.create`) | Never — caller supplies explicit `{ key, type, required }` fields.                                     |
| Automation graph (`brew.automations.create / patch`)        | Never — caller supplies explicit `{ nodes, connections }`.                                             |
| Email body (`brew.emails.generate`)                         | Yes — the email subagent renders HTML from the prompt.                                                 |
| Subject / preview line (`sendEmail.config.subject`)         | Optional `{{variable \| fallback}}` interpolation against the trigger payload, evaluated at fire time. |

If you need free-form natural-language authoring of the full graph,
use the chat-side orchestrator (`/chat` in the dashboard) — that
surface still exposes the `routeToAutomationAgent` /
`createTriggerEvent` tools internally.

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