Skip to main content

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.

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({ emailType, 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:
EntityWhat it isSDK resource
Trigger eventA schema contract (payloadSchema.fields) plus a stable id. Long-lived. Brand-scoped.brew.triggers
AutomationA graph of nodes (trigger, wait, filter, split, sendEmail) that fires on one trigger. Versioned.brew.automations
Automation runOne workflow run started by a fire / test / replay.brew.automationRuns
The full lifecycle: define trigger → mint automation-typed 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.
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.
const { trigger } = await brew.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 automation-typed email body. emailType: 'automation'
//    is required — these rows are referenced by sendEmail nodes and
//    intentionally hidden from the /emails canvas.
const [welcome, dayTwo] = await Promise.all([
  brew.emails.generate({
    prompt: 'Friendly welcome email for new signups — single CTA to the app.',
    emailType: 'automation',
  }),
  brew.emails.generate({
    prompt: 'Day-2 nudge — three getting-started tips, light tone.',
    emailType: 'automation',
  }),
])

// 3. Assemble the automation graph. AutomationNodeInput is a per-kind
//    discriminated union — set node.type and TypeScript narrows
//    node.config automatically.
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,
        // {{variable | fallback}} interpolation against the trigger payload.
        subject: 'Welcome to Brew, {{firstName | there}}!',
      },
    },
    {
      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 },
    },
  ],
  connections: [
    { from: 'trg', to: 'send_welcome' },
    { from: 'send_welcome', to: 'wait_2d' },
    { from: 'wait_2d', to: 'send_day2' },
  ],
})

// 4. Publish.
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:
const TRIGGER_EVENT_ID = 'tri_order_shipped'

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

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,
        subject: 'Order #{{orderId}} is on its way',
      },
    },
  ],
  connections: [{ from: 'trg', to: 'send_confirm' }],
})

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

Recipe 3: Discover existing automation-typed bodies before authoring

brew.emails.list({ emailType: 'automation' }) returns every body already minted for the brand so the agent can reuse instead of re-generating duplicates:
const existing = await brew.emails.list({ emailType: 'automation' })
const reusable = existing.emails.find((e) =>
  e.emailTitle.toLowerCase().includes('welcome')
)

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

Recipe 4: Fire a trigger from your backend

After publishing, your application code calls brew.automationRuns.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.
await brew.automationRuns.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 started 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.automationRuns.test to run an automation with mode: 'test' (no real mail is sent — sendEmail nodes simulate). Returns a real run row you can inspect via brew.automationRuns.get + ?include=logs.
const test = await brew.automationRuns.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 detail = await brew.automationRuns.get({
  automationRunId: test.automationRunIds[0]!,
  include: ['logs'],
})

const run = detail.runs[0]
console.log('Status:', run?.status)
for (const log of detail.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 publish a new version of the graph deterministically. The previously-published row stays live until you publish the new version explicitly.
const { automations } = await brew.automations.get({ automationId: 'auto_abc' })
const latest = automations[0]!

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

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

await brew.automations.publish({
  automationId: 'auto_abc',
  automationVersionId: nextVersion.automationVersionId,
})

Recipe 7: Dry-run validate before publishing

Pass dryRun: true on create (or patch) to get the same validateAutomationForPublish result the publish flow runs — without touching Convex:
const dryRun = await brew.automations.create({
  name: 'Welcome',
  triggerEventId: 'tri_signup',
  nodes: candidateNodes,
  connections: candidateConnections,
  dryRun: true,
})

if ('valid' in dryRun && !dryRun.valid) {
  for (const blocker of dryRun.blockers) {
    console.log(`Blocker: ${blocker.message}`)
  }
}

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.
CodeWhen
AUTHENTICATION_REQUIRED / INVALID_API_KEY / API_KEY_REVOKEDAuth fails.
BRAND_SCOPE_MISMATCHAPI key brand differs from the resource brand.
INVALID_REQUESTBody fails Zod validation — typically a missing emailType on brew.emails.generate, an unknown field (including the removed provider on brew.triggers.create, metadata on brew.automationRuns.fire/.test, or emails[] on brew.sends.create), or trying to send the removed prompt field on brew.triggers.create / brew.automations.create.
PAYLOAD_SCHEMA_EMAIL_REQUIREDpayloadSchema.fields is missing the required email field.
TRIGGER_EVENT_NOT_FOUNDThe supplied triggerEventId doesn’t exist in the brand.
TRIGGER_IMMUTABLETried to PATCH an integration trigger.
TRIGGER_HAS_DEPENDENT_AUTOMATIONSDELETE refused — detach automations first.
AUTOMATION_NOT_FOUNDUnknown automationId in this brand.
AUTOMATION_NOT_PUBLISHEDTried to unpublish an automation that’s not live.
PUBLISH_VALIDATION_FAILEDPublish blocked — the message describes the blocker.
AUTOMATION_RUN_NOT_FOUNDUnknown automationRunId in this brand.
NOT_IMPLEMENTEDReplay + cancel (currently behind P7).
IDEMPOTENCY_CONFLICTSame idempotency key reused with a different request body.
RATE_LIMITEDWindow 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({ emailType, prompt }).
LayerAI involved?
Trigger payload schema (brew.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:

Search Documentation

Type in the “Ask any question” search bar at the top left to instantly find relevant documentation pages.

ChatGPT/Claude Integration

Click “Open in ChatGPT” at the top right of any page to analyze documentation with ChatGPT or Claude for deeper insights.