The mermaid diagram syntax that stays true across flowchart, sequence, class, and the rest is a handful of rules. The first real line names the diagram, and almost every later error is an id, a quote, a direction you didn't mean, or the reserved word end.
I want those rules in my head so I stop treating each diagram type as a new language. The types differ. The ways they break don't, not really. This is not the one-page cheat sheet. The cheat sheet is the list of shapes and arrows. This page is the judgment around the rules, and the mistakes I still make when I type fast. If the cheat sheet and a type page disagree on a token, believe the type page, then check your renderer version. I'm not going to crown a blog the spec.
What Mermaid is is the orientation if you haven't written a diagram yet. The diagram index is where the type pages live once you know which picture you want. Stay here if the file is failing and you suspect the failure would have happened in any type.
The keyword is the first real line
Blank lines don't count. A comment doesn't count as the keyword either. The first line the parser treats as code is the diagram type: flowchart, sequenceDiagram, classDiagram, erDiagram, stateDiagram-v2, and the others. If that line is a sentence, the sentence is now a keyword, and the keyword doesn't exist.
I put titles in the markdown above the fence. "Refund policy" is a heading for humans. It is not a line inside the Mermaid file. When someone pastes the heading in with the diagram, the error lands on line 1 and looks like the tool is broken. Move the heading. The tool was reading it.
Direction, for types that have it, belongs on that first line. flowchart TD is a decision you read downward. flowchart LR is a pipeline you read across. I decide this before the nodes exist. Changing it later is legal and it reflows the picture, so the diff looks like a redesign. Say "rotated, not redesigned" in the pull request if you do it, or reviewers will re-read every branch looking for a policy change that isn't there.
One file, one keyword. A class diagram pasted under erDiagram fails in a way that looks like a brace problem. It is a "you changed questions" problem. Split the fences. The format notes cover how those fences sit in a markdown file. The documentation map is how I find the official page for a type without pretending this site reprinted it.
Ids stay put, labels can change
An id is the name arrows point at. A label is the words a reader sees. pay[Send refund] has the id pay and the label "Send refund." I can change the label to "Send the payout" and every arrow that says pay still works. If I used the sentence as the id, every rename edits every arrow, and the diff is noise around a wording tweak.
Ids should be boring tokens. Letters, numbers, maybe an underscore. pay, gate, human. They should not be the words end, subgraph, graph, or the diagram keyword. Those words already mean something. I keep a short list in my head and I still hit end when the step is "end the incident." The id becomes close. The label can say "End the incident" if I quote it when the words need quoting. The id stays dull.
Labels can be long, and they shouldn't be. A label is a box, not a paragraph. If the rule needs a paragraph, the paragraph goes under the figure and the label names the step. I've put a sentence in a diamond. The diamond grew, the exits got hard to see, and the review discussed the line breaks instead of the policy. Shorten the question. Leave the explanation in prose, where line breaks are free.
The same split exists, with different clothes, on other types. A sequence participant can be participant Pay as Payments service. The id is Pay. The display is longer. A state can be state "In review" as rev. A class name is usually both, which is why renaming a class hurts more. An ER entity is the id. There is no second label. Know which type you're in before you "standardize" names across files. Standardizing an ER entity into two words will not parse.
Quotes and the characters that break a line
Brackets, parentheses, and braces are syntax. A label that contains them has to be quoted, or the parser thinks the shape continued. fee[Fee (USD)] looks obvious and often fails. fee["Fee (USD)"] is a rectangle whose text contains parentheses. The quotes are not part of the visible label. They're how you tell the parser "the rest of this is text."
I quote a label when it has parentheses, a colon, a slash I don't mean as a shape, or a question mark outside a diamond. Diamonds already use ? in gate{Over the limit?}. That's the shape, and it's fine. A rectangle that asks a question is a smell anyway. Questions belong in diamonds, where the exits can be labeled.
Quotes are double quotes in the form I trust. I've seen single quotes work and I've seen them break, depending on the line. I don't bet a diagram on the lenient case. If the text itself contains a double quote, rephrase. "Don't say no" can become "Refuse the request." Escaping is a puzzle you will lose at 4 p.m.
Smart quotes from a chat app or a word processor are a different character. They look like quotes. They are not the quotes the parser wants. If a line looks quoted and still fails, retype the quotes. I lost a review to this and blamed a subgraph. The subgraph was fine. The clipboard was fancy.
The guide to syntax errors lists the other characters that bite. This rule is the one to memorize: if the label isn't plain words, quote it. You will quote a few labels that didn't need it. That's cheaper than a blank preview.
Direction is part of the sentence
TD, TB, LR, RL, and BT are directions. Top-down and top-bottom are the same idea. I use top-down for decisions and left-to-right for pipelines. I don't use bottom-up unless I'm matching a picture someone already reads from the floor. Right-to-left is rare in the docs I write, and when it shows up by accident the story runs backwards. I notice it because the terminator is on the right and my eye started on the left anyway.
Direction is not a fix for a parse error. Flipping TD to LR because the preview turned red is how a policy chart becomes a wide mess that still doesn't parse. Repair the line. Then put the direction back to the reading order you meant.
Subgraphs can set their own direction. They can also make a small chart look like two charts glued together. I set a subgraph direction only when a lane is genuinely read the other way, and I say why above the figure. A silent direction change is the kind of "cleanup" that makes a teammate think the branches moved.
Sequence diagrams don't use TD. Participants left to right are the direction, and you set that by declaring them in order. Class diagrams lay out from the relationships. If you miss flowchart direction so much that you try to add TD under classDiagram, you'll get an error, and the error is correct. Don't import rules across types when the types don't share them. Do import the rules this page is about, because those ones do travel: keyword first, ids, quotes, comments, and end.
Comments the renderer skips
%% starts a comment. The rest of the line is for the next editor, not for the reader of the picture. I use comments for a fact that would be dangerous to encode as a node. "Limit is the config value, don't hardcode 50." That sentence must not become a box. A box would look like a step.
%% limit lives in config, do not hardcode it
flowchart TD
ask[Refund request] --> gate{Over the limit?}Comments don't nest, and they don't wrap to the next line by themselves. A second line needs its own %%. A # comment, the kind you use in a shell script, is not a Mermaid comment. The parser will try to read it as syntax. I have "commented out" a node with # and then debugged the hash.
Don't comment out half a relationship and leave the other half. %% pay --> done is fine if the whole line goes. Commenting out only the label produces a line that still has an arrow and no target, or a target and no arrow, depending on where you put the marks. Delete the line, or comment all of it. Git remembers the deletion. You don't need a museum of dead arrows in the file.
I don't use comments to hide a second diagram "for later." Later becomes never, and the file carries a second policy nobody renders. If the other policy is real, it gets its own fence. If it isn't, delete it.
end is a closer, so it cannot be a name
end closes a subgraph. It closes alt, else's block, and loop. That is why a node id named end is a trap. The parser hears "close the block," not "draw a box called end." Sometimes the error points at the next line, which looks innocent. Sometimes the rest of the diagram vanishes into the block you accidentally closed.
The last step of a flowchart can be done([Done]) or stop([Stop]). The label can be the word Done. The id cannot be end. I grep for this before I push, the same way I grep for a conflict marker. It's one token.
In a sequence diagram you need the word end, and you need it as a closer. The bug there is a missing closer or an extra one, not a refusal to type the word. Count openers. One alt and one loop need two end lines, and the inner block closes first. An extra end is as bad as a missing one. I say "close the loop, then close the alt" while I indent, even though the indent isn't the syntax. The end is the syntax. The indent is how I keep the stack straight.
Don't name a participant, a class, a state, or an entity end either. Some of those might parse today. I still won't, because the next person will copy the name into a flowchart subgraph and the file will fall over in a new place. Reserved means reserved. Pick finish, closeOut, terminal. None of those are clever. Clever is how end got into the file.
The path from code to a picture spends more time on this exact failure, with the broken paste written out. I'm not repeating that walkthrough. The rule is the rule. If your diagram's last box is conceptually "the end," you can say so in English in the label, and you still don't get the id.
A small table of rules that travel
I keep this next to a review when someone asks why the same nit shows up on a flowchart and a sequence. It is not complete. It is the part that doesn't change when the keyword changes.
| Rule | What to write | What breaks |
|---|---|---|
| First real line | A diagram keyword, then direction if the type has one | A title sentence, or a # heading, inside the fence |
| Id versus label | pay[Send refund] | Using the whole sentence as the id |
| Quotes | fee["Fee (USD)"] | fee[Fee (USD)] |
| Comment | %% why this limit is not a number | # this is not a comment here |
| Closer | end on its own line, under the block it closes | A node, participant, or state whose id is end |
| One question per file | A second fence for a second diagram type | classDiagram lines under flowchart |
The middle column is the habit. The right column is what I paste into review comments. I don't add a row for color. Color isn't one of the rules that travel, and a diagram can be correct in black and white. Style after the parse is green and the sentence is true.
If a row here conflicts with a type page, the type page wins. ER verbs need a colon. Sequence messages need a colon. Those are type rules that feel like this table and aren't universal. A flowchart arrow doesn't want a colon. Don't sprinkle colons on flowchart labels because the sequence example had them.
The same rules on a flowchart
This is a small refund policy that uses the rules on purpose. The comment is a comment. The direction is top-down because there's a question. The ids are dull. The label with parentheses is quoted. The terminator is done, not end. There is no subgraph here, so there is no closer, and I didn't add an end line "to be safe." A stray closer is an error.
flowchart TD
%% the numeric limit lives in config
ask[Refund request] --> gate{Over the limit?}
gate -->|No| auto[Approve and pay]
gate -->|Yes| human[Ask a reviewer]
human -->|Approved| auto
auto --> done([Done])
human -->|Rejected| note["Email the reason (short)"]Read the joins. Approved and under-the-limit both reach auto, then done. Rejection does not join. That's the policy. The syntax doesn't know whether the join is right. The syntax knows the arrows attach. I still read them out loud, because a file can be valid and false.
note["Email the reason (short)"] is the quoted label. If I drop the quotes, this is the line I'd expect to fail, not the diamond. People debug the diamond because it's more interesting. The parentheses are the interesting bug. The diamond was fine.
If I needed a subgraph around the request and the gate, I would open it, put those two nodes inside, and close with end before auto. I would not name that subgraph end. A subgraph id follows the same "don't steal keywords" rule. subgraph Intake is enough.
The same rules on a sequence
Participants first, so the order is a decision and not an accident. Colons on messages. end only as the closer. No participant called end. The comment above the keyword would be legal. I left it off so the fence shows the cast clearly.
sequenceDiagram
autonumber
participant App as App
participant Bank as Bank
App->>Bank: Authorize the card
alt approved
Bank-->>App: OK
else declined
Bank-->>App: No
endApp and Bank are ids. The as clause repeats them because this example doesn't need a longer display name. When you need one, participant Bank as Acme Bank keeps the id stable. Don't put parentheses in the display name until you've quoted or simplified. I simplify. "Acme Bank" doesn't need a parenthetical version marker.
The colon is the rule that feels type-specific and still belongs in your fingers. App->>Bank Authorize is not a message. It looks like one if you learned the arrow and forgot the rest. I compare a failing sequence to this fence, token by token, before I rewrite the story. Diagram examples has a longer sequence if you need a loop inside an alt. Copy the closers with the same stack discipline. Inner end first.
autonumber is optional and it isn't an excuse to type numbers into the message text. One numbering scheme. If you turn autonumber off, the numbers in the text become the scheme, and they will rot when you insert a step. I prefer the directive, and I delete hand-typed numbers.
The same rules on a class diagram
Class diagrams don't have flowchart direction and they don't close blocks with end in the flowchart sense. They still start with a keyword. They still want ids you can point at. They still break on an unquoted star, which is the class-diagram version of "this character was syntax."
classDiagram
class Invoice {
+int total
+send()
}
class Payment {
+int amount
}
class TeamInvoice {
+string teamName
}
Invoice <|-- TeamInvoice
Invoice "1" --> "*" Payment : settlesInvoice <|-- TeamInvoice puts the hollow triangle on the parent. Reversing the names is valid syntax and the wrong model. The rule that travels is "the parser will not save you from a false sentence." Quotes around "1" and "*" are the rule that travels in spirit: characters that mean something else have to be wrapped. A bare * on that line is composition syntax, and the association fails. I quote both sides even when the one side feels too simple to need it.
+send() keeps the parentheses because a method's parentheses are the marker, and they're inside the class body where that marker is legal. I don't also write +send() void unless the return type is the review. Empty decoration is how a class diagram becomes a listing. The listing belongs in the code. The diagram belongs on the relationships a reviewer might reject.
I didn't name a class end. I wouldn't name one class either. If the domain word really is "end," as in an end-of-day batch, the class can be EndOfDay and the humans can cope. The keyword can stay a keyword.
Failures that are your line, not the tool
When the preview goes red, I assume the line is wrong before I assume the renderer is wrong. The editor is free, it doesn't require an account, and the preview is the fastest parse. I still get the order of checks wrong when I'm annoyed. The order that works:
- First real line is a keyword this renderer knows. Not a title. Not
useCaseDiagram, which is not a type. - The line number in the message, then the lines above it if the message mentions
endor a block. - Quotes around any label that isn't plain words.
- No id named
end. - Direction only after the file parses.
Then I look at meaning. A green preview can still join the wrong branches, point inheritance at the child, or draw a crow on the parent. Syntax rules get you a picture. They don't get you a true picture. I read the picture out loud. If I can't, the line isn't ready, even though it compiled.
A mismatch between two viewers is usually the text, including a smart quote, or a version gap I'm not going to guess at. Paste the smallest example that should work. If the smallest example fails, say so and include the error. If it passes, your file is the delta. Don't restyle the diagram while you're in that hole. Color will not legalize a missing quote.
I keep the repaired source in git. I don't keep a mental note that "the editor fixes it." The editor previews. The file is the syntax. The next person, and the next CI job, will read the file.
Open the editor and type the keyword before you type a single node. When it fails, change one token and look again. If you change the direction, the quotes, and the ids in one save, you won't know which rule you just learned, and you'll get to learn it again next week.
Related posts
Frequently asked questions
What is the first line of every diagram?
A keyword such as flowchart, sequenceDiagram, or erDiagram. A title sentence before that keyword is a parse error. Put the title in a comment or in the prose.
When do I quote a label?
When it contains parentheses, commas, or other punctuation the parser would treat as syntax. A["Price (USD)"] is the safe form.
Is the cheat sheet the same page?
No. The cheat sheet is the token list. This page is the rules that explain why a token failed.