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

# Migrate From Loops to Brew

> Move contacts, campaigns, and workflows from Loops into Brew, with the API swap, a cutover that avoids double-sends, and what stays behind for now.

Loops customers are usually more code-integrated than a typical marketing-tool switch. Events and per-recipient sends are often wired straight into application code through the API.
Campaigns and workflows get built separately in the dashboard.

One thing to know before you start: **Brew's transactional email isn't generally available yet.** Password resets, magic links, 2FA codes, and receipts stay in Loops for now. This guide migrates the marketing half, campaigns and workflows, and Step 7 covers what to do with the transactional code.

It assumes your Brew brand and sending domain are already set up. If not, [verify your domain](/get-started/verify-your-sending-domain) first. DNS can take up to 72 hours.

Rule for the whole migration: **only one platform sends live email to a given contact at a time.**

## Concepts

| Loops                              | Brew                                                                                |
| ---------------------------------- | ----------------------------------------------------------------------------------- |
| Contact                            | Contact                                                                             |
| Contact property                   | Custom property                                                                     |
| Mailing list                       | Custom property plus an Audience filter, see Step 4                                 |
| Filter / Segment                   | [Audience](/audience/create-audiences) (dynamic filter)                             |
| Campaign                           | [Email](/create-emails/emails)                                                      |
| Workflow                           | [Automation](/create-emails/automations)                                            |
| Transactional email                | Not mappable yet: Brew's transactional email isn't GA. Keep it in Loops, see Step 7 |
| Event                              | Trigger event (custom HTTP trigger or integration event)                            |
| `{tag}`, `{EVENT_PROPERTY:name}`   | `{{ property \| fallback }}`                                                        |
| `transactionalId`, `dataVariables` | No Brew equivalent yet, stays in your Loops code, see Step 7                        |

## 1. Export Contacts From Loops

<Warning>
  Every marketing recipient must have opted in to hear from you. Importing or syncing contact data into Brew is not permission to email it. See [What You Can Send](/get-started/what-you-can-send).
</Warning>

Audience page → filter to a segment if you want a subset → **Export**. Loops writes one CSV of whatever's in view, with columns for default and custom properties only. There's no separate export per subscription status, so filter by `Subscribed` first if you only want active contacts.

Carries over cleanly: email, name, source, custom properties, and `subscribed` (true/false).

**Mailing list membership doesn't carry over automatically.** Loops' export includes contact properties, not list membership. A straight export and import won't preserve which lists someone belonged to.

If that matters, filter to each list and export it separately, then reconstruct it in Step 4.

Event history and workflow enrollment state don't carry over either. Those live in Loops with no Brew equivalent to import into.

In Loops, unsubscribing blocks campaigns and workflows. Brew's marketing sends respect the same flag, so the behavior your subscribed contacts expect doesn't change.

## 2. Map Your CSV

| Column                        | Required                                |
| ----------------------------- | --------------------------------------- |
| `Email`                       | Yes                                     |
| `First Name`, `Last Name`     | Recommended                             |
| `Subscribed` (`true`/`false`) | No, defaults `true`                     |
| Anything else                 | Becomes a custom property automatically |

Notes:

* `userGroup` and any custom properties become Brew custom properties. Bring `userId` too if anything downstream matches on it
* These names are reserved and can't be used for custom properties: `email`, `first_name`, `last_name`, `subscribed`, `created_at`, `updated_at`, `validation_status`, `last_validated_at`, `import_id`
* Mailing list membership needs its own export per list (see Step 1), then its own import into the matching property. It won't ride along with a general contact import

**Personalization.** Loops uses single curly braces. `{firstName}` works in campaigns and workflows, and `{EVENT_PROPERTY:name}` in workflow emails triggered by events.

The first becomes Brew's `{{ firstName | there }}`. The property name is case-sensitive, and the fallback shows when the value is missing.

The second becomes a trigger payload reference, written the same way: `{{ orderId }}`. See [merge tags](/create-emails/merge-tags).

`{DATA_VARIABLE:name}` lives only in Loops transactional emails, which stay in Loops for now.

One difference worth knowing: in Loops, a missing property value with no fallback means the email doesn't send. In Brew the fallback lives inside the tag itself. There's no separate step where you could forget one and have a send fail silently.

## 3. Import Into Brew

Audience → Add contacts → CSV Upload. Map columns, tick [validation](/audience/validate-contacts) if the list hasn't been checked recently, review, then import. Full import behavior is in [Add Contacts](/audience/add-contacts).

## 4. Rebuild Segmentation

**Filters and Segments** map to Brew [Audiences](/audience/create-audiences), dynamic filters over contact properties, email engagement, and subscription status. Describe one in plain English or build the filters by hand.

**Mailing Lists don't have a direct equivalent.** Brew has one subscription state per contact, subscribed or unsubscribed. There are no opt-in categories and no subscriber-facing preference center. Marketing emails and automations respect that flag.

Rebuild each list as a custom property plus an Audience filtered on it. You keep the targeting, but membership is yours to manage.

Contacts can't pick lists for themselves, and unsubscribing opts them out of all marketing email rather than one list. If a Loops list carried a real consent distinction, decide how to honor it before your first send.

## 5. Rebuild Templates

