Skip to content
Docs

GuideAlpha

Build responsive templates and campaigns with MJML, an alpha open to the Emailit team. The Visual and Code editors, stored documents, validation, Temple in MJML, live collaboration and the MJML API.

Updated Oct 7, 2026

MJML is a markup language for responsive email. You write sections, columns and components such as <mj-text> and <mj-button>, and MJML compiles them to HTML that renders consistently across mail clients. In Emailit, MJML can be the source of a template or campaign: you write it in the dashboard editors or send it through the API, and Emailit validates it and compiles the HTML.

Overview

Who can use MJML

During the alpha, MJML is available to Emailit platform admins only. A workspace Admin role isn’t enough.

Where Emailit team Everyone else
Dashboard MJML editors, MJML import, Edit with AI and live collaboration No MJML editors. An MJML template or campaign shows a note, can’t be opened in an editor, and still sends.
API MJML templates, MJML campaigns and the MJML endpoints 403 with error: "mjml_alpha". A campaign’s content_type: "mjml" stays a plain label, as before the alpha. See Campaigns.
API keys None. API keys belong to a workspace, not to a person. 403 with error: "mjml_alpha"
MCP server The MJML tools and the MJML parameters of the template and campaign tools Not listed

Everyone can still rename, publish, export, send and delete MJML templates and campaigns. Duplicating an MJML template creates a new MJML template, so it needs MJML access.

MJML version

Emailit compiles all MJML with MJML 5.4.1, both on the server and in the editors’ live preview. Validation checks tags, attributes and attribute values against that version. Retrieve the MJML reference returns the version and every component and attribute it supports.

Editors

The dashboard has two MJML editors. Both are versioned and both are 0.x alphas.

Editor ID Version Description
MJML Visual Editor mjml-visual 0.2.0 (alpha) Drag and drop on the rendered email, covering every MJML component and attribute
MJML Code Editor mjml-code 0.2.0 (alpha) MJML with autocomplete, inline validation and a live desktop and mobile preview

Both editors save a template with editor: "mjml". The stored document records which editor, and which version of it, saved it last. Teammates can edit the same template or campaign at the same time. See Editing together.

Source formats

Wherever Emailit accepts MJML (a template’s source, a campaign’s content and the source field of the MJML endpoints), you can send any of these:

Format Example
MJML markup A string starting with <mjml>. An XML declaration or leading comments are allowed.
MJML JSON MJML’s own JSON format, as an object or a JSON string: { "tagName": "mjml", "attributes": {}, "children": [ … ] }. Ending tags such as mj-text carry their HTML in content.
Emailit MJML document The envelope Emailit stores (below), as an object or a JSON string

Anything else is rejected with document.unrecognized. Sources larger than 2 MB are rejected with document.too-large.

The stored document

Emailit stores MJML in a versioned envelope. It’s the template’s source and the campaign’s content:

JSON
{
  "kind": "emailit/mjml",
  "schema_version": 1,
  "mjml_version": "5.4.1",
  "editor": "api",
  "editor_version": null,
  "format": "markup",
  "content": "<mjml>\n  <mj-body>\n    …\n  </mj-body>\n</mjml>"
}
Field Description
kind Always emailit/mjml.
schema_version Version of the envelope shape. Currently 1.
mjml_version The MJML release the content targets. Emailit sets it to the version it compiled with.
editor What last wrote the document: mjml-visual, mjml-code, ai (Edit with AI) or api (the API, the MCP tools and file imports).
editor_version Version of that editor, or null.
format markup: content is MJML markup, kept as written, comments and formatting included. json: content is MJML JSON.
content The MJML.

What you send decides the format: markup is stored as markup and MJML JSON as json. Both are recorded as editor: "api". An envelope you send keeps its editor and editor_version.

API responses return source as this envelope, serialized to a JSON string, and you can send it back unchanged. Responses also include an mjml object with the envelope’s versions:

