GuideAlpha
MJML editors and API
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.
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
- Templates:
editor: "mjml"with the MJML insource. See Create a template. - Campaigns:
content_type: "mjml"with the MJML incontent. See Campaigns. - Automations: the Send email step sends an MJML template like any other template. See Automations.
- MJML endpoints: Validate MJML, Render MJML and Retrieve the MJML reference.
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:
{
"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:
"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 anyhtmlyou 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
nameorsubject, doesn’t recompile. textisn’t generated from the MJML. Sendtextyourself if you want a plain-text part.- Switching an existing template to
editor: "mjml"without sendingsourcecompiles the template’s storedsource, 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.
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:
<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:
{
"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.contentfor campaigns) lists up to five error messages, with their line when it’s known.diagnosticslists every diagnostic, including warnings and info.- A missing or empty source returns
422with"message": "Validation failed","errors": { "source": ["The source field is required for MJML."] }and an emptydiagnosticsarray.
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:
<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:
<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:
<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:
{{#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 atemple.block-crosses-componentswarning, because hiding it would cut through the HTML structure. - Blocks can be nested.
Not supported
<mj-include>is rejected withmjml.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
.mjmlfile, an MJML JSON or Emailit MJML document.jsonfile, or a ZIP withtemplate.mjmland animages/folder at the root. - Export: MJML (markup), MJML JSON (the stored document), HTML, or a ZIP with
template.mjml,template.htmlandimages/. 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.