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

# Compare MJML with other email tools

> How allGood's MJML components and copy schema map to Marketo templates, HubSpot modules, and hand-coded email HTML.

If you already build email templates in Marketo, HubSpot, or plain HTML, you know the problem allGood's email tooling solves. A reusable shell leaves the right parts open for a marketer to fill in. The difference is the model. allGood builds the shell from MJML components instead of annotated or hand-written HTML.

This article lines the tools up side by side. It assumes you have read [How MJML works](https://docs.allgoodhq.com/mk/developer/email/how-mjml-works.md) and know what an [email component](https://docs.allgoodhq.com/mk/content/email/components/index.md) is.

## How each tool builds a template

**Marketo** uses one HTML document, annotated in place. You write real table markup and mark it with Marketo classes, so the editor knows which parts a marketer can change. A template can hold only one `mktoContainer`, and a container can hold only `mktoModule` elements. Editable content inside a module, such as `mktoText` or `mktoImg`, needs a unique `id` and a `mktoName`.

```html theme={null}
<table class="mktoContainer" id="container" mktoName="Container">
  <tr class="mktoModule" id="textModule" mktoName="Open Text Area">
    <td>
      <div class="mktoText" id="mainTextArea" mktoName="Main Text Area">
        <p>Lorem ipsum dolor set amet</p>
      </div>
    </td>
  </tr>
</table>
```

**HubSpot** uses a module, which is a small bundle of files. `module.html` holds the markup, `fields.json` holds the editable fields, and `meta.json` holds the label and icon. You cite a field in the markup with `{{ module.headline }}`.

**Hand-coded email** is one HTML file with no schema and no build step. Whatever you write is close to what ships. Personalization is whatever your sending tool's merge tags provide, such as `*|FNAME|*`.

**allGood** uses a bundle of two files. `template.mjml` is a complete MJML document built from components. `template.json` declares a `copySchema`, which lists the fields a marketer fills in. You cite a field inline, with no wrapper element or separate name:

```xml theme={null}
<mj-text>{{ asset.copy.content.headline.value }}</mj-text>
<mj-button href="{{ asset.copy.content.ctaUrl.value }}">
  {{ asset.copy.content.ctaLabel.value }}
</mj-button>
```

An allGood Email Component, one level down, is a single `.mjml` file. Its editable copy is real placeholder text, marked with an `EDIT` or `LOCKED` comment. It has no schema file. The template that composes components adds the schema.

HubL and allGood's Liquid share a Jinja-style syntax, with `{{ }}` expressions, `{% if %}` blocks, and `|` filters. If you build in HubSpot, the citations will look familiar. Marketo's `${variable_id}` and Velocity come from a different lineage.

## Building blocks

Marketo's structure vocabulary is two words, Container and Module. HubSpot and hand-coded email have no fixed set at all, because the markup is plain HTML from the first line. MJML has a closed set of about thirty tags and one escape hatch, `mj-raw`.

| MJML tag | Marketo | HubSpot | Hand-coded |
| - | - | - | - |
| `mj-section` | A row inside a `mktoContainer` | A row you build, or a drag-and-drop layout section | A nested `<table role="presentation">` |
| `mj-column` | A slice of a row. Marketo forbids side-by-side modules. | A column that you reflow with CSS | A table cell with fluid-hybrid sizing |
| `mj-text` | `mktoText` | Plain HTML text, styled by hand | Plain HTML text with inline styles |
| `mj-image` | `mktoImg` | An `<img>` tag, styled by hand | An `<img>` tag with inline styles |
| `mj-button` | A styled link. Marketo has no button, so you hand-code the table. | Hand-coded, or a HubSpot CTA object | A table button with a VML fallback for Outlook |
| `mj-raw` | Not needed. The whole template is raw HTML. | Not needed. `module.html` is unrestricted. | Not needed |

The practical difference is where the Outlook work happens. In Marketo, HubSpot, and hand-coded email, getting a two-column layout or a rounded button to survive Outlook is your job in every template. Marketo's own advice is to "code like it's 1999." In allGood, the components compile to the markup Outlook needs. The only place you take that responsibility back is `mj-raw`. See [What the compiler handles for you](https://docs.allgoodhq.com/mk/developer/email/how-mjml-works.md#what-the-compiler-handles-for-you).

MJML sections and columns also nest freely and sit side by side by default. There is no limit like Marketo's single container.

## Declaring what varies

Every tool needs a way to say which parts of a template change. Here is how the declarations compare.

| | Marketo | HubSpot | Hand-coded | allGood |
| - | - | - | - | - |
| Where you declare it | A `<meta>` tag in the `<head>`, one per variable | `fields.json`, next to `module.html` | Nowhere. Merge tags are untyped text. | `copySchema` in `template.json` |
| How it is typed | By class: `mktoColor`, `mktoNumber`, `mktoBoolean`, `mktoList` | By field `type`: `text`, `richtext`, `image`, `boolean`, `choice`, `color`, `group` | Not typed | JSON Schema `type`: `string`, `array`, `boolean`, `number` |
| How you cite it | `${variable_id}` | `{{ module.field_name }}` | `*\|FNAME\|*` or `{{first_name}}` | `{{ asset.copy.content.<field>.value }}` |
| Scope | One module, or global with `mktoModuleScope` | One module | Not applicable | One template, which composes several components |
| Default value | A `default` attribute | A `default` key | None | A `default` key |

Three things to know when you switch:

* **Type mapping.** HubSpot has more field types than JSON Schema. A `color` field becomes a `string` with a hex value. A repeatable `group` becomes an `array`. Describe the extra detail in the property's `title` and `description`.
* **Separate files.** Marketo variables live inside the HTML you are editing. An allGood schema is a separate file, and nothing merges it with `template.mjml`. You keep the two in sync by hand. Every property must be cited somewhere, and every citation needs a matching property. An orphan on either side renders blank or does nothing, and no error appears until validation catches it.
* **Typed fields help the AI.** A property's `title` and `description` do more than label a form. allGood's AI reads them to learn what the field is for. For example, a `headline` has to earn an open, and a `ctaLabel` has to read as an action. A Marketo `mktoName` or a bare merge tag cannot carry that meaning. See [Decide what writers fill in](https://docs.allgoodhq.com/mk/content/email/templates/writing-the-copy-schema.md).

## Who renders the HTML

In Marketo, HubSpot, and hand-coded email, the HTML you write is what gets rendered. HubSpot renders `module.html` and your field values together in one pass. Marketo reads your annotated markup directly. In hand-coded email, nothing sits between your file and the recipient's inbox, so you test by sending yourself emails.

In allGood, the MJML is never what gets sent. A render script compiles `template.mjml` into `template.html` first, with no personalization involved. Liquid citations are filled in afterward, at send time. A HubSpot module author never sees this split, but an allGood template author always does. See [The compile step in allGood](https://docs.allgoodhq.com/mk/developer/email/how-mjml-works.md#the-compile-step-in-allgood).

## What happens when a template changes

| Tool | What happens to emails already built |
| - | - |
| Marketo | Nothing. Editing a template does not change emails built from it, and nothing warns you. The link is a one-time copy. |
| HubSpot | Depends on the module type. A Global content module has one shared copy, so an edit reaches every placement. A standard module instance keeps its own field values. |
| Hand-coded | You find every email that used the pattern and edit each one. Nothing tracks which version a send used. |
| allGood | Every placement of a component shares one published version. A template and an email built on a library appear under the library's **Assets** tab, so you see an edit's reach before you publish it. |

allGood also records history for every version and every publish, and you can restore an earlier version directly. An email built from a template keeps a `derived_from` link to the version it was built from, and the system flags when the template has moved on. See [Setting up and reviewing email components](https://docs.allgoodhq.com/mk/content/email/components/setting-up-and-reviewing-email-components.md).

Because every placement shares the same version, allGood has no equivalent of a standard HubSpot module instance with its own values. If one send needs different wording, use the template's `copySchema`. It layers on top of the component and does not fork it.

## Move your existing work over

You do not have to retype anything. allGood's AI can convert an existing email into MJML. That includes a Marketo template's `mktoContainer` and `mktoModule` markup, a HubSpot `module.html`, and a hand-coded HTML email. Conversion turns VML fallbacks and conditional comments into the tags that already handle them. Once the MJML exists, the schema, citations, and render step work the same way.

> [!NOTE]
>
> Treat converted output as a draft. A conversion maps markup mechanically. It will not know that a module your team stopped using should be dropped, or that two near-duplicate modules should become one component.

## Quick map

| In your current tool | In allGood | | |
| - | - | - | - |
| `mktoContainer` and `mktoModule` | `mj-section` and `mj-column` | | |
| `mktoText`, `mktoImg`, `mktoSnippet` | A Liquid citation inline in any component | | |
| `<meta>` variable with `${variable_id}` | `copySchema` property with `{{ asset.copy.content.*.value }}` | | |
| `module.html` | A component's `.mjml` file | | |
| `fields.json` field | `copySchema` property on the template | | |
| Global content module | A published Email Component in your library | | |
| Hand-coded Outlook-safe tables, VML, and conditional comments | The MJML component set | | |
| Raw HTML you are fully responsible for | `mj-raw` | | |
| Merge tag such as \`\* | FNAME | \*\` | A typed `copySchema` property with a `default` |
| A template edit that reaches no one | Drift detection through `derived_from` | | |
| Finding every email that used a pattern | The **Assets** tab's Used by list | | |

## Related articles

* [How MJML works](https://docs.allgoodhq.com/mk/developer/email/how-mjml-works.md)
* [Write an email component](https://docs.allgoodhq.com/mk/developer/email/how-to-write-email-components.md)
* [Decide what writers fill in](https://docs.allgoodhq.com/mk/content/email/templates/writing-the-copy-schema.md)
* [Email components](https://docs.allgoodhq.com/mk/content/email/components/index.md)
* [Coming from Marketo or HubSpot: email](https://docs.allgoodhq.com/mk/content/email/coming-from-marketo-hubspot.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.