JSON
"mjml": {
  "mjml_version": "5.4.1",
  "schema_version": 1,
  "editor": "api",
  "editor_version": null,
  "format": "markup"
}

The dashboard opens a document in the editor that saved it last. Documents written through the API open in the Code Editor when format is markup and in the Visual Editor when it’s json.

Compile and save

For MJML templates and campaigns, Emailit owns the HTML:

  • On create and update, Emailit validates the MJML and compiles it. The compiled HTML is stored as the template’s html, and any html you send is ignored.
  • Sending uses the stored HTML. Temple tags stay in it and are rendered for each recipient at send time.
  • Updating other fields only, such as name or subject, doesn’t recompile.
  • text isn’t generated from the MJML. Send text yourself if you want a plain-text part.
  • Switching an existing template to editor: "mjml" without sending source compiles the template’s stored source, which must then be MJML.

The HTML is compiled when you save, so an existing template’s HTML changes only when it’s saved again.

Terminal
curl https://api.emailit.com/v2/templates \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Welcome",
    "alias": "welcome",
    "subject": "Welcome, {{first_name|\"there\"}}",
    "editor": "mjml",
    "source": "<mjml><mj-head><mj-title>Welcome</mj-title><mj-preview>Your account is ready</mj-preview></mj-head><mj-body><mj-section><mj-column><mj-text>Hi {{first_name|\"there\"}}, welcome aboard.</mj-text><mj-button href=\"{{activation_url}}\">Activate account</mj-button></mj-column></mj-section></mj-body></mjml>"
  }'

Then send it like any other template with Send an email: "template": "welcome" and a variables object.

Validation