| You have                           | Use in Brew                                                                 |
| ---------------------------------- | --------------------------------------------------------------------------- |
| A Loops campaign or workflow email | Screenshot it and use Upload image to recreate, or prompt Brew directly     |
| Custom MJML                        | No direct MJML import. Rebuild in Brew's editor or upload the rendered HTML |
| A Figma source                     | Connect with Figma                                                          |

Open **Emails** in the sidebar, then use the **Import** tools on the right side of the canvas. See [Importing existing designs](/create-emails/emails#importing-existing-designs).

Loops themes, the shared styling across transactional emails, don't carry over as a concept. Brew's [brand identity](/brand/brand-identity) replaces the need for a manually maintained theme. Set it once and every rebuilt template inherits your logo, colors, fonts, and voice.

## 6. Rebuild Workflows as Automations

| Loops trigger                                                                    | Brew equivalent                                                                                                                                                     |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Contact added (fires on new contacts from an integration, form, or API, not CSV) | Integration event if the source is a native Brew integration such as Stripe, Clerk, or Shopify, otherwise a custom HTTP trigger fired from your own signup endpoint |
| Contact updated                                                                  | Custom HTTP trigger fired from your own update logic                                                                                                                |
| Contact added to list                                                            | Custom HTTP trigger fired when you set the matching property                                                                                                        |
| Event received                                                                   | Custom HTTP trigger, or an integration event if it's already a native Brew integration                                                                              |

Loops has a trick for re-running an existing contact through a workflow. Download a CSV, then
re-upload it with "Trigger workflows" toggled on. That maps to a [manual-audience run](/create-emails/automations#manual-audience) in Brew, the one-off, on-demand option, not a live trigger.

Manual-audience automations are launched with `POST /v1/automations/{automationId}/run` rather than published.

Branches become Filter or Split nodes, and delay steps become Wait nodes. Test-fire before publishing, and leave every rebuilt automation unpublished until cutover. That's what prevents a double-send.

## 7. Swap Your Code Over to Triggers

Your Loops code has two kinds of call, and they migrate differently.

**`sendEvent` calls move to Brew now.** A cart-abandonment or trial-ending email is a marketing send fired per recipient by a custom event. In Brew that's a trigger event plus an automation, and every per-recipient marketing send goes through it.

**`sendTransactionalEmail` calls stay in Loops for now.** Brew's transactional email isn't generally available yet. Leave resets, magic links, and
receipts pointed at Loops until it is. When it ships, those calls will follow the same trigger pattern as everything below.

Set each marketing event up once:

1. `POST /v1/automations/triggers` to create the trigger event, for example "Trial started". Capture the `triggerEventId`. You can also create it in-app from **Trigger events**.
2. `POST /v1/emails` to generate the email design. Capture both the `emailId` and the `emailVersionId`.
3. `GET /v1/domains` for a verified `domainId`.
4. `POST /v1/automations` with a Send Email node wired to that trigger. Each sendEmail node carries `emailId`, `emailVersionId`, `domainId`, `subject`, and `previewText`.
5. `PATCH /v1/automations/{automationId}` with `{ "published": true }`.
6. Replace the Loops `sendEvent` call in your backend with a fire request.

Before, with Loops:

```js theme={null}
await loops.sendEvent({
  email: user.email,
  eventName: "trial_started",
  eventProperties: { plan: "pro" },
});
```

After, with Brew:

```js theme={null}
await fetch(
  `https://brew.new/api/v1/automations/triggers/${process.env.BREW_TRIAL_STARTED_TRIGGER_ID}/fire`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BREW_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `trial-started-${subscription.id}`,
    },
    body: JSON.stringify({
      payload: { email: user.email, plan: "pro" },
    }),
  },
);
```

Set an `Idempotency-Key` on every retried call, the same discipline you used in Loops. Derive it from something stable about the action, like the subscription id above.

A key built from a timestamp changes on every retry, which defeats the point and lets a retry send twice. [Idempotency](/api-reference/api/idempotency) covers the replay window and conflict behavior.

Full request shapes are in the [API reference](/api-reference/api/api-introduction). This is the shape of the migration, not a copy-paste replacement for it.

## 8. Cut Over

Move one email type at a time.

1. Warm up your Brew sending domain before any real volume, most engaged contacts first, ramped over several days. [Gradual send](/create-emails/send-options#gradual-send) does the ramping for you.
2. Import contacts and rebuild templates and automations while Loops keeps sending everything as normal.
3. Cut over your lowest-risk campaign first, a newsletter, to confirm delivery and rendering.
4. Move workflows one at a time, once their triggers are confirmed firing correctly. Watch each one in [analytics](/analytics/reading-analytics) before moving the next.
5. Point signup forms and inbound integrations at Brew once everything upstream is verified.
6. Transactional email stays in Loops, so keep the plan that covers it. Downgrade the rest once the marketing side is confirmed, and revisit when Brew's transactional email is generally available.

Never leave the same email type live in both places at once. That's how a contact gets the same campaign twice.

## Quick Reference

| Task                   | Where in Brew                                      |
| ---------------------- | -------------------------------------------------- |
| Import contacts        | Audience → Add contacts → CSV Upload               |
| Build a segment        | Audience → Create Audience                         |
| Import a template      | Emails → canvas → Import                           |
| Build a flow           | Emails → Automations tab                           |
| Create a trigger event | Trigger events, or `POST /v1/automations/triggers` |
| API keys               | Settings → API                                     |

## 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 explore it further with ChatGPT or Claude.
      </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>