Every save runs the same checks as Validate MJML:

  • XML syntax: unclosed or mismatched tags and broken attributes (xml.*)
  • MJML structure, attributes and attribute values for MJML 5.4.1 (mjml.*)
  • Temple syntax and balanced {{#if}} blocks (temple.*)
  • The source format and versions (document.*), and the compiler itself (compiler.*)

Each finding is a diagnostic with a severity:

Severity Effect
error The MJML is rejected. Templates and campaigns aren’t saved.
warning Saved. Probably a mistake: no <mj-title>, text outside a component, a conditional block that crosses components, or HTML over Gmail’s 102 KB clipping limit.
info Saved. A suggestion or a note: no <mj-preview>, an image without alt, or a document written for an older MJML version.

A diagnostic has these fields. Fields that don’t apply are left out.

Field Description
severity error, warning or info
code A stable machine-readable code, for example mjml.invalid-child
message A readable explanation, often with a fix (“Did you mean color?”)
line, column 1-based position in the markup. Markup sources only.
tag The element the diagnostic is about
attribute The attribute, when there is one
path Child-index path from the <mjml> root. [0, 1] is the second child of the first child.

Error response

A template or campaign save with error diagnostics returns 422. For this source:

XML
<mjml>
  <mj-body>
    <mj-section>
      <mj-column>
        <mj-text colour="#333333">Hi {{first_name|"there"}}</mj-text>
        <mj-button href="{{cta_url}}">{{#if trial}}Start trial</mj-button>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

the response is:

JSON
{
  "message": "The MJML is not valid.",
  "errors": {
    "source": [
      "Line 5: <mj-text> has no attribute colour. Did you mean color?",
      "Line 6: {{#if trial}} is never closed with {{/if}}."
    ]
  },
  "diagnostics": [
    {
      "severity": "error",
      "code": "mjml.unknown-attribute",
      "message": "<mj-text> has no attribute colour. Did you mean color?",
      "line": 5,
      "column": 18,
      "tag": "mj-text",
      "attribute": "colour",
      "path": [0, 0, 0, 0]
    },
    {
      "severity": "warning",
      "code": "mjml.missing-title",
      "message": "Add an <mj-title> to <mj-head>; clients and screen readers use it.",
      "line": 1,
      "column": 2,
      "tag": "mjml",
      "path": []
    },
    {
      "severity": "info",
      "code": "mjml.missing-preview",
      "message": "Add an <mj-preview> to control the inbox preview text.",
      "line": 1,
      "column": 2,
      "tag": "mjml",
      "path": []
    },
    {
      "severity": "error",
      "code": "temple.unclosed-if",
      "message": "{{#if trial}} is never closed with {{/if}}.",
      "line": 6,
      "column": 39,
      "tag": "mj-button",
      "path": [0, 0, 0, 1]
    }
  ]
}
  • errors.source (errors.content for campaigns) lists up to five error messages, with their line when it’s known.
  • diagnostics lists every diagnostic, including warnings and info.
  • A missing or empty source returns 422 with "message": "Validation failed", "errors": { "source": ["The source field is required for MJML."] } and an empty diagnostics array.

Validate MJML runs the same checks without saving and returns 200 with valid: false instead of 422.

Diagnostic codes

XML (markup sources)

Code Severity Meaning
xml.unclosed-tag error An element is never closed.
xml.unexpected-closing-tag error A closing tag matches no open element.
xml.malformed-closing-tag error A closing tag can’t be read.
xml.unterminated-tag error An opening tag has no closing >.
xml.unterminated-attribute error An attribute value has no closing quote.
xml.missing-attribute-value error name= has no value.
xml.invalid-attribute error An unexpected character inside a tag.
xml.duplicate-attribute error The same attribute twice on one element. The first one is used.
xml.unterminated-comment error A comment has no -->.
xml.unterminated-cdata error A CDATA section has no ]]>.
xml.unexpected-character error A bare < outside an ending tag.
xml.multiple-roots error More than one root element.
xml.text-outside-root error Text outside <mjml>.
mjml.missing-root error The document is empty.
xml.unquoted-attribute warning An attribute value without quotes.
xml.stray-text warning Text between elements, outside any content component. MJML ignores it.
xml.unexpected-declaration warning A declaration after <mjml> starts.

MJML

Code Severity Meaning
mjml.unknown-tag error Not an MJML 5.4.1 element, with a “did you mean” suggestion when one is close.
mjml.unknown-attribute error The element has no such attribute. A warning on <mjml> itself.
mjml.invalid-attribute-value error The wrong kind of value: not a color, a unit or an allowed value.
mjml.invalid-child error The element isn’t allowed inside its parent.
mjml.invalid-root error The root element isn’t <mjml>.
mjml.missing-body error No <mj-body>.
mjml.duplicate-body error More than one <mj-body>.
mjml.include-not-supported error <mj-include> isn’t supported.
mjml.missing-attribute error or warning A required attribute is missing. An error for <mj-font> name and href, <mj-class> name, <mj-selector> path and <mj-html-attribute> name. A warning for an image src and <mj-breakpoint> width.
mjml.missing-title warning No <mj-title> in <mj-head>.
mjml.empty-title warning <mj-title> is empty.
mjml.duplicate-head warning More than one <mj-head>.
mjml.ignored-content warning Text inside an element that doesn’t take content.
mjml.ignored-children warning Child elements inside an element that only takes content.
mjml.column-widths warning Column widths in a section or group add up to more than 100%.
mjml.unknown-social-network warning An <mj-social-element> name with no built-in icon and no src.
mjml.script warning <script> in content. Mail clients strip it.
mjml.missing-preview info No <mj-preview>.
mjml.missing-alt info An <mj-image> without alt.
mjml.button-without-link info An <mj-button> without href.

Temple

Code Severity Meaning
temple.unclosed-if error {{#if}} without {{/if}}.
temple.endif-without-if error {{/if}} without {{#if}}.
temple.else-without-if error {{else}} outside a block.
temple.duplicate-else error Two {{else}} in one block.
temple.unclosed-expression error {{ without }}.
temple.empty-expression error {{ }}.
temple.empty-condition error {{#if}} without a variable.
temple.malformed-else error {{else}} written with spaces or arguments.
temple.unsupported-block error A block other than {{#if}}, such as {{#each}}.
temple.unsupported-syntax error Triple braces {{{…}}}, partials {{> …}} or comments {{! …}}.
temple.invalid-variable warning A variable that isn’t a valid path.
temple.invalid-condition warning A condition that isn’t a variable path. Comparisons aren’t supported.
temple.block-crosses-components warning A block that opens in one component and closes in another.

Document and compiler

Code Severity Meaning
document.empty error The source is empty.
document.unrecognized error Not MJML markup, MJML JSON or an Emailit MJML document.
document.invalid-json error The source looks like JSON but doesn’t parse.
document.invalid-node error MJML JSON with a malformed node.
document.too-large error The source is larger than 2 MB.
document.unsupported-schema error The envelope’s schema_version is newer than Emailit reads.
document.unsupported-mjml-version error The document’s MJML version can’t be compiled. See Versions and upgrades.
document.assumed-mjml-version info The envelope has no mjml_version, so the current version is assumed.
document.mjml-upgraded info Written for another MJML version and compiled with 5.4.1.
compiler.failed error MJML couldn’t render the document.
compiler.gmail-clipping warning The HTML is larger than 102 KB, so Gmail clips it.

Temple in MJML

Temple tags pass through MJML compilation unchanged. Emailit renders them for each recipient at send time, on the compiled HTML.

Variables in content and attributes

Variables work in content and in any attribute:

XML
<mj-text>Hi {{first_name|"there"}},</mj-text>
<mj-button href="{{activation_url}}">Activate your account</mj-button>
<mj-image src="{{logo_url}}" alt="{{company|'Acme'}}" />
<mj-section background-color="{{brand_color|'#ffffff'}}">
  • Inside an attribute, write defaults with single quotes: href="{{url|'https://example.com'}}".
  • Attribute values that contain Temple aren’t type-checked, because the value is only known at send time. Make sure the variable holds a value that’s valid for the attribute, such as a color for background-color.
  • Values are inserted as they are, without HTML escaping.

Conditional blocks

Inside one component, put the block in its content:

XML
<mj-text>{{#if plan}}You are on the {{plan}} plan.{{else}}You are on the free plan.{{/if}}</mj-text>

To show or hide whole components, put the block tags in <mj-raw> siblings:

XML
<mj-raw>{{#if vip}}</mj-raw>
<mj-section background-color="#fef3c7">
  <mj-column>
    <mj-text>Your VIP perks are ready.</mj-text>
  </mj-column>
</mj-section>
<mj-raw>{{/if}}</mj-raw>

Bare block tags between components are converted to <mj-raw> when the markup is parsed, so this is the same thing:

XML
{{#if vip}}
<mj-section background-color="#fef3c7">
  …
</mj-section>
{{/if}}

Other text between components is ignored by MJML and reported as xml.stray-text.

  • Blocks must balance across the whole document. An unclosed or extra tag is an error.
  • Open and close each block inside one component’s content, or among the <mj-raw> siblings of one parent. A block that opens in one component and closes in another gets a temple.block-crosses-components warning, because hiding it would cut through the HTML structure.
  • Blocks can be nested.

Not supported

  • <mj-include> is rejected with mjml.include-not-supported. Paste the included MJML into the document.
  • Tags and attributes that MJML 5.4.1 doesn’t define are errors.
  • Temple has no loops, helpers, partials, comments, triple braces or comparisons. See Temple.

Variables by channel

The same MJML template can be sent from several places, and each one provides different variables:

Sent from Variables
API Send an email with template The variables object you pass
Automation Send email step Contact automations: the contact’s fields at the top level ({{first_name}}, {{email}}), custom fields as {{cf.<key>}} or {{custom_fields.<key>}}, plus {{contact.*}}, {{payload.*}} and {{meta.*}}
MJML campaigns {{first_name}}, {{last_name}}, {{email}}, {{unsubscribe_url}}, {{cf.<key>}}, and the same fields under {{contact.*}}

The editors insert custom fields as {{cf.<key>}}, which works in MJML campaigns and in automations. For API sends, pass the variables yourself.

To preview a recipient’s version, call Render MJML with variables.

Campaigns

A campaign with content_type: "mjml" stores its MJML in content, in any of the source formats, and Emailit compiles its html. As with templates, any html you send is ignored. Invalid MJML returns 422 with errors.content and diagnostics. Sending an empty content clears both the content and the HTML. Campaign responses include the same mjml object as templates.

MJML campaigns render the subject, HTML and text with Temple for each recipient, on test sends too. These variables are available:

Variable Value
{{first_name}} Contact first name
{{last_name}} Contact last name
{{email}} Contact email address
{{unsubscribe_url}} Unsubscribe link for this contact and campaign
{{cf.<key>}} Contact custom field, for example {{cf.company}}
{{contact.first_name}}, {{contact.cf.<key>}}, … The same fields under contact

Empty contact fields count as missing, so defaults apply: {{first_name|"there"}} renders there for a contact without a first name. Keep a {{unsubscribe_url}} link in the footer of marketing email.

Classic campaigns (HTML, text and the other editors) keep the fixed merge tags:

Classic campaigns MJML campaigns
Engine Fixed merge tags Temple
{{#if}} … {{else}} … {{/if}} Not processed Supported
Defaults such as {{first_name|"there"}} Not processed Supported. Empty fields count as missing.
Letter case {{FIRST_NAME}} works Paths are case-sensitive
Unknown tags Left in the message as written Rendered as empty

In the dashboard, starting a campaign from an MJML template copies the template’s MJML document into the campaign.

Without MJML access

During the alpha, Emailit compiles campaign MJML only for the Emailit team. For everyone else, including API keys, content_type: "mjml" stays the plain label it was before the alpha: content is stored as you send it, you send the compiled HTML in html, and sends use the classic merge tags. Starting a campaign from an MJML template copies the template’s HTML into an HTML campaign.

Automations

The Send email step references a template by ID (tem_…). MJML templates work like any other: the step sends the template’s compiled HTML and renders Temple with the automation’s variables. See Automation emails.

In the step settings, Design a new email creates an MJML template from a starter design, selects it for the step and opens it in the Visual Editor. Edit email opens the selected MJML template. Edits change the template itself, so every step and API call that uses the template gets them. Without MJML access, the step shows a link to the template instead.

Editing together

Everyone who opens the same saved MJML template or campaign edits one shared draft in real time, in either editor:

  • Presence: the header shows who else is editing and what they’re doing. In the Visual Editor you see their selections, their cursors and a short highlight in their color where they change something. Layers shows who has a component selected. Select someone’s avatar to jump to their selection.
  • Edits merge: changes to different components, attributes or parts of a text combine instead of overwriting each other. While someone types into a text on the canvas, that text is locked for others.
  • Code Editor: your changes merge into the shared draft as you type. Changes from others appear in your code once you pause typing, so your cursor doesn’t jump. If your code has a syntax error, they wait until you fix it.
  • Undo and redo only undo your own changes.
  • Saving: there’s one Save for everyone. The header shows unsaved changes for the whole draft, and who saved last. Sending always uses the saved version.
  • The draft is kept: closing the editor or losing the connection doesn’t lose changes. They stay in the shared draft and sync when you’re back online. Reopening the editor restores unsaved changes, and you can discard them to go back to the saved version.
  • Saved elsewhere: when the template or campaign is saved outside the editor (the API, MCP or Edit with AI) while it’s open, a draft without unsaved changes switches to the saved version. A draft with unsaved changes keeps them and offers Load saved version or Keep this draft.
  • Deleted: if the template or campaign is deleted, or is no longer MJML, while you edit, the editor tells you and lets you copy the MJML.

Editing together needs saved, valid MJML. A template or campaign whose MJML doesn’t parse, or one that isn’t saved as MJML yet, opens without it: each person edits alone and the last save wins. The editor says so in a banner. In a suspended workspace the editors are read-only.

Versions and upgrades

Emailit compiles with one MJML version at a time, currently 5.4.1. Each stored document records the mjml_version it targets, and Emailit checks it whenever the document is compiled:

Document mjml_version Result
5.4.1 Compiled as is
Missing 5.4.1 is assumed (document.assumed-mjml-version, info)
Another 5.x release Compiled with 5.4.1 (document.mjml-upgraded, info)
4.x Migrated to MJML 5, then compiled with 5.4.1 (document.mjml-upgraded, info). MJML 4 and 5 share the same components and attributes; the HTML output differs slightly.
3.x or older Rejected with document.unsupported-mjml-version
A newer major version Rejected with document.unsupported-mjml-version

A document whose schema_version is newer than Emailit reads is rejected with document.unsupported-schema. Saving stores the document with the current mjml_version and schema_version. Check the preview after an upgrade.

The editors show their version and changelog. When a document was saved with a newer editor version than the page you have open, the editor asks you to reload.

MJML for AI agents

Retrieve the MJML reference gives tooling and AI models what they need to write valid MJML for Emailit: every component with its allowed parents, children and attributes (type and default), the Temple guide, authoring rules and a compact plain-text reference_text for prompts.

On the MCP server, sessions of the Emailit team also get these tools in the templates toolset:

Tool Description
get-mjml-reference The reference as text: the MJML and editor versions, authoring rules, the Temple guide and the component reference.
validate-mjml Validate MJML: valid and the diagnostics.
render-mjml Render MJML: the compiled HTML and, with variables, rendered_html.
create-template, update-template Also take editor: "mjml" and source, the MJML markup or MJML JSON as a string. Emailit compiles the HTML.
create-campaign, update-campaign Also take content_type: "mjml" with the MJML in content.

When a save fails validation, the tool error lists the error and warning diagnostics with their line numbers, so the agent can fix the source. A typical flow: read the reference, write the MJML, call validate-mjml until valid is true, save with create-template, then check a recipient’s version with render-mjml. Other sessions don’t see these tools or parameters.

In the dashboard

  • Templates: create a template and choose MJML Visual Editor (Alpha) or MJML Code Editor (Alpha).
  • Visual Editor: drag and drop on the rendered email, a layers tree, a property panel for every MJML attribute, document settings (head, fonts, styles and default attributes), Temple variables in any property and conditional blocks around components.
  • Code Editor: autocomplete for MJML tags, attributes, values and Temple, inline validation with quick fixes, and formatting.
  • Both editors: a live desktop and mobile preview compiled in the browser with MJML 5.4.1, a preview with sample data (Temple rendered), a problems list and an AI assistant when it’s enabled. You can switch between Visual and Code on the same document; switching to Visual needs code without syntax errors. Saving is blocked while the MJML has errors.
  • Editing together: teammates who open the same template or campaign edit it in real time. See Editing together.
  • Edit with AI: describe a change to an MJML template or campaign without opening the editor.
  • Import: an .mjml file, an MJML JSON or Emailit MJML document .json file, or a ZIP with template.mjml and an images/ folder at the root.
  • Export: MJML (markup), MJML JSON (the stored document), HTML, or a ZIP with template.mjml, template.html and images/. Export works for everyone.
  • Campaigns: choose the MJML Visual or Code Editor for the campaign content.
  • Automations: design the email of a Send email step in place. See Automations.

Was this page helpful?

Thanks for the feedback.

Thanks, we read every message.